├── requirements.txt ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.yml │ └── bug_report.yml ├── FUNDING.yml ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml ├── CONTRIBUTING.md ├── workflows │ ├── publish.yml │ └── test.yml └── CODE_OF_CONDUCT.md ├── otherfiles ├── logo.png ├── requirements-splitter.py ├── RELEASE.md └── version_check.py ├── MANIFEST.in ├── pytest.ini ├── mycoffee ├── __init__.py ├── __main__.py ├── params.py └── functions.py ├── dev-requirements.txt ├── .coveragerc ├── AUTHORS.md ├── autopep8.bat ├── autopep8.sh ├── SECURITY.md ├── LICENSE ├── .gitignore ├── setup.py ├── METHODS.md ├── CHANGELOG.md ├── README.md └── test ├── functions_test.py ├── cli_test.py └── verified_test.py /requirements.txt: -------------------------------------------------------------------------------- 1 | pyyaml>=3.12 2 | art>=5.3 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://github.com/sepandhaghighi/mycoffee#show-your-support -------------------------------------------------------------------------------- /otherfiles/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sepandhaghighi/mycoffee/HEAD/otherfiles/logo.png -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include *.md 3 | include *.spec 4 | include *.txt 5 | include *.yml 6 | include *.ini -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | # content of pytest.ini 2 | [pytest] 3 | addopts = --doctest-modules 4 | doctest_optionflags= NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL ELLIPSIS -------------------------------------------------------------------------------- /mycoffee/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """mycoffee modules.""" 3 | from mycoffee.params import MY_COFFEE_VERSION 4 | __version__ = MY_COFFEE_VERSION 5 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | #### Reference Issues/PRs 2 | 3 | #### What does this implement/fix? Explain your changes. 4 | 5 | #### Any other comments? 6 | 7 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | pyyaml==6.0.3 2 | art==6.5 3 | pytest>=4.3.1 4 | pytest-cov>=2.6.1 5 | setuptools>=40.8.0 6 | vulture>=1.0 7 | bandit>=1.5.1 8 | pydocstyle>=3.0.0 9 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | omit = 4 | */mycoffee/__main__.py 5 | */mycoffee/__init__.py 6 | [report] 7 | # Regexes for lines to exclude from consideration 8 | exclude_lines = 9 | pragma: no cover 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | time: "01:30" 8 | open-pull-requests-limit: 10 9 | target-branch: dev 10 | assignees: 11 | - "sepandhaghighi" 12 | -------------------------------------------------------------------------------- /otherfiles/requirements-splitter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Requirements splitter.""" 3 | 4 | test_req = "" 5 | 6 | with open('dev-requirements.txt', 'r') as f: 7 | for line in f: 8 | if '==' not in line: 9 | test_req += line 10 | 11 | with open('test-requirements.txt', 'w') as f: 12 | f.write(test_req) 13 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | # Core Developers 2 | ---------- 3 | - [@sepandhaghighi](http://github.com/sepandhaghighi) 4 | 5 | 6 | # Other Contributors 7 | ---------- 8 | - [@boreshnavard](https://github.com/boreshnavard) ** 9 | - [@AHReccese](https://github.com/AHReccese) 10 | - [@sadrasabouri](https://github.com/sadrasabouri) 11 | 12 | 13 | ** Graphic designer 14 | -------------------------------------------------------------------------------- /autopep8.bat: -------------------------------------------------------------------------------- 1 | python -m autopep8 mycoffee --recursive --aggressive --aggressive --in-place --pep8-passes 2000 --max-line-length 120 --verbose 2 | python -m autopep8 otherfiles --recursive --aggressive --aggressive --in-place --pep8-passes 2000 --max-line-length 120 --verbose 3 | python -m autopep8 setup.py --recursive --aggressive --aggressive --in-place --pep8-passes 2000 --max-line-length 120 --verbose 4 | -------------------------------------------------------------------------------- /autopep8.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | python -m autopep8 mycoffee --recursive --aggressive --aggressive --in-place --pep8-passes 2000 --max-line-length 120 --verbose 3 | python -m autopep8 otherfiles --recursive --aggressive --aggressive --in-place --pep8-passes 2000 --max-line-length 120 --verbose 4 | python -m autopep8 setup.py --recursive --aggressive --aggressive --in-place --pep8-passes 2000 --max-line-length 120 --verbose 5 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------------- | ------------------ | 7 | | 2.1 | :white_check_mark: | 8 | | < 2.1 | :x: | 9 | 10 | ## Reporting a Vulnerability 11 | 12 | Please report security vulnerabilities by email to [me@sepand.tech](mailto:me@sepand.tech "me@sepand.tech"). 13 | 14 | If the security vulnerability is accepted, a dedicated bugfix release will be issued as soon as possible (depending on the complexity of the fix). -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution 2 | 3 | Changes and improvements are more than welcome! ❤️ Feel free to fork and open a pull request. 4 | 5 | 6 | Please consider the following : 7 | 8 | 9 | 1. Fork it! 10 | 2. Create your feature branch (under `dev` branch) 11 | 3. Add your functions/methods to proper files 12 | 4. Add standard `docstring` to your functions/methods 13 | 5. Pass all CI tests 14 | 6. Update `CHANGELOG.md` 15 | - Describe changes under `[Unreleased]` section 16 | 7. Submit a pull request into `dev` (please complete the pull request template) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Sepand Haghighi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest a feature for this project 3 | title: "[Feature]: " 4 | body: 5 | - type: textarea 6 | id: description 7 | attributes: 8 | label: Describe the feature you want to add 9 | placeholder: > 10 | I'd like to be able to [...] 11 | validations: 12 | required: true 13 | - type: textarea 14 | id: possible-solution 15 | attributes: 16 | label: Describe your proposed solution 17 | placeholder: > 18 | I think this could be done by [...] 19 | validations: 20 | required: false 21 | - type: textarea 22 | id: alternatives 23 | attributes: 24 | label: Describe alternatives you've considered, if relevant 25 | placeholder: > 26 | Another way to do this would be [...] 27 | validations: 28 | required: false 29 | - type: textarea 30 | id: additional-context 31 | attributes: 32 | label: Additional context 33 | placeholder: > 34 | Add any other context or screenshots about the feature request here. 35 | validations: 36 | required: false 37 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | push: 8 | # Sequence of patterns matched against refs/tags 9 | tags: 10 | - '*' # Push events to matching v*, i.e. v1.0, v20.15.10 11 | 12 | jobs: 13 | deploy: 14 | 15 | runs-on: ubuntu-22.04 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Set up Python 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: '3.x' 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | pip install setuptools wheel twine 27 | - name: Build and publish 28 | env: 29 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 30 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 31 | run: | 32 | python setup.py sdist bdist_wheel 33 | twine upload dist/*.tar.gz 34 | twine upload dist/*.whl 35 | -------------------------------------------------------------------------------- /otherfiles/RELEASE.md: -------------------------------------------------------------------------------- 1 | # MyCoffee Release Instructions 2 | 3 | **Last Update: 2025-08-17** 4 | 5 | 1. Create the `release` branch under `dev` 6 | 2. Update all version tags 7 | 1. `setup.py` 8 | 2. `README.md` 9 | 3. `SECURITY.md` 10 | 4. `otherfiles/version_check.py` 11 | 5. `mycoffee/params.py` 12 | 6. `test/cli_test.py` 13 | 3. Update `CHANGELOG.md` 14 | 1. Add a new header under `Unreleased` section (Example: `## [0.1] - 2022-08-17`) 15 | 2. Add a new compare link to the end of the file (Example: `[0.2]: https://github.com/sepandhaghighi/mycoffee/compare/v0.1...v0.2`) 16 | 3. Update `dev` compare link (Example: `[Unreleased]: https://github.com/sepandhaghighi/mycoffee/compare/v0.2...dev`) 17 | 4. Update `.github/ISSUE_TEMPLATE/bug_report.yml` 18 | 1. Add new version tag to `MyCoffee version` dropbox options 19 | 5. Create a PR from `release` to `dev` 20 | 1. Title: `Version x.x` (Example: `Version 0.1`) 21 | 2. Tag all related issues 22 | 3. Labels: `release` 23 | 4. Set milestone 24 | 5. Wait for all CI pass 25 | 6. Need review 26 | 7. Squash and merge 27 | 8. Delete `release` branch 28 | 6. Merge `dev` branch into `main` 29 | 1. `git checkout main` 30 | 2. `git merge dev` 31 | 3. `git push origin main` 32 | 4. Wait for all CI pass 33 | 7. Create a new release 34 | 1. Target branch: `main` 35 | 2. Tag: `vx.x` (Example: `v0.1`) 36 | 3. Title: `Version x.x` (Example: `Version 0.1`) 37 | 4. Copy changelogs 38 | 5. Tag all related issues 39 | 8. Bump!! 40 | 9. Close this version issues 41 | 10. Close milestone 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # dotenv 80 | .env 81 | 82 | # virtualenv 83 | .venv/ 84 | venv/ 85 | ENV/ 86 | 87 | # Spyder project settings 88 | .spyderproject 89 | 90 | # Rope project settings 91 | .ropeproject 92 | ### Example user template template 93 | ### Example user template 94 | 95 | # IntelliJ project files 96 | .idea 97 | *.iml 98 | out 99 | gen 100 | 101 | # Test files 102 | save_test*.txt 103 | save_test*.json 104 | save_test*.yaml 105 | -------------------------------------------------------------------------------- /otherfiles/version_check.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Version-check script.""" 3 | import os 4 | import sys 5 | import codecs 6 | 7 | Failed = 0 8 | VERSION = "2.1" 9 | 10 | README_ITEMS = [ 11 | "[Version {0}](https://github.com/sepandhaghighi/mycoffee/archive/v{0}.zip)", 12 | "pip install mycoffee=={0}"] 13 | 14 | SETUP_ITEMS = [ 15 | "version='{0}'", 16 | "https://github.com/sepandhaghighi/mycoffee/tarball/v{0}"] 17 | 18 | CHANGELOG_ITEMS = [ 19 | "## [{0}]", 20 | "https://github.com/sepandhaghighi/mycoffee/compare/v{0}...dev", 21 | "[{0}]:"] 22 | 23 | PARAMS_ITEMS = ['MY_COFFEE_VERSION = "{0}"'] 24 | ISSUE_TEMPLATE_ITEMS = ["- MyCoffee {0}"] 25 | SECURITY_ITEMS = ["| {0} | :white_check_mark: |", "| < {0} | :x: |"] 26 | 27 | FILES = { 28 | "setup.py": SETUP_ITEMS, 29 | "README.md": README_ITEMS, 30 | "CHANGELOG.md": CHANGELOG_ITEMS, 31 | "SECURITY.md": SECURITY_ITEMS, 32 | os.path.join( 33 | "mycoffee", 34 | "params.py"): PARAMS_ITEMS, 35 | os.path.join( 36 | ".github", 37 | "ISSUE_TEMPLATE", 38 | "bug_report.yml"): ISSUE_TEMPLATE_ITEMS, 39 | } 40 | 41 | TEST_NUMBER = len(FILES) 42 | 43 | 44 | def print_result(failed: bool = False) -> None: 45 | """ 46 | Print final result. 47 | 48 | :param failed: failed flag 49 | """ 50 | message = "Version tag tests " 51 | if not failed: 52 | print("\n" + message + "passed!") 53 | else: 54 | print("\n" + message + "failed!") 55 | print("Passed : " + str(TEST_NUMBER - Failed) + "/" + str(TEST_NUMBER)) 56 | 57 | 58 | if __name__ == "__main__": 59 | for file_name in FILES: 60 | try: 61 | file_content = codecs.open( 62 | file_name, "r", "utf-8", "ignore").read() 63 | for test_item in FILES[file_name]: 64 | if file_content.find(test_item.format(VERSION)) == -1: 65 | print("Incorrect version tag in " + file_name) 66 | Failed += 1 67 | break 68 | except Exception as e: 69 | Failed += 1 70 | print("Error in " + file_name + "\n" + "Message : " + str(e)) 71 | if Failed == 0: 72 | print_result(False) 73 | sys.exit(0) 74 | else: 75 | print_result(True) 76 | sys.exit(1) 77 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: CI 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | - dev 11 | 12 | pull_request: 13 | branches: 14 | - main 15 | - dev 16 | 17 | env: 18 | TEST_PYTHON_VERSION: 3.9 19 | TEST_OS: 'ubuntu-22.04' 20 | 21 | jobs: 22 | build: 23 | 24 | runs-on: ${{ matrix.os }} 25 | strategy: 26 | fail-fast: false 27 | matrix: 28 | os: [ubuntu-22.04, windows-2022, macos-15-intel] 29 | python-version: [3.7, 3.8, 3.9, 3.10.5, 3.11.0, 3.12.0, 3.13.0] 30 | steps: 31 | - uses: actions/checkout@v4 32 | - name: Set up Python ${{ matrix.python-version }} 33 | uses: actions/setup-python@v5 34 | with: 35 | python-version: ${{ matrix.python-version }} 36 | - name: Installation 37 | run: | 38 | python -m pip install --upgrade pip 39 | pip install . 40 | - name: First test 41 | run: | 42 | mycoffee --version 43 | mycoffee --method=chemex --water=20 --cups=3 --coffee-ratio=2 --water-ratio=37 --message="Temp: 92 C" 44 | mycoffee --method=chemex --water=20 --cups=3 --coffee-ratio=2 --water-ratio=37 --message="Temp: 92 C" --ignore-warnings 45 | mycoffee --method=chemex --coffee=2 --cups=3 --coffee-ratio=2 --water-ratio=37 --message="Temp: 92 C" --mode="coffee-to-water" 46 | - name: Install dev-requirements 47 | run: | 48 | python otherfiles/requirements-splitter.py 49 | pip install --upgrade --upgrade-strategy=only-if-needed -r test-requirements.txt 50 | - name: Version check 51 | run: | 52 | python otherfiles/version_check.py 53 | if: matrix.python-version == env.TEST_PYTHON_VERSION 54 | - name: Test with pytest 55 | run: | 56 | python -m pytest test --cov=mycoffee --cov-report=term 57 | - name: Other tests 58 | run: | 59 | python -m vulture mycoffee/ setup.py --min-confidence 65 --exclude=__init__.py --sort-by-size 60 | python -m bandit -r mycoffee -s B311 61 | python -m pydocstyle --match-dir=mycoffee -v 62 | if: matrix.python-version == env.TEST_PYTHON_VERSION && matrix.os == env.TEST_OS 63 | - name: Upload coverage to Codecov 64 | uses: codecov/codecov-action@v4 65 | with: 66 | fail_ci_if_error: true 67 | token: ${{ secrets.CODECOV_TOKEN }} 68 | if: matrix.python-version == env.TEST_PYTHON_VERSION && matrix.os == env.TEST_OS 69 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Setup module.""" 3 | from typing import List 4 | try: 5 | from setuptools import setup 6 | except ImportError: 7 | from distutils.core import setup 8 | 9 | 10 | def get_requires() -> List[str]: 11 | """Read requirements.txt.""" 12 | requirements = open("requirements.txt", "r").read() 13 | return list(filter(lambda x: x != "", requirements.split())) 14 | 15 | 16 | def read_description() -> str: 17 | """Read README.md and CHANGELOG.md.""" 18 | try: 19 | with open("README.md") as r: 20 | description = "\n" 21 | description += r.read() 22 | with open("CHANGELOG.md") as c: 23 | description += "\n" 24 | description += c.read() 25 | return description 26 | except Exception: 27 | return '''Brew Perfect Coffee Right from Your Terminal''' 28 | 29 | 30 | setup( 31 | name='mycoffee', 32 | packages=['mycoffee'], 33 | version='2.1', 34 | description='Brew Perfect Coffee Right from Your Terminal', 35 | long_description=read_description(), 36 | long_description_content_type='text/markdown', 37 | include_package_data=True, 38 | author='Sepand Haghighi', 39 | author_email='me@sepand.tech', 40 | url='https://github.com/sepandhaghighi/mycoffee', 41 | download_url='https://github.com/sepandhaghighi/mycoffee/tarball/v2.1', 42 | keywords="coffee ratio terminal brew calculator cli", 43 | project_urls={ 44 | 'Source': 'https://github.com/sepandhaghighi/mycoffee' 45 | }, 46 | install_requires=get_requires(), 47 | python_requires='>=3.7', 48 | classifiers=[ 49 | 'Development Status :: 5 - Production/Stable', 50 | 'Natural Language :: English', 51 | 'License :: OSI Approved :: MIT License', 52 | 'Operating System :: OS Independent', 53 | 'Programming Language :: Python :: 3.7', 54 | 'Programming Language :: Python :: 3.8', 55 | 'Programming Language :: Python :: 3.9', 56 | 'Programming Language :: Python :: 3.10', 57 | 'Programming Language :: Python :: 3.11', 58 | 'Programming Language :: Python :: 3.12', 59 | 'Programming Language :: Python :: 3.13', 60 | 'Intended Audience :: Developers', 61 | 'Intended Audience :: Education', 62 | 'Intended Audience :: End Users/Desktop', 63 | 'Intended Audience :: Other Audience', 64 | 'Topic :: Games/Entertainment', 65 | 'Topic :: Utilities', 66 | ], 67 | license='MIT', 68 | entry_points={ 69 | 'console_scripts': [ 70 | 'mycoffee = mycoffee.__main__:main', 71 | ] 72 | } 73 | ) 74 | -------------------------------------------------------------------------------- /mycoffee/__main__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """mycoffee main.""" 3 | from mycoffee.params import METHODS_MAP, EXIT_MESSAGE, FILE_FORMATS_LIST, MODES_LIST 4 | from mycoffee.params import COFFEE_UNITS_MAP, WATER_UNITS_MAP, TEMPERATURE_UNITS_MAP 5 | from mycoffee.functions import run_program, validate_positive_int, validate_positive_float 6 | import argparse 7 | 8 | 9 | def main() -> None: 10 | """CLI main function.""" 11 | parser = argparse.ArgumentParser() 12 | parser.add_argument( 13 | '--method', 14 | help='brewing method', 15 | type=str.lower, 16 | choices=sorted(METHODS_MAP), 17 | default="custom") 18 | parser.add_argument('--message', help='extra information about the brewing method', type=str) 19 | parser.add_argument( 20 | '--coffee-ratio', 21 | help='coefficient for the coffee component in the ratio', 22 | type=validate_positive_float) 23 | parser.add_argument( 24 | '--water-ratio', 25 | help='coefficient for the water component in the ratio', 26 | type=validate_positive_float) 27 | parser.add_argument('--water', help='amount of water in each cup', type=validate_positive_float) 28 | parser.add_argument('--coffee', help='amount of coffee in each cup', type=validate_positive_float) 29 | parser.add_argument('--cups', help='number of cups', type=validate_positive_int) 30 | parser.add_argument('--grind', help='grind size (um)', type=validate_positive_int) 31 | parser.add_argument('--temperature', help='brewing temperature', type=float) 32 | parser.add_argument( 33 | '--digits', 34 | help='number of digits up to which the result is rounded', 35 | type=int, 36 | default=3) 37 | parser.add_argument( 38 | '--coffee-unit', 39 | help='coffee unit', 40 | type=str.lower, 41 | choices=sorted(COFFEE_UNITS_MAP), 42 | default="g") 43 | parser.add_argument('--water-unit', help='water unit', type=str.lower, choices=sorted(WATER_UNITS_MAP), default="g") 44 | parser.add_argument( 45 | '--temperature-unit', 46 | help='temperature unit', 47 | type=str.upper, 48 | choices=sorted(TEMPERATURE_UNITS_MAP), 49 | default="C") 50 | parser.add_argument('--coffee-units-list', help='coffee units list', nargs="?", const=1) 51 | parser.add_argument('--water-units-list', help='water units list', nargs="?", const=1) 52 | parser.add_argument('--temperature-units-list', help='temperature units list', nargs="?", const=1) 53 | parser.add_argument('--methods-list', help='brewing methods list', nargs="?", const=1) 54 | parser.add_argument('--version', help='version', nargs="?", const=1) 55 | parser.add_argument('--info', help='info', nargs="?", const=1) 56 | parser.add_argument('--ignore-warnings', help='ignore warnings', nargs="?", const=1) 57 | parser.add_argument('--mode', help='conversion mode', type=str.lower, choices=MODES_LIST, default="water-to-coffee") 58 | parser.add_argument('--save-path', help='file path to save', type=str) 59 | parser.add_argument('--save-format', help='file format', type=str.lower, choices=FILE_FORMATS_LIST, default="text") 60 | args = parser.parse_args() 61 | try: 62 | run_program(args) 63 | except (KeyboardInterrupt, EOFError): 64 | print(EXIT_MESSAGE) 65 | 66 | 67 | if __name__ == "__main__": 68 | main() 69 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | title: "[Bug]: " 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for your time to fill out this bug report! 9 | - type: input 10 | id: contact 11 | attributes: 12 | label: Contact details 13 | description: How can we get in touch with you if we need more info? 14 | placeholder: ex. email@example.com 15 | validations: 16 | required: false 17 | - type: textarea 18 | id: what-happened 19 | attributes: 20 | label: What happened? 21 | description: Provide a clear and concise description of what the bug is. 22 | placeholder: > 23 | Tell us a description of the bug. 24 | validations: 25 | required: true 26 | - type: textarea 27 | id: step-to-reproduce 28 | attributes: 29 | label: Steps to reproduce 30 | description: Provide details of how to reproduce the bug. 31 | placeholder: > 32 | ex. 1. Go to '...' 33 | validations: 34 | required: true 35 | - type: textarea 36 | id: expected-behavior 37 | attributes: 38 | label: Expected behavior 39 | description: What did you expect to happen? 40 | placeholder: > 41 | ex. I expected '...' to happen 42 | validations: 43 | required: true 44 | - type: textarea 45 | id: actual-behavior 46 | attributes: 47 | label: Actual behavior 48 | description: What did actually happen? 49 | placeholder: > 50 | ex. Instead '...' happened 51 | validations: 52 | required: true 53 | - type: dropdown 54 | id: operating-system 55 | attributes: 56 | label: Operating system 57 | description: Which operating system are you using? 58 | options: 59 | - Windows 60 | - macOS 61 | - Linux 62 | default: 0 63 | validations: 64 | required: true 65 | - type: dropdown 66 | id: python-version 67 | attributes: 68 | label: Python version 69 | description: Which version of Python are you using? 70 | options: 71 | - Python 3.13 72 | - Python 3.12 73 | - Python 3.11 74 | - Python 3.10 75 | - Python 3.9 76 | - Python 3.8 77 | - Python 3.7 78 | - Python 3.6 79 | default: 1 80 | validations: 81 | required: true 82 | - type: dropdown 83 | id: mycoffee-version 84 | attributes: 85 | label: MyCoffee version 86 | description: Which version of MyCoffee are you using? 87 | options: 88 | - MyCoffee 2.1 89 | - MyCoffee 2.0 90 | - MyCoffee 1.9 91 | - MyCoffee 1.8 92 | - MyCoffee 1.7 93 | - MyCoffee 1.6 94 | - MyCoffee 1.5 95 | - MyCoffee 1.4 96 | - MyCoffee 1.3 97 | - MyCoffee 1.2 98 | - MyCoffee 1.1 99 | - MyCoffee 1.0 100 | - MyCoffee 0.9 101 | - MyCoffee 0.8 102 | - MyCoffee 0.7 103 | - MyCoffee 0.6 104 | - MyCoffee 0.5 105 | - MyCoffee 0.4 106 | - MyCoffee 0.3 107 | - MyCoffee 0.2 108 | - MyCoffee 0.1 109 | default: 0 110 | validations: 111 | required: true 112 | - type: textarea 113 | id: logs 114 | attributes: 115 | label: Relevant log output 116 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. 117 | render: shell 118 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at me@sepand.tech. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /METHODS.md: -------------------------------------------------------------------------------- 1 | # Methods List 2 | 3 | **Last Update: 2025-09-22** 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 |
TitleCodeRatioRatio-LLRatio-ULGrind-LL(um)Grind-UL(um)Temperature-LL(C)Temperature-UL(C)Water(g)Coffee(g)Version
Customcustom1/17------------24014.118>=0.1
V60v603/501/181/14400700859525015>=0.1
Chemexchemex1/151/211/10410930859524016>=0.1
Espressoespresso1/21/2.51/1.518038085953618>=0.1
French pressfrench-press1/151/181/12690130085951208>=0.1
Siphonsiphon1/151/161/12375800919424016>=0.1
Pour-overpour-over1/151/161/14410930909324016>=0.2
Auto dripauto-drip1/161/171/1430090090961288>=0.2
Cold brewcold-brew1/111/151/8800140004024222>=0.2
Cold brew concentratecold-brew-conc1/51/61/4800140004012024>=0.2
Moka potmoka-pot1/101/121/73606608595606>=0.2
Ristrettoristretto1/11/1.51/118038085951818>=0.3
Lungolungo1/41/41/2.518038085957218>=0.3
Turkishturkish1/101/121/8402209095505>=0.3
Cuppingcupping11/2001/191/1746085085951508.25>=0.3
AeroPress standardaero-press1/151/181/1232096090951359>=0.4
AeroPress concentrateaero-press-conc1/61/71/532096090959015>=0.4
AeroPress invertedaero-press-inv1/121/141/10320960909513211>=0.4
Steep-and-releasesteep-and-release1/161/171/14450825859525515.9375>=0.4
Clever dripperclever-dripper1/16.671/201/15400800919625014.997>=1.9
Phin filterphin-filter1/21/41/220040090967236>=1.9
Kalita wavekalita-wave1/161/171/158001000909640025>=2.1
Instant coffeeinstant-coffee1/351/501/15----80931755>=2.1
343 | 344 | **Notes**: 345 | 346 | - *LL: Lower Limit* 347 | - *UL: Upper Limit* 348 | - *g: Gram* 349 | - *um: Micrometer* 350 | - *C: Degree Celsius* 351 | 352 | 353 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | ## [2.1] - 2025-10-08 9 | ### Added 10 | - 2 new methods 11 | 1. Kalita wave 12 | 2. Instant coffee 13 | - `get_date_now` function 14 | - `format_date` function 15 | ### Changed 16 | - Test system modified 17 | - `README.md` updated 18 | ## [2.0] - 2025-08-27 19 | ### Added 20 | - `YAML` format 21 | - `ratio` mode 22 | ### Changed 23 | - Test system modified 24 | - `README.md` updated 25 | - `calc_coffee` function renamed to `calculate_coffee` 26 | - `calc_water` function renamed to `calculate_water` 27 | - `run` function renamed to `run_program` 28 | ## [1.9] - 2025-06-21 29 | ### Added 30 | - 2 new methods 31 | 1. Clever dripper 32 | 2. Phin filter 33 | - `--coffee` argument 34 | - `--mode` argument 35 | ### Changed 36 | - Test system modified 37 | - `README.md` updated 38 | - `METHODS.md` updated 39 | ## [1.8] - 2025-05-02 40 | ### Changed 41 | - `get_result` function modified 42 | - `filter_params` function modified 43 | - `mycoffee_info` function modified 44 | - `coffee` parameter splitted to `coffee[cup]` and `coffee[total]` 45 | - `water` parameter splitted to `water[cup]` and `water[total]` 46 | - `check_ratio_limits` function parameters updated 47 | - `check_temperature_limits` function parameters updated 48 | - `check_grind_limits` function parameters updated 49 | - `calc_coffee` function parameters updated 50 | - Test system modified 51 | - Warning messages updated 52 | - `Python 3.6` support dropped 53 | ## [1.7] - 2025-03-26 54 | ### Added 55 | - `ratio` parameter 56 | - `strength` parameter 57 | ### Changed 58 | - Python typing features added to all modules 59 | - Test system modified 60 | - `README.md` updated 61 | ## [1.6] - 2025-03-12 62 | ### Added 63 | - `--save-path` argument 64 | - `--save-format` argument 65 | ### Changed 66 | - Warning functions modified 67 | - Result functions modified 68 | - Input case sensitivity bug fixed 69 | - Temperature bug fixed 70 | - Test system modified 71 | - `README.md` updated 72 | ## [1.5] - 2025-02-26 73 | ### Added 74 | - `--info` argument 75 | - `--ignore-warnings` argument 76 | - `--temperature-unit` argument 77 | - `--temperature-units-list` argument 78 | ### Changed 79 | - Test system modified 80 | - `README.md` updated 81 | ## [1.4] - 2025-02-15 82 | ### Added 83 | - `--temperature` argument 84 | - Temperature upper limit 85 | - Temperature lower limit 86 | ### Changed 87 | - `--info` argument renamed to `--message` 88 | - Warning messages updated 89 | - `README.md` updated 90 | - `METHODS.md` updated 91 | ## [1.3] - 2025-01-31 92 | ### Added 93 | - `get_grind_type` function 94 | - `validate_positive_int` function 95 | - `validate_positive_float` function 96 | ### Changed 97 | - `README.md` updated 98 | - `check_ratio_limits` function bug fixed 99 | - String templates modified 100 | - Test system modified 101 | ## [1.2] - 2025-01-13 102 | ### Added 103 | - 2 new water units 104 | 1. Troy Pounds (`t lb`) 105 | 2. Pennyweight (`dwt`) 106 | - 2 new coffee units 107 | 1. Troy Pounds (`t lb`) 108 | 2. Pennyweight (`dwt`) 109 | - `--grind` argument 110 | - Grind upper limit 111 | - Grind lower limit 112 | - `check_grind_limits` function 113 | ### Changed 114 | - `README.md` updated 115 | - `METHODS.md` updated 116 | ## [1.1] - 2025-01-02 117 | ### Added 118 | - 5 new water units 119 | 1. Troy Ounce (`t oz`) 120 | 2. Grain (`gr`) 121 | 3. Carats (`ct`) 122 | 4. Cubic Centimeters (`cc`) 123 | 5. Centiliters (`cl`) 124 | - 3 new coffee units 125 | 1. Troy Ounce (`t oz`) 126 | 2. Grain (`gr`) 127 | 3. Carats (`ct`) 128 | ### Changed 129 | - `README.md` updated 130 | ## [1.0] - 2024-12-17 131 | ### Added 132 | - 3 new water units 133 | 1. Pint (`pt`) 134 | 2. Quart (`qt`) 135 | 3. Fluid Ounce (`fl oz`) 136 | ### Changed 137 | - `README.md` updated 138 | ## [0.9] - 2024-12-06 139 | ### Added 140 | - 4 new water units 141 | 1. Tablespoon (`tbsp`) 142 | 2. Teaspoon (`tsp`) 143 | 3. Dessertspoon (`dsp`) 144 | 4. Cup (`cup`) 145 | ## [0.8] - 2024-11-29 146 | ### Added 147 | - 1 new coffee unit 148 | 1. Cup (`cup`) 149 | - 6 new water units 150 | 1. Milliliter (`ml`) 151 | 2. Liter (`l`) 152 | 3. Ounce (`oz`) 153 | 4. Pound (`lb`) 154 | 5. Milligram (`mg`) 155 | 6. Kilogram (`kg`) 156 | - `convert_water` function 157 | - `show_water_units_list` function 158 | - `--water-unit` argument 159 | ### Changed 160 | - Test system modified 161 | - `README.md` updated 162 | ## [0.7] - 2024-11-21 163 | ### Added 164 | - 4 new coffee units 165 | 1. Coffee beans (`cb`) 166 | 2. Tablespoon (`tbsp`) 167 | 3. Teaspoon (`tsp`) 168 | 4. Dessertspoon (`dsp`) 169 | - `convert_coffee` function 170 | ### Changed 171 | - GitHub actions are limited to the `dev` and `main` branches 172 | ## [0.6] - 2024-10-18 173 | ### Added 174 | - `show_coffee_units_list` function 175 | - `--coffee-unit` argument 176 | ### Changed 177 | - Test system modified 178 | - Cups bug fixed 179 | - `calc_coffee` function updated 180 | - `README.md` updated 181 | - `Python 3.13` added to `test.yml` 182 | ## [0.5] - 2024-10-08 183 | ### Added 184 | - Ratio upper limit 185 | - Ratio lower limit 186 | - `check_ratio_limits` function 187 | ### Changed 188 | - Test system modified 189 | - `print_message` function renamed to `print_result` 190 | ## [0.4] - 2024-10-01 191 | ### Added 192 | - 4 new methods 193 | 1. AeroPress standard 194 | 2. AeroPress concentrate 195 | 3. AeroPress inverted 196 | 4. Steep-and-release 197 | - `--digits` argument 198 | ### Changed 199 | - `README.md` updated 200 | - Test system modified 201 | - `filter_params` function updated 202 | ## [0.3] - 2024-09-24 203 | ### Added 204 | - Logo 205 | - 4 new methods 206 | 1. Ristretto 207 | 2. Lungo 208 | 3. Turkish 209 | 4. Cupping 210 | ## [0.2] - 2024-09-17 211 | ### Added 212 | - 5 new methods 213 | 1. Pour-over 214 | 2. Auto drip 215 | 3. Cold brew 216 | 4. Cold brew concentrate 217 | 5. Moka pot 218 | - `is_int` function 219 | - `filter_params` function 220 | ### Changed 221 | - `README.md` updated 222 | - `--coffee-ratio` type changed from `int` to `float` 223 | - `--water-ratio` type changed from `int` to `float` 224 | - `coffee_calc` function renamed to `calc_coffee` 225 | - `print_message` function updated 226 | - Test system modified 227 | ## [0.1] - 2024-09-02 228 | ### Added 229 | - 6 new methods 230 | 1. V60 231 | 2. Espresso 232 | 3. Chemex 233 | 4. French-press 234 | 5. Siphon 235 | 6. Custom 236 | 237 | [Unreleased]: https://github.com/sepandhaghighi/mycoffee/compare/v2.1...dev 238 | [2.1]: https://github.com/sepandhaghighi/mycoffee/compare/v2.0...v2.1 239 | [2.0]: https://github.com/sepandhaghighi/mycoffee/compare/v1.9...v2.0 240 | [1.9]: https://github.com/sepandhaghighi/mycoffee/compare/v1.8...v1.9 241 | [1.8]: https://github.com/sepandhaghighi/mycoffee/compare/v1.7...v1.8 242 | [1.7]: https://github.com/sepandhaghighi/mycoffee/compare/v1.6...v1.7 243 | [1.6]: https://github.com/sepandhaghighi/mycoffee/compare/v1.5...v1.6 244 | [1.5]: https://github.com/sepandhaghighi/mycoffee/compare/v1.4...v1.5 245 | [1.4]: https://github.com/sepandhaghighi/mycoffee/compare/v1.3...v1.4 246 | [1.3]: https://github.com/sepandhaghighi/mycoffee/compare/v1.2...v1.3 247 | [1.2]: https://github.com/sepandhaghighi/mycoffee/compare/v1.1...v1.2 248 | [1.1]: https://github.com/sepandhaghighi/mycoffee/compare/v1.0...v1.1 249 | [1.0]: https://github.com/sepandhaghighi/mycoffee/compare/v0.9...v1.0 250 | [0.9]: https://github.com/sepandhaghighi/mycoffee/compare/v0.8...v0.9 251 | [0.8]: https://github.com/sepandhaghighi/mycoffee/compare/v0.7...v0.8 252 | [0.7]: https://github.com/sepandhaghighi/mycoffee/compare/v0.6...v0.7 253 | [0.6]: https://github.com/sepandhaghighi/mycoffee/compare/v0.5...v0.6 254 | [0.5]: https://github.com/sepandhaghighi/mycoffee/compare/v0.4...v0.5 255 | [0.4]: https://github.com/sepandhaghighi/mycoffee/compare/v0.3...v0.4 256 | [0.3]: https://github.com/sepandhaghighi/mycoffee/compare/v0.2...v0.3 257 | [0.2]: https://github.com/sepandhaghighi/mycoffee/compare/v0.1...v0.2 258 | [0.1]: https://github.com/sepandhaghighi/mycoffee/compare/c2d0bb4...v0.1 259 | 260 | 261 | 262 | -------------------------------------------------------------------------------- /mycoffee/params.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """mycoffee params.""" 3 | from fractions import Fraction 4 | 5 | MY_COFFEE_VERSION = "2.1" 6 | INPUT_ERROR_MESSAGE = "[Error] Wrong input" 7 | POSITIVE_INTEGER_ERROR_MESSAGE = "invalid positive int value: '{string}'" 8 | POSITIVE_FLOAT_ERROR_MESSAGE = "invalid positive float value: '{string}'" 9 | RATIO_WARNING_MESSAGE = "The ratio is not within the recommended range. For `{method}`, the ratio can be anywhere between `{lower_limit}` and `{upper_limit}`" 10 | GRIND_WARNING_MESSAGE = "The grind size is not within the recommended range. For `{method}`, the grind size can be anywhere between `{lower_limit} um` and `{upper_limit} um`" 11 | TEMPERATURE_WARNING_MESSAGE = "The temperature is not within the recommended range. For `{method}`, the temperature can be anywhere between `{lower_limit} {unit}` and `{upper_limit} {unit}`" 12 | SAVE_FILE_ERROR_MESSAGE = "[Error] Failed to save file!" 13 | SAVE_FILE_SUCCESS_MESSAGE = "[Info] File saved successfully!" 14 | INPUT_EXAMPLE = "Example: mycoffee --method=v60" 15 | EXIT_MESSAGE = "See you. Bye!" 16 | EMPTY_MESSAGE = "Nothing :)" 17 | MY_COFFEE_REPO = "https://github.com/sepandhaghighi/mycoffee" 18 | MY_COFFEE_OVERVIEW = ''' 19 | MyCoffee is a command-line tool for coffee enthusiasts who love brewing with precision. 20 | It helps you calculate the perfect coffee-to-water ratio for various brewing methods, 21 | ensuring you brew your ideal cup every time-right from your terminal. 22 | ''' 23 | 24 | DATE_ISO_8601_FORMAT = "%Y-%m-%dT%H:%M:%SZ" 25 | DATE_DISPLAY_FORMAT = "%Y-%m-%d %H:%M" 26 | 27 | MESSAGE_TEMPLATE = """ 28 | 29 | Date: {date} 30 | 31 | Mode: {mode} 32 | 33 | Method: `{method}` 34 | 35 | Cups: {cups} 36 | 37 | Coffee: 38 | 39 | - Cup: {coffee[cup]} {coffee[unit]} 40 | - Total: {coffee[total]} {coffee[unit]} 41 | 42 | Water: 43 | 44 | - Cup: {water[cup]} {water[unit]} 45 | - Total: {water[total]} {water[unit]} 46 | 47 | Ratio: {coffee[ratio]}/{water[ratio]} ({ratio}) 48 | 49 | Strength: {strength} 50 | 51 | Grind: {grind[value]} {grind[unit]} ({grind[type]}) 52 | 53 | Temperature: {temperature[value]} {temperature[unit]} 54 | 55 | Message: {message} 56 | """ 57 | 58 | METHODS_LIST_TEMPLATE = "{index}. `{item}` - {data}" 59 | 60 | 61 | DEFAULT_PARAMS = { 62 | "cups": 1, 63 | "water": 0, 64 | "coffee": 0, 65 | "coffee_ratio": 1, 66 | "water_ratio": 1, 67 | "grind": 700, 68 | "temperature": 90, 69 | "coffee_unit": "g", 70 | "water_unit": "g", 71 | "temperature_unit": "C", 72 | "digits": 3, 73 | "mode": "water-to-coffee", 74 | "message": "" 75 | 76 | } 77 | 78 | METHODS_MAP = { 79 | "custom": { 80 | "coffee_ratio": 1, 81 | "water_ratio": 17, 82 | "grind": 700, 83 | "temperature": 90, 84 | "water": 240, 85 | "coffee": 14.118, 86 | "message": "Custom brewing method" 87 | }, 88 | "v60": { 89 | "coffee_ratio": 3, 90 | "water_ratio": 50, 91 | "grind": 550, 92 | "temperature": 91, 93 | "temperature_lower_limit": 85, 94 | "temperature_upper_limit": 95, 95 | "grind_lower_limit": 400, 96 | "grind_upper_limit": 700, 97 | "ratio_lower_limit": Fraction(1, 18), 98 | "ratio_upper_limit": Fraction(1, 14), 99 | "water": 250, 100 | "coffee": 15, 101 | "message": "V60 method" 102 | }, 103 | "espresso": { 104 | "coffee_ratio": 1, 105 | "water_ratio": 2, 106 | "grind": 280, 107 | "temperature": 92, 108 | "temperature_lower_limit": 85, 109 | "temperature_upper_limit": 95, 110 | "grind_lower_limit": 180, 111 | "grind_upper_limit": 380, 112 | "ratio_lower_limit": Fraction(2, 5), 113 | "ratio_upper_limit": Fraction(2, 3), 114 | "water": 36, 115 | "coffee": 18, 116 | "message": "Espresso method" 117 | }, 118 | "ristretto": { 119 | "coffee_ratio": 1, 120 | "water_ratio": 1, 121 | "grind": 280, 122 | "temperature": 92, 123 | "temperature_lower_limit": 85, 124 | "temperature_upper_limit": 95, 125 | "grind_lower_limit": 180, 126 | "grind_upper_limit": 380, 127 | "ratio_lower_limit": Fraction(2, 3), 128 | "ratio_upper_limit": Fraction(1, 1), 129 | "water": 18, 130 | "coffee": 18, 131 | "message": "Ristretto method" 132 | }, 133 | "lungo": { 134 | "coffee_ratio": 1, 135 | "water_ratio": 4, 136 | "grind": 280, 137 | "temperature": 92, 138 | "temperature_lower_limit": 85, 139 | "temperature_upper_limit": 95, 140 | "grind_lower_limit": 180, 141 | "grind_upper_limit": 380, 142 | "ratio_lower_limit": Fraction(1, 4), 143 | "ratio_upper_limit": Fraction(2, 5), 144 | "water": 72, 145 | "coffee": 18, 146 | "message": "Lungo method" 147 | }, 148 | "chemex": { 149 | "coffee_ratio": 1, 150 | "water_ratio": 15, 151 | "grind": 670, 152 | "temperature": 94, 153 | "temperature_lower_limit": 85, 154 | "temperature_upper_limit": 95, 155 | "grind_lower_limit": 410, 156 | "grind_upper_limit": 930, 157 | "ratio_lower_limit": Fraction(1, 21), 158 | "ratio_upper_limit": Fraction(1, 10), 159 | "water": 240, 160 | "coffee": 16, 161 | "message": "Chemex method" 162 | }, 163 | "french-press": { 164 | "coffee_ratio": 1, 165 | "water_ratio": 15, 166 | "grind": 995, 167 | "temperature": 90, 168 | "temperature_lower_limit": 85, 169 | "temperature_upper_limit": 95, 170 | "grind_lower_limit": 690, 171 | "grind_upper_limit": 1300, 172 | "ratio_lower_limit": Fraction(1, 18), 173 | "ratio_upper_limit": Fraction(1, 12), 174 | "water": 120, 175 | "coffee": 8, 176 | "message": "French press method" 177 | }, 178 | "siphon": { 179 | "coffee_ratio": 1, 180 | "water_ratio": 15, 181 | "grind": 588, 182 | "temperature": 93, 183 | "temperature_lower_limit": 91, 184 | "temperature_upper_limit": 94, 185 | "grind_lower_limit": 375, 186 | "grind_upper_limit": 800, 187 | "ratio_lower_limit": Fraction(1, 16), 188 | "ratio_upper_limit": Fraction(1, 12), 189 | "water": 240, 190 | "coffee": 16, 191 | "message": "Siphon method" 192 | }, 193 | "pour-over": { 194 | "coffee_ratio": 1, 195 | "water_ratio": 15, 196 | "grind": 670, 197 | "temperature": 92, 198 | "temperature_lower_limit": 90, 199 | "temperature_upper_limit": 93, 200 | "grind_lower_limit": 410, 201 | "grind_upper_limit": 930, 202 | "ratio_lower_limit": Fraction(1, 16), 203 | "ratio_upper_limit": Fraction(1, 14), 204 | "water": 240, 205 | "coffee": 16, 206 | "message": "Pour-over method" 207 | }, 208 | "auto-drip": { 209 | "coffee_ratio": 1, 210 | "water_ratio": 16, 211 | "grind": 600, 212 | "temperature": 93, 213 | "temperature_lower_limit": 90, 214 | "temperature_upper_limit": 96, 215 | "grind_lower_limit": 300, 216 | "grind_upper_limit": 900, 217 | "ratio_lower_limit": Fraction(1, 17), 218 | "ratio_upper_limit": Fraction(1, 14), 219 | "water": 128, 220 | "coffee": 8, 221 | "message": "Auto drip method" 222 | }, 223 | "cold-brew": { 224 | "coffee_ratio": 1, 225 | "water_ratio": 11, 226 | "grind": 1100, 227 | "temperature": 20, 228 | "temperature_lower_limit": 0, 229 | "temperature_upper_limit": 40, 230 | "grind_lower_limit": 800, 231 | "grind_upper_limit": 1400, 232 | "ratio_lower_limit": Fraction(1, 15), 233 | "ratio_upper_limit": Fraction(1, 8), 234 | "water": 242, 235 | "coffee": 22, 236 | "message": "Cold brew method" 237 | }, 238 | "cold-brew-conc": { 239 | "coffee_ratio": 1, 240 | "water_ratio": 5, 241 | "grind": 1100, 242 | "temperature": 20, 243 | "temperature_lower_limit": 0, 244 | "temperature_upper_limit": 40, 245 | "grind_lower_limit": 800, 246 | "grind_upper_limit": 1400, 247 | "ratio_lower_limit": Fraction(1, 6), 248 | "ratio_upper_limit": Fraction(1, 4), 249 | "water": 120, 250 | "coffee": 24, 251 | "message": "Cold brew concentrate method" 252 | }, 253 | "moka-pot": { 254 | "coffee_ratio": 1, 255 | "water_ratio": 10, 256 | "grind": 510, 257 | "temperature": 93, 258 | "temperature_lower_limit": 85, 259 | "temperature_upper_limit": 95, 260 | "grind_lower_limit": 360, 261 | "grind_upper_limit": 660, 262 | "ratio_lower_limit": Fraction(1, 12), 263 | "ratio_upper_limit": Fraction(1, 7), 264 | "water": 60, 265 | "coffee": 6, 266 | "message": "Moka pot method" 267 | }, 268 | "turkish": { 269 | "coffee_ratio": 1, 270 | "water_ratio": 10, 271 | "grind": 130, 272 | "temperature": 90, 273 | "temperature_lower_limit": 90, 274 | "temperature_upper_limit": 95, 275 | "grind_lower_limit": 40, 276 | "grind_upper_limit": 220, 277 | "ratio_lower_limit": Fraction(1, 12), 278 | "ratio_upper_limit": Fraction(1, 8), 279 | "water": 50, 280 | "coffee": 5, 281 | "message": "Turkish method" 282 | }, 283 | "cupping": { 284 | "coffee_ratio": 11, 285 | "water_ratio": 200, 286 | "grind": 655, 287 | "temperature": 93, 288 | "temperature_lower_limit": 85, 289 | "temperature_upper_limit": 95, 290 | "grind_lower_limit": 460, 291 | "grind_upper_limit": 850, 292 | "ratio_lower_limit": Fraction(1, 19), 293 | "ratio_upper_limit": Fraction(1, 17), 294 | "water": 150, 295 | "coffee": 8.25, 296 | "message": "Cupping method" 297 | }, 298 | "aero-press": { 299 | "coffee_ratio": 1, 300 | "water_ratio": 15, 301 | "grind": 640, 302 | "temperature": 93, 303 | "temperature_lower_limit": 90, 304 | "temperature_upper_limit": 95, 305 | "grind_lower_limit": 320, 306 | "grind_upper_limit": 960, 307 | "ratio_lower_limit": Fraction(1, 18), 308 | "ratio_upper_limit": Fraction(1, 12), 309 | "water": 135, 310 | "coffee": 9, 311 | "message": "AeroPress standard method" 312 | }, 313 | "aero-press-conc": { 314 | "coffee_ratio": 1, 315 | "water_ratio": 6, 316 | "grind": 640, 317 | "temperature": 93, 318 | "temperature_lower_limit": 90, 319 | "temperature_upper_limit": 95, 320 | "grind_lower_limit": 320, 321 | "grind_upper_limit": 960, 322 | "ratio_lower_limit": Fraction(1, 7), 323 | "ratio_upper_limit": Fraction(1, 5), 324 | "water": 90, 325 | "coffee": 15, 326 | "message": "AeroPress concentrate method" 327 | }, 328 | "aero-press-inv": { 329 | "coffee_ratio": 1, 330 | "water_ratio": 12, 331 | "grind": 640, 332 | "temperature": 93, 333 | "temperature_lower_limit": 90, 334 | "temperature_upper_limit": 95, 335 | "grind_lower_limit": 320, 336 | "grind_upper_limit": 960, 337 | "ratio_lower_limit": Fraction(1, 14), 338 | "ratio_upper_limit": Fraction(1, 10), 339 | "water": 132, 340 | "coffee": 11, 341 | "message": "AeroPress inverted method" 342 | }, 343 | "steep-and-release": { 344 | "coffee_ratio": 1, 345 | "water_ratio": 16, 346 | "grind": 638, 347 | "temperature": 93, 348 | "temperature_lower_limit": 85, 349 | "temperature_upper_limit": 95, 350 | "grind_lower_limit": 450, 351 | "grind_upper_limit": 825, 352 | "ratio_lower_limit": Fraction(1, 17), 353 | "ratio_upper_limit": Fraction(1, 14), 354 | "water": 255, 355 | "coffee": 15.9375, 356 | "message": "Steep-and-release method" 357 | }, 358 | "clever-dripper": { 359 | "coffee_ratio": 1, 360 | "water_ratio": 16.67, 361 | "grind": 600, 362 | "temperature": 93, 363 | "temperature_lower_limit": 91, 364 | "temperature_upper_limit": 96, 365 | "grind_lower_limit": 400, 366 | "grind_upper_limit": 800, 367 | "ratio_lower_limit": Fraction(1, 20), 368 | "ratio_upper_limit": Fraction(1, 15), 369 | "water": 250, 370 | "coffee": 14.997, 371 | "message": "Clever dripper method" 372 | }, 373 | "phin-filter": { 374 | "coffee_ratio": 1, 375 | "water_ratio": 2, 376 | "grind": 300, 377 | "temperature": 93, 378 | "temperature_lower_limit": 90, 379 | "temperature_upper_limit": 96, 380 | "grind_lower_limit": 200, 381 | "grind_upper_limit": 400, 382 | "ratio_lower_limit": Fraction(1, 4), 383 | "ratio_upper_limit": Fraction(1, 2), 384 | "water": 72, 385 | "coffee": 36, 386 | "message": "Phin filter method" 387 | }, 388 | "kalita-wave": { 389 | "coffee_ratio": 1, 390 | "water_ratio": 16, 391 | "grind": 900, 392 | "temperature": 93, 393 | "temperature_lower_limit": 90, 394 | "temperature_upper_limit": 96, 395 | "grind_lower_limit": 800, 396 | "grind_upper_limit": 1000, 397 | "ratio_lower_limit": Fraction(1, 17), 398 | "ratio_upper_limit": Fraction(1, 15), 399 | "water": 400, 400 | "coffee": 25, 401 | "message": "Kalita wave method" 402 | }, 403 | "instant-coffee": { 404 | "coffee_ratio": 1, 405 | "water_ratio": 35, 406 | "grind": 0, 407 | "temperature": 85, 408 | "temperature_lower_limit": 80, 409 | "temperature_upper_limit": 93, 410 | "ratio_lower_limit": Fraction(1, 50), 411 | "ratio_upper_limit": Fraction(1, 15), 412 | "water": 175, 413 | "coffee": 5, 414 | "message": "Instant coffee" 415 | }, 416 | } 417 | 418 | COFFEE_UNITS_MAP = { 419 | "g": {"name": "gram", "rate": 1}, 420 | "oz": {"name": "ounce", "rate": 0.03527396195}, 421 | "lb": {"name": "pound", "rate": 0.00220462262185}, 422 | "mg": {"name": "milligram", "rate": 1000}, 423 | "kg": {"name": "kilogram", "rate": 0.001}, 424 | "cb": {"name": "coffee bean", "rate": 7.5471698}, 425 | "tbsp": {"name": "tablespoon", "rate": 0.18528}, 426 | "tsp": {"name": "teaspoon", "rate": 0.55585}, 427 | "dsp": {"name": "dessertspoon", "rate": 0.27792}, 428 | "cup": {"name": "cup", "rate": 0.01158}, 429 | "t oz": {"name": "troy ounce", "rate": 0.032151}, 430 | "gr": {"name": "grain", "rate": 15.4324}, 431 | "ct": {"name": "carat", "rate": 5}, 432 | "t lb": {"name": "troy pound", "rate": 0.0026792289}, 433 | "dwt": {"name": "pennyweight", "rate": 0.643015}, 434 | } 435 | 436 | WATER_UNITS_MAP = { 437 | "g": {"name": "gram", "rate": 1}, 438 | "ml": {"name": "milliliter", "rate": 1}, 439 | "cc": {"name": "cubic centimeter", "rate": 1}, 440 | "cl": {"name": "centiliter", "rate": 0.1}, 441 | "l": {"name": "liter", "rate": 0.001}, 442 | "oz": {"name": "ounce", "rate": 0.03527396195}, 443 | "lb": {"name": "pound", "rate": 0.00220462262185}, 444 | "mg": {"name": "milligram", "rate": 1000}, 445 | "kg": {"name": "kilogram", "rate": 0.001}, 446 | "tbsp": {"name": "tablespoon", "rate": 0.067628}, 447 | "tsp": {"name": "teaspoon", "rate": 0.20288}, 448 | "dsp": {"name": "dessertspoon", "rate": 0.10144}, 449 | "cup": {"name": "cup", "rate": 0.0042268}, 450 | "pt": {"name": "pint", "rate": 0.00211338}, 451 | "qt": {"name": "quart", "rate": 0.00105669}, 452 | "fl oz": {"name": "fluid ounce", "rate": 0.033814}, 453 | "t oz": {"name": "troy ounce", "rate": 0.032151}, 454 | "gr": {"name": "grain", "rate": 15.4324}, 455 | "ct": {"name": "carat", "rate": 5}, 456 | "t lb": {"name": "troy pound", "rate": 0.0026792289}, 457 | "dwt": {"name": "pennyweight", "rate": 0.643015}, 458 | } 459 | 460 | TEMPERATURE_UNITS_MAP = { 461 | "C": {"name": "Celsius"}, 462 | "F": {"name": "Fahrenheit"}, 463 | "K": {"name": "Kelvin"} 464 | } 465 | 466 | FILE_FORMATS_LIST = ["text", "json", "yaml"] 467 | 468 | MODES_LIST = ["water-to-coffee", "coffee-to-water", "ratio"] 469 | 470 | MODE_TO_NAME = { 471 | "water-to-coffee": "Water --> Coffee", 472 | "coffee-to-water": "Coffee --> Water", 473 | "ratio": "Water & Coffee --> Ratio"} 474 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

MyCoffee: Brew Perfect Coffee Right from Your Terminal

4 |
5 | built with Python3 6 | GitHub repo size 7 | PyPI version 8 | 9 |
10 | 11 | ## Overview 12 | 13 |

14 | MyCoffee is a command-line tool for coffee enthusiasts who love brewing with precision. It helps you calculate the perfect coffee-to-water ratio for various brewing methods, ensuring you brew your ideal cup every time-right from your terminal. 15 |

16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
PyPI Counter
Github Stars
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
Branchmaindev
CI
42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 |
Code QualityCodeFactor
51 | 52 | 53 | ## Installation 54 | 55 | ### Source Code 56 | - Download [Version 2.1](https://github.com/sepandhaghighi/mycoffee/archive/v2.1.zip) or [Latest Source](https://github.com/sepandhaghighi/mycoffee/archive/dev.zip) 57 | - `pip install .` 58 | 59 | ### PyPI 60 | 61 | - Check [Python Packaging User Guide](https://packaging.python.org/installing/) 62 | - `pip install mycoffee==2.1` 63 | 64 | 65 | ## Usage 66 | 67 | ℹ️ You can use `mycoffee` or `python -m mycoffee` to run this program 68 | 69 | ### Version 70 | 71 | ```console 72 | > mycoffee --version 73 | 74 | 2.1 75 | ``` 76 | 77 | ### Info 78 | 79 | ```console 80 | > mycoffee --info 81 | __ __ ____ __ __ 82 | | \/ | _ _ / ___| ___ / _| / _| ___ ___ 83 | | |\/| || | | || | / _ \ | |_ | |_ / _ \ / _ \ 84 | | | | || |_| || |___ | (_) || _|| _|| __/| __/ 85 | |_| |_| \__, | \____| \___/ |_| |_| \___| \___| 86 | |___/ 87 | 88 | __ __ ____ _ 89 | \ \ / / _ |___ \ / | 90 | \ \ / / (_) __) | | | 91 | \ V / _ / __/ _ | | 92 | \_/ (_)|_____|(_)|_| 93 | 94 | 95 | MyCoffee is a command-line tool for coffee enthusiasts who love brewing with precision. 96 | It helps you calculate the perfect coffee-to-water ratio for various brewing methods, 97 | ensuring you brew your ideal cup every time-right from your terminal. 98 | 99 | Repo : https://github.com/sepandhaghighi/mycoffee 100 | ``` 101 | 102 | 103 | ### Method 104 | 105 | ```console 106 | > mycoffee --method=v60 107 | __ __ _ _ ___ _____ ____ ____ ____ ____ 108 | ( \/ )( \/ ) / __)( _ )( ___)( ___)( ___)( ___) 109 | ) ( \ / ( (__ )(_)( )__) )__) )__) )__) 110 | (_/\/\_) (__) \___)(_____)(__) (__) (____)(____) 111 | 112 | 113 | 114 | Date: 2025-09-10 17:56 115 | 116 | Mode: Water --> Coffee 117 | 118 | Method: `v60` 119 | 120 | Cups: 1 121 | 122 | Coffee: 123 | 124 | - Cup: 15 g 125 | - Total: 15 g 126 | 127 | Water: 128 | 129 | - Cup: 250 g 130 | - Total: 250 g 131 | 132 | Ratio: 3/50 (0.06) 133 | 134 | Strength: Medium 135 | 136 | Grind: 550 um (Medium-Fine) 137 | 138 | Temperature: 91 C 139 | 140 | Message: V60 method 141 | ``` 142 | 143 | * [Methods List](https://github.com/sepandhaghighi/mycoffee/blob/main/METHODS.md) 144 | * `mycoffee --methods-list` 145 | 146 | ### Mode 147 | 148 | #### Water to Coffee 149 | ```console 150 | > mycoffee --method=v60 --mode="water-to-coffee" --water=300 151 | __ __ _ _ ___ _____ ____ ____ ____ ____ 152 | ( \/ )( \/ ) / __)( _ )( ___)( ___)( ___)( ___) 153 | ) ( \ / ( (__ )(_)( )__) )__) )__) )__) 154 | (_/\/\_) (__) \___)(_____)(__) (__) (____)(____) 155 | 156 | 157 | 158 | Date: 2025-09-10 17:56 159 | 160 | Mode: Water --> Coffee 161 | 162 | Method: `v60` 163 | 164 | Cups: 1 165 | 166 | Coffee: 167 | 168 | - Cup: 18 g 169 | - Total: 18 g 170 | 171 | Water: 172 | 173 | - Cup: 300 g 174 | - Total: 300 g 175 | 176 | Ratio: 3/50 (0.06) 177 | 178 | Strength: Medium 179 | 180 | Grind: 550 um (Medium-Fine) 181 | 182 | Temperature: 91 C 183 | 184 | Message: V60 method 185 | ``` 186 | 187 | #### Coffee to Water 188 | ```console 189 | > mycoffee --method=v60 --mode="coffee-to-water" --coffee=12 190 | __ __ _ _ ___ _____ ____ ____ ____ ____ 191 | ( \/ )( \/ ) / __)( _ )( ___)( ___)( ___)( ___) 192 | ) ( \ / ( (__ )(_)( )__) )__) )__) )__) 193 | (_/\/\_) (__) \___)(_____)(__) (__) (____)(____) 194 | 195 | 196 | 197 | Date: 2025-09-10 17:56 198 | 199 | Mode: Coffee --> Water 200 | 201 | Method: `v60` 202 | 203 | Cups: 1 204 | 205 | Coffee: 206 | 207 | - Cup: 12 g 208 | - Total: 12 g 209 | 210 | Water: 211 | 212 | - Cup: 200 g 213 | - Total: 200 g 214 | 215 | Ratio: 3/50 (0.06) 216 | 217 | Strength: Medium 218 | 219 | Grind: 550 um (Medium-Fine) 220 | 221 | Temperature: 91 C 222 | 223 | Message: V60 method 224 | ``` 225 | 226 | #### Ratio 227 | ```console 228 | > mycoffee --method=v60 --mode="ratio" --coffee=18 --water=300 229 | __ __ _ _ ___ _____ ____ ____ ____ ____ 230 | ( \/ )( \/ ) / __)( _ )( ___)( ___)( ___)( ___) 231 | ) ( \ / ( (__ )(_)( )__) )__) )__) )__) 232 | (_/\/\_) (__) \___)(_____)(__) (__) (____)(____) 233 | 234 | 235 | 236 | Date: 2025-09-10 17:56 237 | 238 | Mode: Water & Coffee --> Ratio 239 | 240 | Method: `v60` 241 | 242 | Cups: 1 243 | 244 | Coffee: 245 | 246 | - Cup: 18 g 247 | - Total: 18 g 248 | 249 | Water: 250 | 251 | - Cup: 300 g 252 | - Total: 300 g 253 | 254 | Ratio: 3/50 (0.06) 255 | 256 | Strength: Medium 257 | 258 | Grind: 550 um (Medium-Fine) 259 | 260 | Temperature: 91 C 261 | 262 | Message: V60 method 263 | ``` 264 | 265 | ### Customize 266 | 267 | ℹ️ You can run `mycoffee --coffee-units-list` to view the supported coffee units 268 | 269 | ℹ️ You can run `mycoffee --water-units-list` to view the supported water units 270 | 271 | ```console 272 | > mycoffee --method=chemex --water=20 --cups=3 --coffee-ratio=2 --water-ratio=37 --coffee-unit="t oz" --water-unit="fl oz" --grind=750 --temperature=88 273 | 274 | __ __ _ _ ___ _____ ____ ____ ____ ____ 275 | ( \/ )( \/ ) / __)( _ )( ___)( ___)( ___)( ___) 276 | ) ( \ / ( (__ )(_)( )__) )__) )__) )__) 277 | (_/\/\_) (__) \___)(_____)(__) (__) (____)(____) 278 | 279 | 280 | 281 | Date: 2025-09-10 17:56 282 | 283 | Mode: Water --> Coffee 284 | 285 | Method: `chemex` 286 | 287 | Cups: 3 288 | 289 | Coffee: 290 | 291 | - Cup: 1.028 t oz 292 | - Total: 3.084 t oz 293 | 294 | Water: 295 | 296 | - Cup: 20 fl oz 297 | - Total: 60 fl oz 298 | 299 | Ratio: 2/37 (0.054) 300 | 301 | Strength: Medium 302 | 303 | Grind: 750 um (Medium) 304 | 305 | Temperature: 88 C 306 | 307 | Message: Chemex method 308 | ``` 309 | 310 | ### Save 311 | 312 | ℹ️ File format valid choices: [`text`, `json`, `yaml`] 313 | 314 | ℹ️ The default file format is `text` 315 | 316 | ```console 317 | > mycoffee --method=chemex --water=20 --cups=3 --coffee-ratio=2 --water-ratio=37 --save-path="profile1.txt" --save-format="text" 318 | ``` 319 | 320 | ## Parameters 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 |
ParameterDescriptionTypeDefault
--modeSpecifies the conversion modeStringwater-to-coffee
--methodSpecifies the coffee brewing methodStringcustom
--waterSets the amount of water in each cupPositive float240
--coffeeSets the amount of coffee in each cupPositive float14.118
--cupsIndicates the number of cupsPositive integer1
--grindGrind size (um)Positive integer700
--temperatureBrewing temperatureFloat90
--coffee-ratioCoefficient for the coffee component in the ratioPositive float1
--water-ratioCoefficient for the water component in the ratioPositive float17
--messageExtra information about the brewing methodStringCustom brewing method
--digitsNumber of digits up to which the result is roundedInteger3
--coffee-unitCoffee unitStringg
--water-unitWater unitStringg
--temperature-unitTemperature unitStringC
--save-pathFile path to save the outputString--
--save-formatFormat to save the outputStringtext
430 | 431 | 432 | 433 | ## Issues & Bug Reports 434 | 435 | Just fill an issue and describe it. We'll check it ASAP! 436 | 437 | - Please complete the issue template 438 | 439 | 440 | ## References 441 | 442 |
1- Coffee to water ratio calculator
443 |
2- V60 Brew Guide
444 |
3- How to Brew Coffee with a Chemex
445 |
4- Using French press for perfect coffee
446 |
5- How to Brew the Perfect Cup of Siphon Coffee
447 |
6- Using Espresso Brew Ratios
448 |
7- My Best Coffee Recipes of 2022
449 |
8- Auto Drip Brewing Guide
450 |
9- Guide To Cold Brew
451 |
10- Cold Brew Concentrate Recipe
452 |
11- How to Make Coffee in a Moka Pot
453 |
12- How to Make Turkish Coffee at Home
454 |
13- How to Cup Coffee
455 |
14- Tetsu Kasuya AeroPress Recipe
456 |
15- All about the intervals
457 |
16- Clever Dripper; Square Mile Coffee
458 |
17- AeroPress Product User Manuals
459 |
18- RapidTables - Weight Converter
460 |
19- Whole bean to ground coffee calculator
461 |
20- Weight to Volume Converter for Recipes
462 |
21- How Much Coffee per Cup?
463 |
22- Weight Calculator
464 |
23- Volume Conversion Calculator - Inch Calculator
465 |
24- Metric Conversion Charts and Calculators
466 |
25- Coffee grind size chart
467 |
26- The best temperature to brew coffee
468 |
27- How to Brew Coffee with a Syphon
469 |
28- Guide To Home Coffee Makers
470 |
29- Can you brew coffee with warm water?
471 |
30- How to Brew Coffee Using a Cezve
472 |
31- Coffee cupping
473 |
32- The Latest Method to Brew Coffee with Your Clever Dripper
474 |
33- How to Make Vietnamese Coffee with Traditional Phin Drip Filter
475 |
34- Kalita Wave 185 Brew Guide
476 |
35- How to Make the Perfect Instant Coffee, Hot or Iced
477 |
36- How to make the perfect coffee
478 | 479 | ## Show Your Support 480 | 481 |

Star This Repo

482 | 483 | Give a ⭐️ if this project helped you! 484 | 485 |

Donate to Our Project

486 | 487 |

Bitcoin

488 | 1KtNLEEeUbTEK9PdN6Ya3ZAKXaqoKUuxCy 489 |

Ethereum

490 | 0xcD4Db18B6664A9662123D4307B074aE968535388 491 |

Litecoin

492 | Ldnz5gMcEeV8BAdsyf8FstWDC6uyYR6pgZ 493 |

Doge

494 | DDUnKpFQbBqLpFVZ9DfuVysBdr249HxVDh 495 |

Tron

496 | TCZxzPZLcJHr2qR3uPUB1tXB6L3FDSSAx7 497 |

Ripple

498 | rN7ZuRG7HDGHR5nof8nu5LrsbmSB61V1qq 499 |

Binance Coin

500 | bnb1zglwcf0ac3d0s2f6ck5kgwvcru4tlctt4p5qef 501 |

Tether

502 | 0xcD4Db18B6664A9662123D4307B074aE968535388 503 |

Dash

504 | Xd3Yn2qZJ7VE8nbKw2fS98aLxR5M6WUU3s 505 |

Stellar

506 | GALPOLPISRHIYHLQER2TLJRGUSZH52RYDK6C3HIU4PSMNAV65Q36EGNL 507 |

Zilliqa

508 | zil1knmz8zj88cf0exr2ry7nav9elehxfcgqu3c5e5 509 |

Coffeete

510 | 511 | 512 | 513 | 514 | -------------------------------------------------------------------------------- /mycoffee/functions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """mycoffee functions.""" 3 | from typing import Union, Dict, List 4 | import json 5 | import yaml 6 | import math 7 | import fractions 8 | import argparse 9 | from datetime import datetime, timezone 10 | from mycoffee.params import MESSAGE_TEMPLATE, METHODS_LIST_TEMPLATE, EMPTY_MESSAGE 11 | from mycoffee.params import MY_COFFEE_VERSION, DEFAULT_PARAMS 12 | from mycoffee.params import METHODS_MAP, COFFEE_UNITS_MAP, WATER_UNITS_MAP, TEMPERATURE_UNITS_MAP 13 | from mycoffee.params import RATIO_WARNING_MESSAGE, GRIND_WARNING_MESSAGE, TEMPERATURE_WARNING_MESSAGE 14 | from mycoffee.params import POSITIVE_INTEGER_ERROR_MESSAGE, POSITIVE_FLOAT_ERROR_MESSAGE 15 | from mycoffee.params import MY_COFFEE_OVERVIEW, MY_COFFEE_REPO 16 | from mycoffee.params import SAVE_FILE_ERROR_MESSAGE, SAVE_FILE_SUCCESS_MESSAGE 17 | from mycoffee.params import MODE_TO_NAME 18 | from mycoffee.params import DATE_ISO_8601_FORMAT, DATE_DISPLAY_FORMAT 19 | from art import tprint 20 | 21 | 22 | def mycoffee_info() -> None: # pragma: no cover 23 | """Print mycoffee details.""" 24 | tprint("MyCoffee") 25 | tprint("V:" + MY_COFFEE_VERSION) 26 | print(MY_COFFEE_OVERVIEW) 27 | print("Repo : " + MY_COFFEE_REPO) 28 | 29 | 30 | def get_date_now() -> str: 31 | """Return the current UTC datetime as an ISO 8601 string.""" 32 | utc_now = datetime.utcnow().replace(tzinfo=timezone.utc) 33 | return utc_now.strftime(DATE_ISO_8601_FORMAT) 34 | 35 | 36 | def format_date(input_date: str) -> str: 37 | """ 38 | Convert an ISO 8601 datetime string (UTC) into local time and format it for display. 39 | 40 | :param input_date: a datetime string in ISO 8601 format (UTC) 41 | """ 42 | utc_date = datetime.strptime(input_date, DATE_ISO_8601_FORMAT).replace(tzinfo=timezone.utc) 43 | local_date = utc_date.astimezone() 44 | return local_date.strftime(DATE_DISPLAY_FORMAT) 45 | 46 | 47 | def validate_positive_int(string: str) -> int: 48 | """ 49 | Validate and return a positive integer or raise argparse.ArgumentTypeError. 50 | 51 | :param string: input string 52 | """ 53 | try: 54 | number = int(string) 55 | if number <= 0: 56 | raise Exception 57 | return number 58 | except Exception: 59 | raise argparse.ArgumentTypeError(POSITIVE_INTEGER_ERROR_MESSAGE.format(string=string)) 60 | 61 | 62 | def validate_positive_float(string: str) -> float: 63 | """ 64 | Validate and return a positive float or raise argparse.ArgumentTypeError. 65 | 66 | :param string: input string 67 | """ 68 | try: 69 | number = float(string) 70 | if number <= 0: 71 | raise Exception 72 | return number 73 | except Exception: 74 | raise argparse.ArgumentTypeError(POSITIVE_FLOAT_ERROR_MESSAGE.format(string=string)) 75 | 76 | 77 | def is_int(number: Union[int, float]) -> bool: 78 | """ 79 | Check that input number is int or not. 80 | 81 | :param number: input number 82 | """ 83 | if int(number) == number: 84 | return True 85 | return False 86 | 87 | 88 | def format_result(params: Dict[str, Union[str, int, float, dict]]) -> str: 89 | """ 90 | Format result. 91 | 92 | :param params: parameters 93 | """ 94 | result = MESSAGE_TEMPLATE.format( 95 | date=format_date(params["date"]), 96 | method=params["method"], 97 | cups=params["cups"], 98 | coffee=params["coffee"], 99 | water=params["water"], 100 | ratio=params["ratio"], 101 | message=params["message"], 102 | grind=params["grind"], 103 | temperature=params["temperature"], 104 | strength=params["strength"], 105 | mode=MODE_TO_NAME[params["mode"]]) 106 | return result 107 | 108 | 109 | def print_result(params: Dict[str, Union[str, int, float, dict]], ignore_warnings: bool = False) -> None: 110 | """ 111 | Print result. 112 | 113 | :param params: parameters 114 | :param ignore_warnings: ignore warnings flag 115 | """ 116 | tprint("MyCoffee", font="bulbhead") 117 | print(format_result(params)) 118 | if not ignore_warnings: 119 | warnings_list = params["warnings"] 120 | if len(warnings_list) > 0: 121 | for message in warnings_list: 122 | print("[Warning] " + message) 123 | 124 | 125 | def save_result( 126 | params: Dict[str, Union[str, int, float, dict]], 127 | file_path: str, 128 | file_format: str = "text", 129 | ignore_warnings: bool = False) -> Dict[str, Union[bool, str]]: 130 | """ 131 | Save result. 132 | 133 | :param params: parameters 134 | :param file_path: file path 135 | :param file_format: file format 136 | :param ignore_warnings: ignore warnings flag 137 | """ 138 | details = {"status": True, "message": SAVE_FILE_SUCCESS_MESSAGE} 139 | try: 140 | if file_format == "json": 141 | save_result_json(params, file_path, ignore_warnings) 142 | elif file_format == "yaml": 143 | save_result_yaml(params, file_path, ignore_warnings) 144 | else: 145 | save_result_text(params, file_path, ignore_warnings) 146 | except Exception as e: 147 | details["status"] = False 148 | details["message"] = str(e) 149 | return details 150 | 151 | 152 | def save_result_text(params: Dict[str, Union[str, int, float, dict]], 153 | file_path: str, ignore_warnings: bool = False) -> None: 154 | """ 155 | Save result as a text file. 156 | 157 | :param params: parameters 158 | :param file_path: file path 159 | :param ignore_warnings: ignore warnings flag 160 | """ 161 | result = format_result(params).strip() 162 | if not ignore_warnings: 163 | warnings_list = params["warnings"] 164 | if len(warnings_list) > 0: 165 | result += "\n\n" 166 | for message in warnings_list: 167 | result += "[Warning] " + message + "\n" 168 | with open(file_path, "w") as file: 169 | file.write(result) 170 | 171 | 172 | def save_result_json(params: Dict[str, Union[str, int, float, dict]], 173 | file_path: str, ignore_warnings: bool = False) -> None: 174 | """ 175 | Save result as a JSON file. 176 | 177 | :param params: parameters 178 | :param file_path: file path 179 | :param ignore_warnings: ignore warnings flag 180 | """ 181 | result = params.copy() 182 | result["mycoffee_version"] = MY_COFFEE_VERSION 183 | if ignore_warnings: 184 | result["warnings"] = [] 185 | with open(file_path, "w") as file: 186 | json.dump(result, file) 187 | 188 | 189 | def save_result_yaml(params: Dict[str, Union[str, int, float, dict]], 190 | file_path: str, ignore_warnings: bool = False) -> None: 191 | """ 192 | Save result as a YAML file. 193 | 194 | :param params: parameters 195 | :param file_path: file path 196 | :param ignore_warnings: ignore warnings flag 197 | """ 198 | result = params.copy() 199 | result["mycoffee_version"] = MY_COFFEE_VERSION 200 | if ignore_warnings: 201 | result["warnings"] = [] 202 | with open(file_path, "w") as file: 203 | yaml.safe_dump(result, file, default_flow_style=False) 204 | 205 | 206 | def get_warnings(params: Dict[str, Union[str, int, float]]) -> List[str]: 207 | """ 208 | Get warnings as a list. 209 | 210 | :param params: parameters 211 | """ 212 | warnings_list = [] 213 | method = params["method"] 214 | if not check_ratio_limits(method=method, ratio=params["ratio"]): 215 | ratio_lower_limit = METHODS_MAP[method]["ratio_lower_limit"] 216 | ratio_upper_limit = METHODS_MAP[method]["ratio_upper_limit"] 217 | warnings_list.append( 218 | RATIO_WARNING_MESSAGE.format( 219 | method=method, 220 | lower_limit=str(ratio_lower_limit), 221 | upper_limit=str(ratio_upper_limit))) 222 | if not check_grind_limits(method=method, grind=params["grind"]["value"]): 223 | grind_lower_limit = METHODS_MAP[method]["grind_lower_limit"] 224 | grind_upper_limit = METHODS_MAP[method]["grind_upper_limit"] 225 | warnings_list.append( 226 | GRIND_WARNING_MESSAGE.format( 227 | method=method, 228 | lower_limit=str(grind_lower_limit), 229 | upper_limit=str(grind_upper_limit))) 230 | if not check_temperature_limits( 231 | method=method, 232 | temperature=params["temperature"]["value"], 233 | temperature_unit=params["temperature"]["unit"]): 234 | temperature_lower_limit = convert_temperature( 235 | METHODS_MAP[method]["temperature_lower_limit"], 236 | from_unit="C", 237 | to_unit=params["temperature"]["unit"], 238 | digits=params["digits"]) 239 | temperature_upper_limit = convert_temperature( 240 | METHODS_MAP[method]["temperature_upper_limit"], 241 | from_unit="C", 242 | to_unit=params["temperature"]["unit"], 243 | digits=params["digits"]) 244 | warnings_list.append( 245 | TEMPERATURE_WARNING_MESSAGE.format( 246 | method=method, 247 | lower_limit=str(temperature_lower_limit), 248 | upper_limit=str(temperature_upper_limit), 249 | unit=params["temperature"]["unit"])) 250 | return warnings_list 251 | 252 | 253 | def get_grind_type(grind: int) -> str: 254 | """ 255 | Return grind type. 256 | 257 | :param grind: grind size 258 | """ 259 | if grind <= 200: 260 | return "Extra-Fine" 261 | elif grind <= 400: 262 | return "Fine" 263 | elif grind <= 600: 264 | return "Medium-Fine" 265 | elif grind <= 800: 266 | return "Medium" 267 | elif grind <= 1000: 268 | return "Medium-Coarse" 269 | elif grind <= 1200: 270 | return "Coarse" 271 | return "Extra-Coarse" 272 | 273 | 274 | def get_brew_strength(ratio: float) -> str: 275 | """ 276 | Return brew strength. 277 | 278 | :param ratio: coffee to water ratio 279 | """ 280 | strength_labels = ["Very Weak", "Weak", "Medium", "Strong", "Very Strong"] 281 | thresholds = [1 / 40, 1 / 22, 1 / 15, 1 / 12, 1 / 8] 282 | 283 | if ratio < thresholds[0]: 284 | return strength_labels[0] 285 | elif ratio < thresholds[1]: 286 | return strength_labels[1] 287 | elif ratio < thresholds[2]: 288 | return strength_labels[2] 289 | elif ratio < thresholds[3]: 290 | return strength_labels[3] 291 | else: 292 | return strength_labels[4] 293 | 294 | 295 | def load_method_params(method_name: str) -> Dict[str, Union[str, int, float]]: 296 | """ 297 | Load method params. 298 | 299 | :param method_name: method name 300 | """ 301 | method_params = dict() 302 | for item in DEFAULT_PARAMS: 303 | if item in METHODS_MAP[method_name]: 304 | method_params[item] = METHODS_MAP[method_name][item] 305 | else: 306 | method_params[item] = DEFAULT_PARAMS[item] 307 | return method_params 308 | 309 | 310 | def show_methods_list() -> None: 311 | """Show methods list.""" 312 | print("Methods list:\n") 313 | for i, method in enumerate(sorted(METHODS_MAP), 1): 314 | print( 315 | METHODS_LIST_TEMPLATE.format( 316 | index=i, 317 | item=method, 318 | data=METHODS_MAP[method]['message'])) 319 | 320 | 321 | def show_coffee_units_list() -> None: 322 | """Show coffee units list.""" 323 | print("Coffee units list:\n") 324 | for i, unit in enumerate(sorted(COFFEE_UNITS_MAP), 1): 325 | print( 326 | METHODS_LIST_TEMPLATE.format( 327 | index=i, 328 | item=unit, 329 | data=COFFEE_UNITS_MAP[unit]['name'])) 330 | 331 | 332 | def show_water_units_list() -> None: 333 | """Show water units list.""" 334 | print("Water units list:\n") 335 | for i, unit in enumerate(sorted(WATER_UNITS_MAP), 1): 336 | print( 337 | METHODS_LIST_TEMPLATE.format( 338 | index=i, 339 | item=unit, 340 | data=WATER_UNITS_MAP[unit]['name'])) 341 | 342 | 343 | def show_temperature_units_list() -> None: 344 | """Show temperature units list.""" 345 | print("Temperature units list:\n") 346 | for i, unit in enumerate(sorted(TEMPERATURE_UNITS_MAP), 1): 347 | print( 348 | METHODS_LIST_TEMPLATE.format( 349 | index=i, 350 | item=unit, 351 | data=TEMPERATURE_UNITS_MAP[unit]['name'])) 352 | 353 | 354 | def load_params(args: argparse.Namespace) -> Dict[str, Union[str, int, float]]: 355 | """ 356 | Load params as a dictionary. 357 | 358 | :param args: input arguments 359 | """ 360 | params = load_method_params(args.method) 361 | for item in params: 362 | if getattr(args, item) is not None: 363 | params[item] = getattr(args, item) 364 | if getattr(args, "water") is None: 365 | params["water"] = convert_water(params["water"], params["water_unit"]) 366 | if getattr(args, "coffee") is None: 367 | params["coffee"] = convert_coffee(params["coffee"], params["coffee_unit"]) 368 | if getattr(args, "temperature") is None: 369 | params["temperature"] = convert_temperature( 370 | params["temperature"], 371 | from_unit="C", 372 | to_unit=params["temperature_unit"], 373 | digits=params["digits"]) 374 | params["method"] = args.method 375 | return params 376 | 377 | 378 | def filter_params(params: Dict[str, Union[str, int, float]]) -> Dict[str, Union[str, int, float]]: 379 | """ 380 | Filter params. 381 | 382 | :param params: parameters 383 | """ 384 | digits = params["digits"] 385 | items = [ 386 | ("coffee", "cup"), 387 | ("coffee", "total"), 388 | ("water", "cup"), 389 | ("water", "total"), 390 | ("coffee", "ratio"), 391 | ("water", "ratio"), 392 | ("temperature", "value"), 393 | ("ratio",) 394 | ] 395 | for item in items: 396 | if len(item) == 2: 397 | key1, key2 = item 398 | value = round(params[key1][key2], digits) 399 | params[key1][key2] = int(value) if is_int(value) else value 400 | else: 401 | key = item[0] 402 | value = round(params[key], digits) 403 | params[key] = int(value) if is_int(value) else value 404 | if len(params["message"]) == 0: 405 | params["message"] = EMPTY_MESSAGE 406 | return params 407 | 408 | 409 | def check_ratio_limits(method: str, ratio: float) -> bool: 410 | """ 411 | Return True if the ratio is within limits, otherwise False. 412 | 413 | :param method: brewing method 414 | :param ratio: coffee/water ratio 415 | """ 416 | if "ratio_lower_limit" in METHODS_MAP[method] and "ratio_upper_limit" in METHODS_MAP[method]: 417 | ratio_lower_limit = float(METHODS_MAP[method]["ratio_lower_limit"]) 418 | ratio_upper_limit = float(METHODS_MAP[method]["ratio_upper_limit"]) 419 | if ratio < ratio_lower_limit or ratio > ratio_upper_limit: 420 | return False 421 | return True 422 | 423 | 424 | def check_grind_limits(method: str, grind: int) -> bool: 425 | """ 426 | Return True if the grind is within limits, otherwise False. 427 | 428 | :param method: brewing method 429 | :param grind: grind size 430 | """ 431 | if "grind_lower_limit" in METHODS_MAP[method] and "grind_upper_limit" in METHODS_MAP[method]: 432 | grind_lower_limit = METHODS_MAP[method]["grind_lower_limit"] 433 | grind_upper_limit = METHODS_MAP[method]["grind_upper_limit"] 434 | if grind < grind_lower_limit or grind > grind_upper_limit: 435 | return False 436 | return True 437 | 438 | 439 | def convert_temperature(value: float, from_unit: str, to_unit: str, digits: int = 3) -> float: 440 | """ 441 | Convert temperature. 442 | 443 | :param value: temperature value to convert 444 | :param from_unit: unit of the input value 445 | :param to_unit: unit to convert to 446 | :param digits: number of digits up to which the result is rounded 447 | """ 448 | from_unit = from_unit.upper() 449 | to_unit = to_unit.upper() 450 | 451 | result = value 452 | if from_unit != to_unit: 453 | if from_unit == 'F': 454 | celsius = (value - 32) * 5 / 9 455 | elif from_unit == 'K': 456 | celsius = value - 273.15 457 | else: 458 | celsius = value 459 | 460 | if to_unit == 'F': 461 | result = (celsius * 9 / 5) + 32 462 | elif to_unit == 'K': 463 | result = celsius + 273.15 464 | else: 465 | result = celsius 466 | result = round(result, digits) 467 | if is_int(result): 468 | result = int(result) 469 | return result 470 | 471 | 472 | def check_temperature_limits(method: str, temperature: float, temperature_unit: str) -> bool: 473 | """ 474 | Return True if the temperature is within limits, otherwise False. 475 | 476 | :param method: brewing method 477 | :param temperature: temperature value 478 | :param temperature_unit: temperature unit 479 | """ 480 | if "temperature_lower_limit" in METHODS_MAP[method] and "temperature_upper_limit" in METHODS_MAP[method]: 481 | temperature = convert_temperature(temperature, from_unit=temperature_unit, to_unit="C") 482 | temperature_lower_limit = METHODS_MAP[method]["temperature_lower_limit"] 483 | temperature_upper_limit = METHODS_MAP[method]["temperature_upper_limit"] 484 | if temperature < temperature_lower_limit or temperature > temperature_upper_limit: 485 | return False 486 | return True 487 | 488 | 489 | def convert_coffee(coffee: float, unit: str, reverse: bool = False) -> Union[float, int]: 490 | """ 491 | Convert and return the coffee amount as a float or int. 492 | 493 | :param coffee: coffee amount 494 | :param unit: coffee unit 495 | :param reverse: reverse convert flag 496 | """ 497 | rate = COFFEE_UNITS_MAP[unit]["rate"] 498 | if reverse: 499 | rate = 1 / rate 500 | coffee = coffee * rate 501 | if unit == "cb": 502 | coffee = math.ceil(coffee) 503 | return coffee 504 | 505 | 506 | def convert_water(water: float, unit: str, reverse: bool = False) -> Union[float, int]: 507 | """ 508 | Convert and return the water amount as a float or int. 509 | 510 | :param water: water amount 511 | :param unit: water unit 512 | :param reverse: reverse convert flag 513 | """ 514 | rate = WATER_UNITS_MAP[unit]["rate"] 515 | if reverse: 516 | rate = 1 / rate 517 | water = water * rate 518 | return water 519 | 520 | 521 | def calculate_coffee(ratio: float, water: float, water_unit: str, coffee_unit: str) -> float: 522 | """ 523 | Calculate coffee. 524 | 525 | :param ratio: coffee/water ratio 526 | :param water: water amount 527 | :param water_unit: water unit 528 | :param coffee_unit: coffee unit 529 | """ 530 | water_gram = convert_water(water, water_unit, True) 531 | coffee_gram = water_gram * ratio 532 | coffee = convert_coffee(coffee_gram, coffee_unit) 533 | return coffee 534 | 535 | 536 | def calculate_water(ratio: float, coffee: float, water_unit: str, coffee_unit: str) -> float: 537 | """ 538 | Calculate water. 539 | 540 | :param ratio: coffee/water ratio 541 | :param coffee: coffee amount 542 | :param water_unit: water unit 543 | :param coffee_unit: coffee unit 544 | """ 545 | coffee_gram = convert_coffee(coffee, coffee_unit, True) 546 | water_gram = coffee_gram * (1 / ratio) 547 | water = convert_water(water_gram, water_unit) 548 | return water 549 | 550 | 551 | def calculate_ratio(coffee: float, water: float, coffee_unit: str, water_unit: str) -> float: 552 | """ 553 | Calculate ratio. 554 | 555 | :param coffee: coffee amount 556 | :param water: water amount 557 | :param coffee_unit: coffee unit 558 | :param water_unit: water unit 559 | """ 560 | coffee_gram = convert_coffee(coffee, coffee_unit, True) 561 | water_gram = convert_water(water, water_unit, True) 562 | ratio = coffee_gram / water_gram 563 | return ratio 564 | 565 | 566 | def get_result_by_water(params: Dict[str, Union[str, int, float]], 567 | enable_filter: bool = True) -> Dict[str, Union[str, int, float, dict]]: 568 | """ 569 | Get result by water. 570 | 571 | :param params: parameters 572 | :param enable_filter: filter flag 573 | """ 574 | result_params = params.copy() 575 | result_params["ratio"] = params["coffee_ratio"] / params["water_ratio"] 576 | result_params["coffee"] = { 577 | "total": None, 578 | "cup": calculate_coffee( 579 | ratio=result_params["ratio"], 580 | water=params["water"], 581 | water_unit=params["water_unit"], 582 | coffee_unit=params["coffee_unit"]), 583 | "ratio": params["coffee_ratio"], 584 | "unit": params["coffee_unit"]} 585 | result_params["water"] = { 586 | "total": result_params["cups"] * params["water"], 587 | "cup": params["water"], 588 | "ratio": params["water_ratio"], 589 | "unit": params["water_unit"]} 590 | result_params["grind"] = { 591 | "unit": "um", 592 | "value": params["grind"], 593 | "type": get_grind_type(params["grind"]), 594 | } 595 | result_params["temperature"] = { 596 | "unit": params["temperature_unit"], 597 | "value": params["temperature"] 598 | } 599 | for item in ["temperature_unit", "water_ratio", "coffee_ratio", "coffee_unit", "water_unit"]: 600 | del result_params[item] 601 | result_params["coffee"]["total"] = result_params["cups"] * result_params["coffee"]["cup"] 602 | result_params["strength"] = get_brew_strength(ratio=result_params["ratio"]) 603 | if enable_filter: 604 | result_params = filter_params(result_params) 605 | result_params["warnings"] = get_warnings(result_params) 606 | result_params["date"] = get_date_now() 607 | return result_params 608 | 609 | 610 | def get_result_by_coffee(params: Dict[str, Union[str, int, float]], 611 | enable_filter: bool = True) -> Dict[str, Union[str, int, float, dict]]: 612 | """ 613 | Get result by coffee. 614 | 615 | :param params: parameters 616 | :param enable_filter: filter flag 617 | """ 618 | result_params = params.copy() 619 | result_params["ratio"] = params["coffee_ratio"] / params["water_ratio"] 620 | result_params["coffee"] = { 621 | "total": result_params["cups"] * params["coffee"], 622 | "cup": params["coffee"], 623 | "ratio": params["coffee_ratio"], 624 | "unit": params["coffee_unit"]} 625 | result_params["water"] = { 626 | "total": None, 627 | "cup": calculate_water( 628 | ratio=result_params["ratio"], 629 | coffee=params["coffee"], 630 | water_unit=params["water_unit"], 631 | coffee_unit=params["coffee_unit"]), 632 | "ratio": params["water_ratio"], 633 | "unit": params["water_unit"]} 634 | result_params["grind"] = { 635 | "unit": "um", 636 | "value": params["grind"], 637 | "type": get_grind_type(params["grind"]), 638 | } 639 | result_params["temperature"] = { 640 | "unit": params["temperature_unit"], 641 | "value": params["temperature"] 642 | } 643 | for item in ["temperature_unit", "water_ratio", "coffee_ratio", "coffee_unit", "water_unit"]: 644 | del result_params[item] 645 | result_params["water"]["total"] = result_params["cups"] * result_params["water"]["cup"] 646 | result_params["strength"] = get_brew_strength(ratio=result_params["ratio"]) 647 | if enable_filter: 648 | result_params = filter_params(result_params) 649 | result_params["warnings"] = get_warnings(result_params) 650 | result_params["date"] = get_date_now() 651 | return result_params 652 | 653 | 654 | def get_result_by_coffee_and_water(params: Dict[str, Union[str, int, float]], 655 | enable_filter: bool = True) -> Dict[str, Union[str, int, float, dict]]: 656 | """ 657 | Get result by coffee and water. 658 | 659 | :param params: parameters 660 | :param enable_filter: filter flag 661 | """ 662 | result_params = params.copy() 663 | result_params["ratio"] = calculate_ratio( 664 | result_params["coffee"], 665 | result_params["water"], 666 | result_params["coffee_unit"], 667 | result_params["water_unit"]) 668 | ratio_fraction = fractions.Fraction(result_params["ratio"]).limit_denominator() 669 | result_params["coffee"] = { 670 | "total": result_params["cups"] * params["coffee"], 671 | "cup": params["coffee"], 672 | "ratio": ratio_fraction.numerator, 673 | "unit": params["coffee_unit"]} 674 | result_params["water"] = { 675 | "total": result_params["cups"] * params["water"], 676 | "cup": params["water"], 677 | "ratio": ratio_fraction.denominator, 678 | "unit": params["water_unit"]} 679 | result_params["grind"] = { 680 | "unit": "um", 681 | "value": params["grind"], 682 | "type": get_grind_type(params["grind"]), 683 | } 684 | result_params["temperature"] = { 685 | "unit": params["temperature_unit"], 686 | "value": params["temperature"] 687 | } 688 | for item in ["temperature_unit", "water_ratio", "coffee_ratio", "coffee_unit", "water_unit"]: 689 | del result_params[item] 690 | result_params["strength"] = get_brew_strength(ratio=result_params["ratio"]) 691 | if enable_filter: 692 | result_params = filter_params(result_params) 693 | result_params["warnings"] = get_warnings(result_params) 694 | result_params["date"] = get_date_now() 695 | return result_params 696 | 697 | 698 | def get_result(params: Dict[str, Union[str, int, float]], 699 | enable_filter: bool = True) -> Dict[str, Union[str, int, float, dict]]: 700 | """ 701 | Get result. 702 | 703 | :param params: parameters 704 | :param enable_filter: filter flag 705 | """ 706 | if params["mode"] == "water-to-coffee": 707 | result_params = get_result_by_water(params=params, enable_filter=enable_filter) 708 | elif params["mode"] == "coffee-to-water": 709 | result_params = get_result_by_coffee(params=params, enable_filter=enable_filter) 710 | else: 711 | result_params = get_result_by_coffee_and_water(params=params, enable_filter=enable_filter) 712 | return result_params 713 | 714 | 715 | def run_program(args: argparse.Namespace) -> None: 716 | """ 717 | Run program. 718 | 719 | :param args: input arguments 720 | """ 721 | if args.version: 722 | print(MY_COFFEE_VERSION) 723 | elif args.info: # pragma: no cover 724 | mycoffee_info() 725 | elif args.methods_list: 726 | show_methods_list() 727 | elif args.coffee_units_list: 728 | show_coffee_units_list() 729 | elif args.water_units_list: 730 | show_water_units_list() 731 | elif args.temperature_units_list: 732 | show_temperature_units_list() 733 | else: 734 | input_params = load_params(args) 735 | result_params = get_result(input_params) 736 | print_result(params=result_params, ignore_warnings=args.ignore_warnings) 737 | if args.save_path: 738 | save_details = save_result( 739 | params=result_params, 740 | file_path=args.save_path, 741 | file_format=args.save_format, 742 | ignore_warnings=args.ignore_warnings) 743 | if not save_details["status"]: 744 | print(SAVE_FILE_ERROR_MESSAGE) 745 | else: 746 | print(save_details["message"]) 747 | -------------------------------------------------------------------------------- /test/functions_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | >>> import os 4 | >>> import json 5 | >>> import yaml 6 | >>> import argparse 7 | >>> from mycoffee.functions import * 8 | >>> from mycoffee.params import * 9 | >>> convert_coffee(122, "g") 10 | 122 11 | >>> convert_coffee(122, "cb") 12 | 921 13 | >>> convert_water(1, "g") 14 | 1 15 | >>> convert_water(1, "kg") 16 | 0.001 17 | >>> convert_water(1, "kg", False) 18 | 0.001 19 | >>> convert_water(1, "kg", True) 20 | 1000.0 21 | >>> get_grind_type(100) 22 | 'Extra-Fine' 23 | >>> get_brew_strength(1/60) 24 | 'Very Weak' 25 | >>> get_brew_strength(1/30) 26 | 'Weak' 27 | >>> get_brew_strength(1/22) 28 | 'Medium' 29 | >>> get_brew_strength(1/15) 30 | 'Strong' 31 | >>> get_brew_strength(1/2) 32 | 'Very Strong' 33 | >>> input_params = {"method":"v60", "cups":2, "water":500, "coffee_ratio": 3, "water_ratio":50, "message":"V60 method", "digits":3, "coffee_unit": "g", "water_unit": "g", "temperature_unit": "C", "grind": 500, "temperature":93, "mode":"water-to-coffee"} 34 | >>> result_params = get_result(input_params) 35 | >>> print_result(result_params) 36 | __ __ _ _ ___ _____ ____ ____ ____ ____ 37 | ( \/ )( \/ ) / __)( _ )( ___)( ___)( ___)( ___) 38 | ) ( \ / ( (__ )(_)( )__) )__) )__) )__) 39 | (_/\/\_) (__) \___)(_____)(__) (__) (____)(____) 40 | 41 | 42 | 43 | 44 | ... 45 | 46 | Mode: Water --> Coffee 47 | 48 | Method: `v60` 49 | 50 | Cups: 2 51 | 52 | Coffee: 53 | - Cup: 30 g 54 | - Total: 60 g 55 | 56 | Water: 57 | 58 | - Cup: 500 g 59 | - Total: 1000 g 60 | 61 | Ratio: 3/50 (0.06) 62 | 63 | Strength: Medium 64 | 65 | Grind: 500 um (Medium-Fine) 66 | 67 | Temperature: 93 C 68 | 69 | Message: V60 method 70 | 71 | >>> save_details = save_result(result_params, "save_test1.txt") 72 | >>> save_details["status"] 73 | True 74 | >>> save_details["message"] == "[Info] File saved successfully!" 75 | True 76 | >>> file = open("save_test1.txt", "r") 77 | >>> print(file.read()) 78 | 79 | ... 80 | 81 | Mode: Water --> Coffee 82 | 83 | Method: `v60` 84 | 85 | Cups: 2 86 | 87 | Coffee: 88 | - Cup: 30 g 89 | - Total: 60 g 90 | 91 | Water: 92 | 93 | - Cup: 500 g 94 | - Total: 1000 g 95 | 96 | Ratio: 3/50 (0.06) 97 | 98 | Strength: Medium 99 | 100 | Grind: 500 um (Medium-Fine) 101 | 102 | Temperature: 93 C 103 | 104 | Message: V60 method 105 | >>> file.close() 106 | >>> save_details = save_result(result_params, "save_test1.json", "json") 107 | >>> save_details["status"] 108 | True 109 | >>> save_details["message"] == "[Info] File saved successfully!" 110 | True 111 | >>> file = open("save_test1.json", "r") 112 | >>> save_test1_object = json.load(file) 113 | >>> _ = format_date(save_test1_object["date"]) 114 | >>> del save_test1_object["date"] 115 | >>> save_test1_object == {'mycoffee_version': MY_COFFEE_VERSION, "mode":"water-to-coffee", 'temperature': {'value':93, 'unit':'C'}, 'method': 'v60', 'water': {'cup':500, 'total':1000, 'unit':'g','ratio':50}, 'cups': 2, 'digits': 3, 'coffee': {'total':60, 'cup': 30, 'unit': 'g', 'ratio': 3}, 'message': 'V60 method', 'grind': {'value':500, 'unit':'um', 'type':get_grind_type(500)},'warnings': [], 'ratio': 0.06, 'strength': get_brew_strength(0.06)} 116 | True 117 | >>> file.close() 118 | >>> save_details = save_result(result_params, "save_test1.yaml", "yaml") 119 | >>> save_details["status"] 120 | True 121 | >>> save_details["message"] == "[Info] File saved successfully!" 122 | True 123 | >>> file = open("save_test1.yaml", "r") 124 | >>> save_test1_object = yaml.safe_load(file) 125 | >>> _ = format_date(save_test1_object["date"]) 126 | >>> del save_test1_object["date"] 127 | >>> save_test1_object == {'mycoffee_version': MY_COFFEE_VERSION, "mode":"water-to-coffee", 'temperature': {'value':93, 'unit':'C'}, 'method': 'v60', 'water': {'cup':500, 'total':1000, 'unit':'g','ratio':50}, 'cups': 2, 'digits': 3, 'coffee': {'total':60, 'cup': 30, 'unit': 'g', 'ratio': 3}, 'message': 'V60 method', 'grind': {'value':500, 'unit':'um', 'type':get_grind_type(500)},'warnings': [], 'ratio': 0.06, 'strength': get_brew_strength(0.06)} 128 | True 129 | >>> file.close() 130 | >>> input_params = {"method":"v60", "cups":2, "coffee":30, "water":500, "coffee_ratio": 3, "water_ratio":50, "message":"V60 method", "digits":3, "coffee_unit": "g", "water_unit": "g", "temperature_unit": "C", "grind": 500, "temperature":93, "mode":"ratio"} 131 | >>> result_params = get_result(input_params) 132 | >>> print_result(result_params) 133 | __ __ _ _ ___ _____ ____ ____ ____ ____ 134 | ( \/ )( \/ ) / __)( _ )( ___)( ___)( ___)( ___) 135 | ) ( \ / ( (__ )(_)( )__) )__) )__) )__) 136 | (_/\/\_) (__) \___)(_____)(__) (__) (____)(____) 137 | 138 | 139 | 140 | 141 | ... 142 | 143 | Mode: Water & Coffee --> Ratio 144 | 145 | Method: `v60` 146 | 147 | Cups: 2 148 | 149 | Coffee: 150 | - Cup: 30 g 151 | - Total: 60 g 152 | 153 | Water: 154 | 155 | - Cup: 500 g 156 | - Total: 1000 g 157 | 158 | Ratio: 3/50 (0.06) 159 | 160 | Strength: Medium 161 | 162 | Grind: 500 um (Medium-Fine) 163 | 164 | Temperature: 93 C 165 | 166 | Message: V60 method 167 | 168 | >>> save_details = save_result(result_params, "save_test7.txt") 169 | >>> save_details["status"] 170 | True 171 | >>> save_details["message"] == "[Info] File saved successfully!" 172 | True 173 | >>> file = open("save_test7.txt", "r") 174 | >>> print(file.read()) 175 | 176 | ... 177 | 178 | Mode: Water & Coffee --> Ratio 179 | 180 | Method: `v60` 181 | 182 | Cups: 2 183 | 184 | Coffee: 185 | - Cup: 30 g 186 | - Total: 60 g 187 | 188 | Water: 189 | 190 | - Cup: 500 g 191 | - Total: 1000 g 192 | 193 | Ratio: 3/50 (0.06) 194 | 195 | Strength: Medium 196 | 197 | Grind: 500 um (Medium-Fine) 198 | 199 | Temperature: 93 C 200 | 201 | Message: V60 method 202 | >>> file.close() 203 | >>> save_details = save_result(result_params, "save_test7.json", "json") 204 | >>> save_details["status"] 205 | True 206 | >>> save_details["message"] == "[Info] File saved successfully!" 207 | True 208 | >>> file = open("save_test7.json", "r") 209 | >>> save_test4_object = json.load(file) 210 | >>> _ = format_date(save_test4_object["date"]) 211 | >>> del save_test4_object["date"] 212 | >>> save_test4_object == {'mycoffee_version': MY_COFFEE_VERSION, "mode":"ratio", 'temperature': {'value':93, 'unit':'C'}, 'method': 'v60', 'water': {'cup':500, 'total':1000, 'unit':'g','ratio':50}, 'cups': 2, 'digits': 3, 'coffee': {'total':60, 'cup': 30, 'unit': 'g', 'ratio': 3}, 'message': 'V60 method', 'grind': {'value':500, 'unit':'um', 'type':get_grind_type(500)},'warnings': [], 'ratio': 0.06, 'strength': get_brew_strength(0.06)} 213 | True 214 | >>> file.close() 215 | >>> save_details = save_result(result_params, "save_test7.yaml", "yaml") 216 | >>> save_details["status"] 217 | True 218 | >>> save_details["message"] == "[Info] File saved successfully!" 219 | True 220 | >>> file = open("save_test7.yaml", "r") 221 | >>> save_test4_object = yaml.safe_load(file) 222 | >>> _ = format_date(save_test4_object["date"]) 223 | >>> del save_test4_object["date"] 224 | >>> save_test4_object == {'mycoffee_version': MY_COFFEE_VERSION, "mode":"ratio", 'temperature': {'value':93, 'unit':'C'}, 'method': 'v60', 'water': {'cup':500, 'total':1000, 'unit':'g','ratio':50}, 'cups': 2, 'digits': 3, 'coffee': {'total':60, 'cup': 30, 'unit': 'g', 'ratio': 3}, 'message': 'V60 method', 'grind': {'value':500, 'unit':'um', 'type':get_grind_type(500)},'warnings': [], 'ratio': 0.06, 'strength': get_brew_strength(0.06)} 225 | True 226 | >>> input_params = {"method":"v60", "cups":2, "coffee":30, "coffee_ratio": 3, "water_ratio":50, "message":"V60 method", "digits":3, "coffee_unit": "g", "water_unit": "g", "temperature_unit": "C", "grind": 500, "temperature":93, "mode":"coffee-to-water"} 227 | >>> result_params = get_result(input_params) 228 | >>> print_result(result_params) 229 | __ __ _ _ ___ _____ ____ ____ ____ ____ 230 | ( \/ )( \/ ) / __)( _ )( ___)( ___)( ___)( ___) 231 | ) ( \ / ( (__ )(_)( )__) )__) )__) )__) 232 | (_/\/\_) (__) \___)(_____)(__) (__) (____)(____) 233 | 234 | 235 | 236 | 237 | ... 238 | 239 | Mode: Coffee --> Water 240 | 241 | Method: `v60` 242 | 243 | Cups: 2 244 | 245 | Coffee: 246 | - Cup: 30 g 247 | - Total: 60 g 248 | 249 | Water: 250 | 251 | - Cup: 500 g 252 | - Total: 1000 g 253 | 254 | Ratio: 3/50 (0.06) 255 | 256 | Strength: Medium 257 | 258 | Grind: 500 um (Medium-Fine) 259 | 260 | Temperature: 93 C 261 | 262 | Message: V60 method 263 | 264 | >>> save_details = save_result(result_params, "save_test4.txt") 265 | >>> save_details["status"] 266 | True 267 | >>> save_details["message"] == "[Info] File saved successfully!" 268 | True 269 | >>> file = open("save_test4.txt", "r") 270 | >>> print(file.read()) 271 | 272 | ... 273 | 274 | Mode: Coffee --> Water 275 | 276 | Method: `v60` 277 | 278 | Cups: 2 279 | 280 | Coffee: 281 | - Cup: 30 g 282 | - Total: 60 g 283 | 284 | Water: 285 | 286 | - Cup: 500 g 287 | - Total: 1000 g 288 | 289 | Ratio: 3/50 (0.06) 290 | 291 | Strength: Medium 292 | 293 | Grind: 500 um (Medium-Fine) 294 | 295 | Temperature: 93 C 296 | 297 | Message: V60 method 298 | >>> file.close() 299 | >>> save_details = save_result(result_params, "save_test4.json", "json") 300 | >>> save_details["status"] 301 | True 302 | >>> save_details["message"] == "[Info] File saved successfully!" 303 | True 304 | >>> file = open("save_test4.json", "r") 305 | >>> save_test4_object = json.load(file) 306 | 307 | >>> _ = format_date(save_test4_object["date"]) 308 | >>> del save_test4_object["date"] 309 | >>> save_test4_object == {'mycoffee_version': MY_COFFEE_VERSION, "mode":"coffee-to-water", 'temperature': {'value':93, 'unit':'C'}, 'method': 'v60', 'water': {'cup':500, 'total':1000, 'unit':'g','ratio':50}, 'cups': 2, 'digits': 3, 'coffee': {'total':60, 'cup': 30, 'unit': 'g', 'ratio': 3}, 'message': 'V60 method', 'grind': {'value':500, 'unit':'um', 'type':get_grind_type(500)},'warnings': [], 'ratio': 0.06, 'strength': get_brew_strength(0.06)} 310 | True 311 | >>> file.close() 312 | >>> save_details = save_result(result_params, "save_test4.yaml", "yaml") 313 | >>> save_details["status"] 314 | True 315 | >>> save_details["message"] == "[Info] File saved successfully!" 316 | True 317 | >>> file = open("save_test4.yaml", "r") 318 | >>> save_test4_object = yaml.safe_load(file) 319 | >>> _ = format_date(save_test4_object["date"]) 320 | >>> del save_test4_object["date"] 321 | >>> save_test4_object == {'mycoffee_version': MY_COFFEE_VERSION, "mode":"coffee-to-water", 'temperature': {'value':93, 'unit':'C'}, 'method': 'v60', 'water': {'cup':500, 'total':1000, 'unit':'g','ratio':50}, 'cups': 2, 'digits': 3, 'coffee': {'total':60, 'cup': 30, 'unit': 'g', 'ratio': 3}, 'message': 'V60 method', 'grind': {'value':500, 'unit':'um', 'type':get_grind_type(500)},'warnings': [], 'ratio': 0.06, 'strength': get_brew_strength(0.06)} 322 | True 323 | >>> file.close() 324 | >>> save_details = save_result({}, 2) 325 | >>> save_details["status"] 326 | False 327 | >>> input_params = {"method":"v60", "cups":2, "water":500, "coffee_ratio": 3, "water_ratio":50, "message":"V60 method", "digits":3, "coffee_unit": "g", "water_unit": "g", "temperature_unit": "F", "grind": 500, "temperature":65, "mode":"water-to-coffee"} 328 | >>> result_params = get_result(input_params) 329 | >>> print_result(result_params) 330 | __ __ _ _ ___ _____ ____ ____ ____ ____ 331 | ( \/ )( \/ ) / __)( _ )( ___)( ___)( ___)( ___) 332 | ) ( \ / ( (__ )(_)( )__) )__) )__) )__) 333 | (_/\/\_) (__) \___)(_____)(__) (__) (____)(____) 334 | 335 | 336 | 337 | 338 | ... 339 | 340 | Mode: Water --> Coffee 341 | 342 | Method: `v60` 343 | 344 | Cups: 2 345 | 346 | Coffee: 347 | - Cup: 30 g 348 | - Total: 60 g 349 | 350 | Water: 351 | 352 | - Cup: 500 g 353 | - Total: 1000 g 354 | 355 | Ratio: 3/50 (0.06) 356 | 357 | Strength: Medium 358 | 359 | Grind: 500 um (Medium-Fine) 360 | 361 | Temperature: 65 F 362 | 363 | Message: V60 method 364 | 365 | [Warning] The temperature is not within the recommended range. For `v60`, the temperature can be anywhere between `185 F` and `203 F` 366 | >>> input_params = {"method":"v60", "cups":2, "coffee":30, "coffee_ratio": 3, "water_ratio":50, "message":"V60 method", "digits":3, "coffee_unit": "g", "water_unit": "g", "temperature_unit": "F", "grind": 500, "temperature":65, "mode":"coffee-to-water"} 367 | >>> result_params = get_result(input_params) 368 | >>> print_result(result_params) 369 | __ __ _ _ ___ _____ ____ ____ ____ ____ 370 | ( \/ )( \/ ) / __)( _ )( ___)( ___)( ___)( ___) 371 | ) ( \ / ( (__ )(_)( )__) )__) )__) )__) 372 | (_/\/\_) (__) \___)(_____)(__) (__) (____)(____) 373 | 374 | 375 | 376 | 377 | ... 378 | 379 | Mode: Coffee --> Water 380 | 381 | Method: `v60` 382 | 383 | Cups: 2 384 | 385 | Coffee: 386 | - Cup: 30 g 387 | - Total: 60 g 388 | 389 | Water: 390 | 391 | - Cup: 500 g 392 | - Total: 1000 g 393 | 394 | Ratio: 3/50 (0.06) 395 | 396 | Strength: Medium 397 | 398 | Grind: 500 um (Medium-Fine) 399 | 400 | Temperature: 65 F 401 | 402 | Message: V60 method 403 | 404 | [Warning] The temperature is not within the recommended range. For `v60`, the temperature can be anywhere between `185 F` and `203 F` 405 | >>> print_result(result_params, ignore_warnings=True) 406 | __ __ _ _ ___ _____ ____ ____ ____ ____ 407 | ( \/ )( \/ ) / __)( _ )( ___)( ___)( ___)( ___) 408 | ) ( \ / ( (__ )(_)( )__) )__) )__) )__) 409 | (_/\/\_) (__) \___)(_____)(__) (__) (____)(____) 410 | 411 | 412 | 413 | 414 | ... 415 | 416 | Mode: Coffee --> Water 417 | 418 | Method: `v60` 419 | 420 | Cups: 2 421 | 422 | Coffee: 423 | - Cup: 30 g 424 | - Total: 60 g 425 | 426 | Water: 427 | 428 | - Cup: 500 g 429 | - Total: 1000 g 430 | 431 | Ratio: 3/50 (0.06) 432 | 433 | Strength: Medium 434 | 435 | Grind: 500 um (Medium-Fine) 436 | 437 | Temperature: 65 F 438 | 439 | Message: V60 method 440 | 441 | >>> input_params = {"method":"v60", "cups":2, "water":500, "coffee_ratio": 3, "water_ratio":50, "message":"", "digits":3, "coffee_unit": "g", "water_unit": "g", "grind": 600, "temperature":95, "temperature_unit": "C", "mode":"water-to-coffee"} 442 | >>> result_params = get_result(input_params) 443 | >>> check_ratio_limits(method=result_params["method"], ratio=result_params["ratio"]) 444 | True 445 | >>> check_grind_limits(method=result_params["method"], grind=result_params["grind"]["value"]) 446 | True 447 | >>> check_temperature_limits(method=result_params["method"], temperature=result_params["temperature"]["value"], temperature_unit=result_params["temperature"]["unit"]) 448 | True 449 | >>> print_result(result_params) 450 | __ __ _ _ ___ _____ ____ ____ ____ ____ 451 | ( \/ )( \/ ) / __)( _ )( ___)( ___)( ___)( ___) 452 | ) ( \ / ( (__ )(_)( )__) )__) )__) )__) 453 | (_/\/\_) (__) \___)(_____)(__) (__) (____)(____) 454 | 455 | 456 | 457 | 458 | ... 459 | 460 | Mode: Water --> Coffee 461 | 462 | Method: `v60` 463 | 464 | Cups: 2 465 | 466 | Coffee: 467 | - Cup: 30 g 468 | - Total: 60 g 469 | 470 | Water: 471 | 472 | - Cup: 500 g 473 | - Total: 1000 g 474 | 475 | Ratio: 3/50 (0.06) 476 | 477 | Strength: Medium 478 | 479 | Grind: 600 um (Medium-Fine) 480 | 481 | Temperature: 95 C 482 | 483 | Message: Nothing :) 484 | 485 | >>> input_params = {"method":"v60", "cups":2, "water":0.5, "coffee_ratio": 3, "water_ratio":50, "message":"", "digits":3, "coffee_unit": "g", "water_unit": "kg", "grind": 700, "temperature":95, "temperature_unit": "C", "mode":"water-to-coffee"} 486 | >>> result_params = get_result(input_params) 487 | >>> check_ratio_limits(method=result_params["method"], ratio=result_params["ratio"]) 488 | True 489 | >>> check_grind_limits(method=result_params["method"], grind=result_params["grind"]["value"]) 490 | True 491 | >>> check_temperature_limits(method=result_params["method"], temperature=result_params["temperature"]["value"], temperature_unit=result_params["temperature"]["unit"]) 492 | True 493 | >>> print_result(result_params) 494 | __ __ _ _ ___ _____ ____ ____ ____ ____ 495 | ( \/ )( \/ ) / __)( _ )( ___)( ___)( ___)( ___) 496 | ) ( \ / ( (__ )(_)( )__) )__) )__) )__) 497 | (_/\/\_) (__) \___)(_____)(__) (__) (____)(____) 498 | 499 | 500 | 501 | 502 | ... 503 | 504 | Mode: Water --> Coffee 505 | 506 | Method: `v60` 507 | 508 | Cups: 2 509 | 510 | Coffee: 511 | - Cup: 30 g 512 | - Total: 60 g 513 | 514 | Water: 515 | 516 | - Cup: 0.5 kg 517 | - Total: 1 kg 518 | 519 | Ratio: 3/50 (0.06) 520 | 521 | Strength: Medium 522 | 523 | Grind: 700 um (Medium) 524 | 525 | Temperature: 95 C 526 | 527 | Message: Nothing :) 528 | 529 | >>> input_params = {"method":"v60", "cups":2, "water":500, "coffee_ratio": 6, "water_ratio":1000, "message":"", "digits":3, "coffee_unit": "g", "water_unit": "g", "grind": 500, "temperature":95, "temperature_unit": "C", "mode":"water-to-coffee"} 530 | >>> result_params = get_result(input_params) 531 | >>> check_ratio_limits(method=result_params["method"], ratio=result_params["ratio"]) 532 | False 533 | >>> check_grind_limits(method=result_params["method"], grind=result_params["grind"]["value"]) 534 | True 535 | >>> check_temperature_limits(method=result_params["method"], temperature=result_params["temperature"]["value"], temperature_unit=result_params["temperature"]["unit"]) 536 | True 537 | >>> print_result(result_params) 538 | __ __ _ _ ___ _____ ____ ____ ____ ____ 539 | ( \/ )( \/ ) / __)( _ )( ___)( ___)( ___)( ___) 540 | ) ( \ / ( (__ )(_)( )__) )__) )__) )__) 541 | (_/\/\_) (__) \___)(_____)(__) (__) (____)(____) 542 | 543 | 544 | 545 | 546 | ... 547 | 548 | Mode: Water --> Coffee 549 | 550 | Method: `v60` 551 | 552 | Cups: 2 553 | 554 | Coffee: 555 | - Cup: 3 g 556 | - Total: 6 g 557 | 558 | Water: 559 | 560 | - Cup: 500 g 561 | - Total: 1000 g 562 | 563 | Ratio: 6/1000 (0.006) 564 | 565 | Strength: Very Weak 566 | 567 | Grind: 500 um (Medium-Fine) 568 | 569 | Temperature: 95 C 570 | 571 | Message: Nothing :) 572 | 573 | [Warning] The ratio is not within the recommended range. For `v60`, the ratio can be anywhere between `1/18` and `1/14` 574 | >>> input_params = {"method":"v60", "cups":2, "water":500, "coffee_ratio": 1, "water_ratio":18, "message":"", "digits":3, "coffee_unit": "g", "water_unit": "g", "grind": 1400, "temperature":95,"temperature_unit": "C", "mode":"water-to-coffee"} 575 | >>> result_params = get_result(input_params) 576 | >>> check_ratio_limits(method=result_params["method"], ratio=result_params["ratio"]) 577 | True 578 | >>> check_grind_limits(method=result_params["method"], grind=result_params["grind"]["value"]) 579 | False 580 | >>> check_temperature_limits(method=result_params["method"], temperature=result_params["temperature"]["value"], temperature_unit=result_params["temperature"]["unit"]) 581 | True 582 | >>> print_result(result_params) 583 | __ __ _ _ ___ _____ ____ ____ ____ ____ 584 | ( \/ )( \/ ) / __)( _ )( ___)( ___)( ___)( ___) 585 | ) ( \ / ( (__ )(_)( )__) )__) )__) )__) 586 | (_/\/\_) (__) \___)(_____)(__) (__) (____)(____) 587 | 588 | 589 | 590 | 591 | ... 592 | 593 | Mode: Water --> Coffee 594 | 595 | Method: `v60` 596 | 597 | Cups: 2 598 | 599 | Coffee: 600 | - Cup: 27.778 g 601 | - Total: 55.556 g 602 | 603 | Water: 604 | 605 | - Cup: 500 g 606 | - Total: 1000 g 607 | 608 | Ratio: 1/18 (0.056) 609 | 610 | Strength: Medium 611 | 612 | Grind: 1400 um (Extra-Coarse) 613 | 614 | Temperature: 95 C 615 | 616 | Message: Nothing :) 617 | 618 | [Warning] The grind size is not within the recommended range. For `v60`, the grind size can be anywhere between `400 um` and `700 um` 619 | >>> input_params = {"method":"v60", "cups":2, "water":500, "coffee_ratio": 1, "water_ratio":18, "message":"", "digits":3, "coffee_unit": "g", "water_unit": "g", "grind": 20, "temperature": 50.2, "temperature_unit": "C", "mode":"water-to-coffee"} 620 | >>> result_params = get_result(input_params) 621 | >>> check_ratio_limits(method=result_params["method"], ratio=result_params["ratio"]) 622 | True 623 | >>> check_grind_limits(method=result_params["method"], grind=result_params["grind"]["value"]) 624 | False 625 | >>> check_temperature_limits(method=result_params["method"], temperature=result_params["temperature"]["value"], temperature_unit=result_params["temperature"]["unit"]) 626 | False 627 | >>> print_result(result_params) 628 | __ __ _ _ ___ _____ ____ ____ ____ ____ 629 | ( \/ )( \/ ) / __)( _ )( ___)( ___)( ___)( ___) 630 | ) ( \ / ( (__ )(_)( )__) )__) )__) )__) 631 | (_/\/\_) (__) \___)(_____)(__) (__) (____)(____) 632 | 633 | 634 | 635 | 636 | ... 637 | 638 | Mode: Water --> Coffee 639 | 640 | Method: `v60` 641 | 642 | Cups: 2 643 | 644 | Coffee: 645 | - Cup: 27.778 g 646 | - Total: 55.556 g 647 | 648 | Water: 649 | 650 | - Cup: 500 g 651 | - Total: 1000 g 652 | 653 | Ratio: 1/18 (0.056) 654 | 655 | Strength: Medium 656 | 657 | Grind: 20 um (Extra-Fine) 658 | 659 | Temperature: 50.2 C 660 | 661 | Message: Nothing :) 662 | 663 | [Warning] The grind size is not within the recommended range. For `v60`, the grind size can be anywhere between `400 um` and `700 um` 664 | [Warning] The temperature is not within the recommended range. For `v60`, the temperature can be anywhere between `85 C` and `95 C` 665 | >>> input_params = {"method":"v60", "cups":2, "water":500, "coffee_ratio": 1, "water_ratio":18, "message":"", "digits":3, "coffee_unit": "g", "water_unit": "g", "grind": 20, "temperature": 122.36, "temperature_unit": "F", "mode":"water-to-coffee"} 666 | >>> result_params = get_result(input_params) 667 | >>> check_ratio_limits(method=result_params["method"], ratio=result_params["ratio"]) 668 | True 669 | >>> check_grind_limits(method=result_params["method"], grind=result_params["grind"]["value"]) 670 | False 671 | >>> check_temperature_limits(method=result_params["method"], temperature=result_params["temperature"]["value"], temperature_unit=result_params["temperature"]["unit"]) 672 | False 673 | >>> print_result(result_params) 674 | __ __ _ _ ___ _____ ____ ____ ____ ____ 675 | ( \/ )( \/ ) / __)( _ )( ___)( ___)( ___)( ___) 676 | ) ( \ / ( (__ )(_)( )__) )__) )__) )__) 677 | (_/\/\_) (__) \___)(_____)(__) (__) (____)(____) 678 | 679 | 680 | 681 | 682 | ... 683 | 684 | Mode: Water --> Coffee 685 | 686 | Method: `v60` 687 | 688 | Cups: 2 689 | 690 | Coffee: 691 | - Cup: 27.778 g 692 | - Total: 55.556 g 693 | 694 | Water: 695 | 696 | - Cup: 500 g 697 | - Total: 1000 g 698 | 699 | Ratio: 1/18 (0.056) 700 | 701 | Strength: Medium 702 | 703 | Grind: 20 um (Extra-Fine) 704 | 705 | Temperature: 122.36 F 706 | 707 | Message: Nothing :) 708 | 709 | [Warning] The grind size is not within the recommended range. For `v60`, the grind size can be anywhere between `400 um` and `700 um` 710 | [Warning] The temperature is not within the recommended range. For `v60`, the temperature can be anywhere between `185 F` and `203 F` 711 | >>> input_params = {"method":"custom", "cups":2, "water":500, "coffee_ratio": 6, "water_ratio":1000, "message":"", "digits":3, "coffee_unit": "g", "water_unit": "g", "temperature": 94, "temperature_unit": "C", "grind": 700, "mode":"water-to-coffee"} 712 | >>> result_params = get_result(input_params) 713 | >>> check_ratio_limits(method=result_params["method"], ratio=result_params["ratio"]) 714 | True 715 | >>> check_grind_limits(method=result_params["method"], grind=result_params["grind"]["value"]) 716 | True 717 | >>> check_temperature_limits(method=result_params["method"], temperature=result_params["temperature"]["value"], temperature_unit=result_params["temperature"]["unit"]) 718 | True 719 | >>> input_params = {"method":"v60", "cups":2, "water":500, "coffee_ratio": 1.2, "water_ratio":18.4, "message":"", "digits":3, "coffee_unit": "g", "water_unit": "g", "grind": 20, "temperature":94, "temperature_unit": "C", "mode":"water-to-coffee"} 720 | >>> result_params = get_result(input_params) 721 | >>> check_ratio_limits(method=result_params["method"], ratio=result_params["ratio"]) 722 | True 723 | >>> check_grind_limits(method=result_params["method"], grind=result_params["grind"]["value"]) 724 | False 725 | >>> check_temperature_limits(method=result_params["method"], temperature=result_params["temperature"]["value"], temperature_unit=result_params["temperature"]["unit"]) 726 | True 727 | >>> input_params = {"method":"v60", "cups":2, "water":500, "coffee_ratio": 1.2, "water_ratio":50.1, "message":"", "digits":3, "coffee_unit": "g", "water_unit": "g", "grind": 20, "temperature":94, "temperature_unit": "C", "mode":"water-to-coffee"} 728 | >>> result_params = get_result(input_params) 729 | >>> check_ratio_limits(method=result_params["method"], ratio=result_params["ratio"]) 730 | False 731 | >>> check_grind_limits(method=result_params["method"], grind=result_params["grind"]["value"]) 732 | False 733 | >>> check_temperature_limits(method=result_params["method"], temperature=result_params["temperature"]["value"], temperature_unit=result_params["temperature"]["unit"]) 734 | True 735 | >>> chemex_params = load_method_params("chemex") 736 | >>> chemex_params == {'message': 'Chemex method', 'water': 240, 'coffee': 16, 'cups': 1, 'coffee_ratio': 1, 'water_ratio': 15, 'digits': 3, 'coffee_unit': 'g', 'water_unit': 'g', 'grind': 670, 'temperature':94, "temperature_unit": "C", "mode":"water-to-coffee"} 737 | True 738 | >>> show_methods_list() 739 | Methods list: 740 | 741 | 1. `aero-press` - AeroPress standard method 742 | 2. `aero-press-conc` - AeroPress concentrate method 743 | 3. `aero-press-inv` - AeroPress inverted method 744 | 4. `auto-drip` - Auto drip method 745 | 5. `chemex` - Chemex method 746 | 6. `clever-dripper` - Clever dripper method 747 | 7. `cold-brew` - Cold brew method 748 | 8. `cold-brew-conc` - Cold brew concentrate method 749 | 9. `cupping` - Cupping method 750 | 10. `custom` - Custom brewing method 751 | 11. `espresso` - Espresso method 752 | 12. `french-press` - French press method 753 | 13. `instant-coffee` - Instant coffee 754 | 14. `kalita-wave` - Kalita wave method 755 | 15. `lungo` - Lungo method 756 | 16. `moka-pot` - Moka pot method 757 | 17. `phin-filter` - Phin filter method 758 | 18. `pour-over` - Pour-over method 759 | 19. `ristretto` - Ristretto method 760 | 20. `siphon` - Siphon method 761 | 21. `steep-and-release` - Steep-and-release method 762 | 22. `turkish` - Turkish method 763 | 23. `v60` - V60 method 764 | >>> show_coffee_units_list() 765 | Coffee units list: 766 | 767 | 1. `cb` - coffee bean 768 | 2. `ct` - carat 769 | 3. `cup` - cup 770 | 4. `dsp` - dessertspoon 771 | 5. `dwt` - pennyweight 772 | 6. `g` - gram 773 | 7. `gr` - grain 774 | 8. `kg` - kilogram 775 | 9. `lb` - pound 776 | 10. `mg` - milligram 777 | 11. `oz` - ounce 778 | 12. `t lb` - troy pound 779 | 13. `t oz` - troy ounce 780 | 14. `tbsp` - tablespoon 781 | 15. `tsp` - teaspoon 782 | >>> show_water_units_list() 783 | Water units list: 784 | 785 | 1. `cc` - cubic centimeter 786 | 2. `cl` - centiliter 787 | 3. `ct` - carat 788 | 4. `cup` - cup 789 | 5. `dsp` - dessertspoon 790 | 6. `dwt` - pennyweight 791 | 7. `fl oz` - fluid ounce 792 | 8. `g` - gram 793 | 9. `gr` - grain 794 | 10. `kg` - kilogram 795 | 11. `l` - liter 796 | 12. `lb` - pound 797 | 13. `mg` - milligram 798 | 14. `ml` - milliliter 799 | 15. `oz` - ounce 800 | 16. `pt` - pint 801 | 17. `qt` - quart 802 | 18. `t lb` - troy pound 803 | 19. `t oz` - troy ounce 804 | 20. `tbsp` - tablespoon 805 | 21. `tsp` - teaspoon 806 | >>> show_temperature_units_list() 807 | Temperature units list: 808 | 809 | 1. `C` - Celsius 810 | 2. `F` - Fahrenheit 811 | 3. `K` - Kelvin 812 | >>> test_params = {"method":"v60", "cups":1, "water":335, "coffee_ratio": 3, "water_ratio":50, "message":"V60 method", 'coffee_unit': 'g', 'water_unit': 'g', "ratio": 3/50} 813 | >>> calculate_coffee(ratio=test_params["ratio"], water=test_params["water"], water_unit=test_params["water_unit"], coffee_unit=test_params["coffee_unit"]) 814 | 20.099999999999998 815 | >>> test_params = {"method":"v60", "cups":2, "water":335, "coffee_ratio": 3, "water_ratio":50, "message":"V60 method", 'coffee_unit': 'g', 'water_unit': 'g', "ratio": 3/50} 816 | >>> calculate_coffee(ratio=test_params["ratio"], water=test_params["water"], water_unit=test_params["water_unit"], coffee_unit=test_params["coffee_unit"]) 817 | 20.099999999999998 818 | >>> test_params = {"method":"v60", "ratio": 3/50, "cups":2, "coffee":{"total":40.2, "cup":20.1, "ratio":3.0, "unit":'g'}, "water":{"cup":335.0, "total":670, "ratio":50.0}, "message":"", "digits":3, "temperature":{"value":94.0, "unit": "C"}} 819 | >>> test_params = filter_params(test_params) 820 | >>> test_params["coffee"]["total"] 821 | 40.2 822 | >>> test_params["coffee"]["cup"] 823 | 20.1 824 | >>> test_params["water"]["ratio"] 825 | 50 826 | >>> test_params["coffee"]["ratio"] 827 | 3 828 | >>> test_params["water"]["cup"] 829 | 335 830 | >>> test_params["water"]["total"] 831 | 670 832 | >>> test_params["temperature"]["value"] 833 | 94 834 | >>> test_params["message"] 835 | 'Nothing :)' 836 | >>> test_params = {"method":"v60", "ratio": 3.12345/50.12345, "cups":2, "coffee":{"total": 41.76653202852158, "cup": 20.88326601426079, "ratio":3.12345, "unit":'g'}, "water":{"cup":335.12345, "total":670.2469, "ratio":50.12345},"message":"","digits":2,"temperature": {"value":94.2, "unit": "C"}} 837 | >>> test_params = filter_params(test_params) 838 | >>> test_params["coffee"]["total"] 839 | 41.77 840 | >>> test_params["coffee"]["cup"] 841 | 20.88 842 | >>> test_params["coffee"]["ratio"] 843 | 3.12 844 | >>> test_params["water"]["ratio"] 845 | 50.12 846 | >>> test_params["water"]["cup"] 847 | 335.12 848 | >>> test_params["water"]["total"] 849 | 670.25 850 | >>> test_params["temperature"]["value"] 851 | 94.2 852 | >>> is_int(12.1) 853 | False 854 | >>> is_int(12.123) 855 | False 856 | >>> is_int(12.0) 857 | True 858 | >>> is_int(15) 859 | True 860 | >>> validate_positive_int("2") 861 | 2 862 | >>> validate_positive_int("2.0") 863 | Traceback (most recent call last): 864 | ... 865 | argparse.ArgumentTypeError: invalid positive int value: '2.0' 866 | >>> validate_positive_int("a") 867 | Traceback (most recent call last): 868 | ... 869 | argparse.ArgumentTypeError: invalid positive int value: 'a' 870 | >>> validate_positive_int("-20") 871 | Traceback (most recent call last): 872 | ... 873 | argparse.ArgumentTypeError: invalid positive int value: '-20' 874 | >>> validate_positive_int("0") 875 | Traceback (most recent call last): 876 | ... 877 | argparse.ArgumentTypeError: invalid positive int value: '0' 878 | >>> validate_positive_float("2") 879 | 2.0 880 | >>> validate_positive_float("0") 881 | Traceback (most recent call last): 882 | ... 883 | argparse.ArgumentTypeError: invalid positive float value: '0' 884 | >>> validate_positive_float("-20") 885 | Traceback (most recent call last): 886 | ... 887 | argparse.ArgumentTypeError: invalid positive float value: '-20' 888 | >>> validate_positive_float("a") 889 | Traceback (most recent call last): 890 | ... 891 | argparse.ArgumentTypeError: invalid positive float value: 'a' 892 | >>> os.remove("save_test1.txt") 893 | >>> os.remove("save_test4.txt") 894 | >>> os.remove("save_test7.txt") 895 | >>> os.remove("save_test1.json") 896 | >>> os.remove("save_test4.json") 897 | >>> os.remove("save_test7.json") 898 | >>> os.remove("save_test1.yaml") 899 | >>> os.remove("save_test4.yaml") 900 | >>> os.remove("save_test7.yaml") 901 | """ 902 | -------------------------------------------------------------------------------- /test/cli_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | >>> import os 4 | >>> import json 5 | >>> import yaml 6 | >>> import argparse 7 | >>> from mycoffee.functions import * 8 | >>> from mycoffee.params import * 9 | >>> parser = argparse.ArgumentParser() 10 | >>> _ = parser.add_argument('--method', help='brewing method', type=str.lower, choices=sorted(METHODS_MAP), default="custom") 11 | >>> _ = parser.add_argument('--message', help='extra information about the brewing method', type=str) 12 | >>> _ = parser.add_argument('--coffee-ratio', help='coffee ratio', type=validate_positive_float) 13 | >>> _ = parser.add_argument('--water-ratio', help='water ratio', type=validate_positive_float) 14 | >>> _ = parser.add_argument('--water', help='water', type=validate_positive_float) 15 | >>> _ = parser.add_argument('--coffee', help='coffee', type=validate_positive_float) 16 | >>> _ = parser.add_argument('--cups', help='number of cups', type=validate_positive_int) 17 | >>> _ = parser.add_argument('--grind', help='grind size (um)', type=validate_positive_int) 18 | >>> _ = parser.add_argument('--temperature', help='brewing temperature', type=float) 19 | >>> _ = parser.add_argument('--digits', help='number of digits up to which the result is rounded', type=int, default=3) 20 | >>> _ = parser.add_argument('--coffee-unit', help='coffee unit', type=str.lower, choices=sorted(COFFEE_UNITS_MAP), default="g") 21 | >>> _ = parser.add_argument('--water-unit', help='water unit', type=str.lower, choices=sorted(WATER_UNITS_MAP), default="g") 22 | >>> _ = parser.add_argument('--temperature-unit', help='temperature unit', type=str.upper, choices=sorted(TEMPERATURE_UNITS_MAP), default="C") 23 | >>> _ = parser.add_argument('--coffee-units-list', help='coffee units list', nargs="?", const=1) 24 | >>> _ = parser.add_argument('--water-units-list', help='water units list', nargs="?", const=1) 25 | >>> _ = parser.add_argument('--temperature-units-list', help='temperature units list', nargs="?", const=1) 26 | >>> _ = parser.add_argument('--methods-list', help='brewing methods list', nargs="?", const=1) 27 | >>> _ = parser.add_argument('--version', help='version', nargs="?", const=1) 28 | >>> _ = parser.add_argument('--info', help='info', nargs="?", const=1) 29 | >>> _ = parser.add_argument('--ignore-warnings', help='ignore warnings', nargs="?", const=1) 30 | >>> _ = parser.add_argument('--mode', help='conversion mode', type=str.lower, choices=MODES_LIST, default="water-to-coffee") 31 | >>> _ = parser.add_argument('--save-path', help='file path to save', type=str) 32 | >>> _ = parser.add_argument('--save-format', help='file format', type=str.lower, choices=FILE_FORMATS_LIST, default="text") 33 | >>> args = parser.parse_args({"--version":True}) 34 | >>> run_program(args) 35 | 2.1 36 | >>> 37 | >>> args = parser.parse_args(["--method", 'v60']) 38 | >>> run_program(args) 39 | __ __ _ _ ___ _____ ____ ____ ____ ____ 40 | ( \/ )( \/ ) / __)( _ )( ___)( ___)( ___)( ___) 41 | ) ( \ / ( (__ )(_)( )__) )__) )__) )__) 42 | (_/\/\_) (__) \___)(_____)(__) (__) (____)(____) 43 | 44 | 45 | 46 | 47 | ... 48 | 49 | Mode: Water --> Coffee 50 | 51 | Method: `v60` 52 | 53 | Cups: 1 54 | 55 | Coffee: 56 | - Cup: 15 g 57 | - Total: 15 g 58 | 59 | Water: 60 | 61 | - Cup: 250 g 62 | - Total: 250 g 63 | 64 | Ratio: 3/50 (0.06) 65 | 66 | Strength: Medium 67 | 68 | Grind: 550 um (Medium-Fine) 69 | 70 | Temperature: 91 C 71 | 72 | Message: V60 method 73 | 74 | >>> args = parser.parse_args(["--method", 'v60', "--mode", 'coffee-to-water']) 75 | >>> run_program(args) 76 | __ __ _ _ ___ _____ ____ ____ ____ ____ 77 | ( \/ )( \/ ) / __)( _ )( ___)( ___)( ___)( ___) 78 | ) ( \ / ( (__ )(_)( )__) )__) )__) )__) 79 | (_/\/\_) (__) \___)(_____)(__) (__) (____)(____) 80 | 81 | 82 | 83 | 84 | ... 85 | 86 | Mode: Coffee --> Water 87 | 88 | Method: `v60` 89 | 90 | Cups: 1 91 | 92 | Coffee: 93 | - Cup: 15 g 94 | - Total: 15 g 95 | 96 | Water: 97 | 98 | - Cup: 250 g 99 | - Total: 250 g 100 | 101 | Ratio: 3/50 (0.06) 102 | 103 | Strength: Medium 104 | 105 | Grind: 550 um (Medium-Fine) 106 | 107 | Temperature: 91 C 108 | 109 | Message: V60 method 110 | 111 | >>> args = parser.parse_args(["--method", 'V60', '--grind', '50', '--save-path', "save_test2.txt"]) 112 | >>> run_program(args) 113 | __ __ _ _ ___ _____ ____ ____ ____ ____ 114 | ( \/ )( \/ ) / __)( _ )( ___)( ___)( ___)( ___) 115 | ) ( \ / ( (__ )(_)( )__) )__) )__) )__) 116 | (_/\/\_) (__) \___)(_____)(__) (__) (____)(____) 117 | 118 | 119 | 120 | 121 | ... 122 | 123 | Mode: Water --> Coffee 124 | 125 | Method: `v60` 126 | 127 | Cups: 1 128 | 129 | Coffee: 130 | - Cup: 15 g 131 | - Total: 15 g 132 | 133 | Water: 134 | 135 | - Cup: 250 g 136 | - Total: 250 g 137 | 138 | Ratio: 3/50 (0.06) 139 | 140 | Strength: Medium 141 | 142 | Grind: 50 um (Extra-Fine) 143 | 144 | Temperature: 91 C 145 | 146 | Message: V60 method 147 | 148 | [Warning] The grind size is not within the recommended range. For `v60`, the grind size can be anywhere between `400 um` and `700 um` 149 | [Info] File saved successfully! 150 | >>> file = open("save_test2.txt", "r") 151 | >>> print(file.read()) 152 | 153 | ... 154 | 155 | Mode: Water --> Coffee 156 | 157 | Method: `v60` 158 | 159 | Cups: 1 160 | 161 | Coffee: 162 | - Cup: 15 g 163 | - Total: 15 g 164 | 165 | Water: 166 | 167 | - Cup: 250 g 168 | - Total: 250 g 169 | 170 | Ratio: 3/50 (0.06) 171 | 172 | Strength: Medium 173 | 174 | Grind: 50 um (Extra-Fine) 175 | 176 | Temperature: 91 C 177 | 178 | Message: V60 method 179 | 180 | [Warning] The grind size is not within the recommended range. For `v60`, the grind size can be anywhere between `400 um` and `700 um` 181 | >>> args = parser.parse_args(["--method", 'V60', '--grind', '50', '--save-path', "save_test5.txt", '--mode', 'coffee-to-water']) 182 | >>> run_program(args) 183 | __ __ _ _ ___ _____ ____ ____ ____ ____ 184 | ( \/ )( \/ ) / __)( _ )( ___)( ___)( ___)( ___) 185 | ) ( \ / ( (__ )(_)( )__) )__) )__) )__) 186 | (_/\/\_) (__) \___)(_____)(__) (__) (____)(____) 187 | 188 | 189 | 190 | 191 | ... 192 | 193 | Mode: Coffee --> Water 194 | 195 | Method: `v60` 196 | 197 | Cups: 1 198 | 199 | Coffee: 200 | - Cup: 15 g 201 | - Total: 15 g 202 | 203 | Water: 204 | 205 | - Cup: 250 g 206 | - Total: 250 g 207 | 208 | Ratio: 3/50 (0.06) 209 | 210 | Strength: Medium 211 | 212 | Grind: 50 um (Extra-Fine) 213 | 214 | Temperature: 91 C 215 | 216 | Message: V60 method 217 | 218 | [Warning] The grind size is not within the recommended range. For `v60`, the grind size can be anywhere between `400 um` and `700 um` 219 | [Info] File saved successfully! 220 | >>> file = open("save_test5.txt", "r") 221 | >>> print(file.read()) 222 | 223 | ... 224 | 225 | Mode: Coffee --> Water 226 | 227 | Method: `v60` 228 | 229 | Cups: 1 230 | 231 | Coffee: 232 | - Cup: 15 g 233 | - Total: 15 g 234 | 235 | Water: 236 | 237 | - Cup: 250 g 238 | - Total: 250 g 239 | 240 | Ratio: 3/50 (0.06) 241 | 242 | Strength: Medium 243 | 244 | Grind: 50 um (Extra-Fine) 245 | 246 | Temperature: 91 C 247 | 248 | Message: V60 method 249 | 250 | [Warning] The grind size is not within the recommended range. For `v60`, the grind size can be anywhere between `400 um` and `700 um` 251 | >>> file.close() 252 | >>> args = parser.parse_args(["--method", 'v60', '--grind', '50', '--save-path', "save_test2.json", '--save-format', "JsOn"]) 253 | >>> run_program(args) 254 | __ __ _ _ ___ _____ ____ ____ ____ ____ 255 | ( \/ )( \/ ) / __)( _ )( ___)( ___)( ___)( ___) 256 | ) ( \ / ( (__ )(_)( )__) )__) )__) )__) 257 | (_/\/\_) (__) \___)(_____)(__) (__) (____)(____) 258 | 259 | 260 | 261 | 262 | ... 263 | 264 | Mode: Water --> Coffee 265 | 266 | Method: `v60` 267 | 268 | Cups: 1 269 | 270 | Coffee: 271 | - Cup: 15 g 272 | - Total: 15 g 273 | 274 | Water: 275 | 276 | - Cup: 250 g 277 | - Total: 250 g 278 | 279 | Ratio: 3/50 (0.06) 280 | 281 | Strength: Medium 282 | 283 | Grind: 50 um (Extra-Fine) 284 | 285 | Temperature: 91 C 286 | 287 | Message: V60 method 288 | 289 | [Warning] The grind size is not within the recommended range. For `v60`, the grind size can be anywhere between `400 um` and `700 um` 290 | [Info] File saved successfully! 291 | >>> file = open("save_test2.json", "r") 292 | >>> save_test2_object = json.load(file) 293 | >>> _ = format_date(save_test2_object["date"]) 294 | >>> del save_test2_object["date"] 295 | >>> save_test2_object == {'mycoffee_version': MY_COFFEE_VERSION, "mode":"water-to-coffee", 'temperature': {'value':91, 'unit':'C'}, 'method': 'v60', 'coffee': {'total':15, 'cup':15, 'unit':'g', 'ratio':3}, 'cups': 1,'digits': 3,'water': {'cup':250, 'total':250, 'unit':'g', 'ratio':50}, 'message': 'V60 method', 'grind': {'value':50, 'unit': 'um', 'type': get_grind_type(50)}, 'warnings': ['The grind size is not within the recommended range. For `v60`, the grind size can be anywhere between `400 um` and `700 um`'], 'ratio': 0.06, 'strength': get_brew_strength(0.06)} 296 | True 297 | >>> file.close() 298 | >>> args = parser.parse_args(["--method", 'v60', '--grind', '50', '--save-path', "save_test2.yaml", '--save-format', "YaMl"]) 299 | >>> run_program(args) 300 | __ __ _ _ ___ _____ ____ ____ ____ ____ 301 | ( \/ )( \/ ) / __)( _ )( ___)( ___)( ___)( ___) 302 | ) ( \ / ( (__ )(_)( )__) )__) )__) )__) 303 | (_/\/\_) (__) \___)(_____)(__) (__) (____)(____) 304 | 305 | 306 | 307 | 308 | ... 309 | 310 | Mode: Water --> Coffee 311 | 312 | Method: `v60` 313 | 314 | Cups: 1 315 | 316 | Coffee: 317 | - Cup: 15 g 318 | - Total: 15 g 319 | 320 | Water: 321 | 322 | - Cup: 250 g 323 | - Total: 250 g 324 | 325 | Ratio: 3/50 (0.06) 326 | 327 | Strength: Medium 328 | 329 | Grind: 50 um (Extra-Fine) 330 | 331 | Temperature: 91 C 332 | 333 | Message: V60 method 334 | 335 | [Warning] The grind size is not within the recommended range. For `v60`, the grind size can be anywhere between `400 um` and `700 um` 336 | [Info] File saved successfully! 337 | >>> file = open("save_test2.yaml", "r") 338 | >>> save_test2_object = yaml.safe_load(file) 339 | >>> _ = format_date(save_test2_object["date"]) 340 | >>> del save_test2_object["date"] 341 | >>> save_test2_object == {'mycoffee_version': MY_COFFEE_VERSION, "mode":"water-to-coffee", 'temperature': {'value':91, 'unit':'C'}, 'method': 'v60', 'coffee': {'total':15, 'cup':15, 'unit':'g', 'ratio':3}, 'cups': 1,'digits': 3,'water': {'cup':250, 'total':250, 'unit':'g', 'ratio':50}, 'message': 'V60 method', 'grind': {'value':50, 'unit': 'um', 'type': get_grind_type(50)}, 'warnings': ['The grind size is not within the recommended range. For `v60`, the grind size can be anywhere between `400 um` and `700 um`'], 'ratio': 0.06, 'strength': get_brew_strength(0.06)} 342 | True 343 | >>> file.close() 344 | >>> args = parser.parse_args(["--method", 'v60', '--grind', '50', '--save-path', "save_test5.json", '--save-format', "JsOn", "--mode", "coffee-to-water"]) 345 | >>> run_program(args) 346 | __ __ _ _ ___ _____ ____ ____ ____ ____ 347 | ( \/ )( \/ ) / __)( _ )( ___)( ___)( ___)( ___) 348 | ) ( \ / ( (__ )(_)( )__) )__) )__) )__) 349 | (_/\/\_) (__) \___)(_____)(__) (__) (____)(____) 350 | 351 | 352 | 353 | 354 | ... 355 | 356 | Mode: Coffee --> Water 357 | 358 | Method: `v60` 359 | 360 | Cups: 1 361 | 362 | Coffee: 363 | - Cup: 15 g 364 | - Total: 15 g 365 | 366 | Water: 367 | 368 | - Cup: 250 g 369 | - Total: 250 g 370 | 371 | Ratio: 3/50 (0.06) 372 | 373 | Strength: Medium 374 | 375 | Grind: 50 um (Extra-Fine) 376 | 377 | Temperature: 91 C 378 | 379 | Message: V60 method 380 | 381 | [Warning] The grind size is not within the recommended range. For `v60`, the grind size can be anywhere between `400 um` and `700 um` 382 | [Info] File saved successfully! 383 | >>> file = open("save_test5.json", "r") 384 | >>> save_test5_object = json.load(file) 385 | >>> _ = format_date(save_test5_object["date"]) 386 | >>> del save_test5_object["date"] 387 | >>> save_test5_object == {'mycoffee_version': MY_COFFEE_VERSION, "mode":"coffee-to-water", 'temperature': {'value':91, 'unit':'C'}, 'method': 'v60', 'coffee': {'total':15, 'cup':15, 'unit':'g', 'ratio':3}, 'cups': 1,'digits': 3,'water': {'cup':250, 'total':250, 'unit':'g', 'ratio':50}, 'message': 'V60 method', 'grind': {'value':50, 'unit': 'um', 'type': get_grind_type(50)}, 'warnings': ['The grind size is not within the recommended range. For `v60`, the grind size can be anywhere between `400 um` and `700 um`'], 'ratio': 0.06, 'strength': get_brew_strength(0.06)} 388 | True 389 | >>> file.close() 390 | >>> args = parser.parse_args(["--method", 'v60', '--grind', '50', '--save-path', "save_test5.yaml", '--save-format', "YAML", "--mode", "coffee-to-water"]) 391 | >>> run_program(args) 392 | __ __ _ _ ___ _____ ____ ____ ____ ____ 393 | ( \/ )( \/ ) / __)( _ )( ___)( ___)( ___)( ___) 394 | ) ( \ / ( (__ )(_)( )__) )__) )__) )__) 395 | (_/\/\_) (__) \___)(_____)(__) (__) (____)(____) 396 | 397 | 398 | 399 | 400 | ... 401 | 402 | Mode: Coffee --> Water 403 | 404 | Method: `v60` 405 | 406 | Cups: 1 407 | 408 | Coffee: 409 | - Cup: 15 g 410 | - Total: 15 g 411 | 412 | Water: 413 | 414 | - Cup: 250 g 415 | - Total: 250 g 416 | 417 | Ratio: 3/50 (0.06) 418 | 419 | Strength: Medium 420 | 421 | Grind: 50 um (Extra-Fine) 422 | 423 | Temperature: 91 C 424 | 425 | Message: V60 method 426 | 427 | [Warning] The grind size is not within the recommended range. For `v60`, the grind size can be anywhere between `400 um` and `700 um` 428 | [Info] File saved successfully! 429 | >>> file = open("save_test5.yaml", "r") 430 | >>> save_test5_object = yaml.safe_load(file) 431 | >>> _ = format_date(save_test5_object["date"]) 432 | >>> del save_test5_object["date"] 433 | >>> save_test5_object == {'mycoffee_version': MY_COFFEE_VERSION, "mode":"coffee-to-water", 'temperature': {'value':91, 'unit':'C'}, 'method': 'v60', 'coffee': {'total':15, 'cup':15, 'unit':'g', 'ratio':3}, 'cups': 1,'digits': 3,'water': {'cup':250, 'total':250, 'unit':'g', 'ratio':50}, 'message': 'V60 method', 'grind': {'value':50, 'unit': 'um', 'type': get_grind_type(50)}, 'warnings': ['The grind size is not within the recommended range. For `v60`, the grind size can be anywhere between `400 um` and `700 um`'], 'ratio': 0.06, 'strength': get_brew_strength(0.06)} 434 | True 435 | >>> file.close() 436 | >>> args = parser.parse_args(["--method", 'v60', '--grind', '50', '--ignore-warnings', '--save-path', "save_test3.txt"]) 437 | >>> run_program(args) 438 | __ __ _ _ ___ _____ ____ ____ ____ ____ 439 | ( \/ )( \/ ) / __)( _ )( ___)( ___)( ___)( ___) 440 | ) ( \ / ( (__ )(_)( )__) )__) )__) )__) 441 | (_/\/\_) (__) \___)(_____)(__) (__) (____)(____) 442 | 443 | 444 | 445 | 446 | ... 447 | 448 | Mode: Water --> Coffee 449 | 450 | Method: `v60` 451 | 452 | Cups: 1 453 | 454 | Coffee: 455 | - Cup: 15 g 456 | - Total: 15 g 457 | 458 | Water: 459 | 460 | - Cup: 250 g 461 | - Total: 250 g 462 | 463 | Ratio: 3/50 (0.06) 464 | 465 | Strength: Medium 466 | 467 | Grind: 50 um (Extra-Fine) 468 | 469 | Temperature: 91 C 470 | 471 | Message: V60 method 472 | 473 | [Info] File saved successfully! 474 | >>> file = open("save_test3.txt", "r") 475 | >>> print(file.read()) 476 | 477 | ... 478 | 479 | Mode: Water --> Coffee 480 | 481 | Method: `v60` 482 | 483 | Cups: 1 484 | 485 | Coffee: 486 | - Cup: 15 g 487 | - Total: 15 g 488 | 489 | Water: 490 | 491 | - Cup: 250 g 492 | - Total: 250 g 493 | 494 | Ratio: 3/50 (0.06) 495 | 496 | Strength: Medium 497 | 498 | Grind: 50 um (Extra-Fine) 499 | 500 | Temperature: 91 C 501 | 502 | Message: V60 method 503 | >>> file.close() 504 | >>> args = parser.parse_args(["--method", 'v60', '--grind', '50', '--ignore-warnings', '--save-path', "save_test6.txt", "--mode", "coffee-to-water", "--coffee", "30"]) 505 | >>> run_program(args) 506 | __ __ _ _ ___ _____ ____ ____ ____ ____ 507 | ( \/ )( \/ ) / __)( _ )( ___)( ___)( ___)( ___) 508 | ) ( \ / ( (__ )(_)( )__) )__) )__) )__) 509 | (_/\/\_) (__) \___)(_____)(__) (__) (____)(____) 510 | 511 | 512 | 513 | 514 | ... 515 | 516 | Mode: Coffee --> Water 517 | 518 | Method: `v60` 519 | 520 | Cups: 1 521 | 522 | Coffee: 523 | - Cup: 30 g 524 | - Total: 30 g 525 | 526 | Water: 527 | 528 | - Cup: 500 g 529 | - Total: 500 g 530 | 531 | Ratio: 3/50 (0.06) 532 | 533 | Strength: Medium 534 | 535 | Grind: 50 um (Extra-Fine) 536 | 537 | Temperature: 91 C 538 | 539 | Message: V60 method 540 | 541 | [Info] File saved successfully! 542 | >>> file = open("save_test6.txt", "r") 543 | >>> print(file.read()) 544 | 545 | ... 546 | 547 | Mode: Coffee --> Water 548 | 549 | Method: `v60` 550 | 551 | Cups: 1 552 | 553 | Coffee: 554 | - Cup: 30 g 555 | - Total: 30 g 556 | 557 | Water: 558 | 559 | - Cup: 500 g 560 | - Total: 500 g 561 | 562 | Ratio: 3/50 (0.06) 563 | 564 | Strength: Medium 565 | 566 | Grind: 50 um (Extra-Fine) 567 | 568 | Temperature: 91 C 569 | 570 | Message: V60 method 571 | >>> file.close() 572 | >>> args = parser.parse_args(["--method", 'v60', '--grind', '50', '--ignore-warnings', '--save-path', "save_test3.json", '--save-format', "json"]) 573 | >>> run_program(args) 574 | __ __ _ _ ___ _____ ____ ____ ____ ____ 575 | ( \/ )( \/ ) / __)( _ )( ___)( ___)( ___)( ___) 576 | ) ( \ / ( (__ )(_)( )__) )__) )__) )__) 577 | (_/\/\_) (__) \___)(_____)(__) (__) (____)(____) 578 | 579 | 580 | 581 | 582 | ... 583 | 584 | Mode: Water --> Coffee 585 | 586 | Method: `v60` 587 | 588 | Cups: 1 589 | 590 | Coffee: 591 | - Cup: 15 g 592 | - Total: 15 g 593 | 594 | Water: 595 | 596 | - Cup: 250 g 597 | - Total: 250 g 598 | 599 | Ratio: 3/50 (0.06) 600 | 601 | Strength: Medium 602 | 603 | Grind: 50 um (Extra-Fine) 604 | 605 | Temperature: 91 C 606 | 607 | Message: V60 method 608 | 609 | [Info] File saved successfully! 610 | >>> file = open("save_test3.json", "r") 611 | >>> save_test3_object = json.load(file) 612 | >>> _ = format_date(save_test3_object["date"]) 613 | >>> del save_test3_object["date"] 614 | >>> save_test3_object == {'mycoffee_version': MY_COFFEE_VERSION, "mode":"water-to-coffee", 'temperature': {'value':91, 'unit':'C'}, 'method': 'v60', 'coffee': {'total': 15, 'cup': 15, 'unit': 'g', 'ratio': 3}, 'cups': 1,'digits': 3, 'water': {'total':250, 'cup':250, 'unit':'g', 'ratio':50}, 'message': 'V60 method', 'grind': {'value':50, 'unit': 'um', 'type': get_grind_type(50)},"warnings":[], 'ratio': 0.06, 'strength': get_brew_strength(0.06)} 615 | True 616 | >>> file.close() 617 | >>> args = parser.parse_args(["--method", 'v60', '--grind', '50', '--ignore-warnings', '--save-path', "save_test3.yaml", '--save-format', "yaml"]) 618 | >>> run_program(args) 619 | __ __ _ _ ___ _____ ____ ____ ____ ____ 620 | ( \/ )( \/ ) / __)( _ )( ___)( ___)( ___)( ___) 621 | ) ( \ / ( (__ )(_)( )__) )__) )__) )__) 622 | (_/\/\_) (__) \___)(_____)(__) (__) (____)(____) 623 | 624 | 625 | 626 | 627 | ... 628 | 629 | Mode: Water --> Coffee 630 | 631 | Method: `v60` 632 | 633 | Cups: 1 634 | 635 | Coffee: 636 | - Cup: 15 g 637 | - Total: 15 g 638 | 639 | Water: 640 | 641 | - Cup: 250 g 642 | - Total: 250 g 643 | 644 | Ratio: 3/50 (0.06) 645 | 646 | Strength: Medium 647 | 648 | Grind: 50 um (Extra-Fine) 649 | 650 | Temperature: 91 C 651 | 652 | Message: V60 method 653 | 654 | [Info] File saved successfully! 655 | >>> file = open("save_test3.yaml", "r") 656 | >>> save_test3_object = yaml.safe_load(file) 657 | >>> _ = format_date(save_test3_object["date"]) 658 | >>> del save_test3_object["date"] 659 | >>> save_test3_object == {'mycoffee_version': MY_COFFEE_VERSION, "mode":"water-to-coffee", 'temperature': {'value':91, 'unit':'C'}, 'method': 'v60', 'coffee': {'total': 15, 'cup': 15, 'unit': 'g', 'ratio': 3}, 'cups': 1,'digits': 3, 'water': {'total':250, 'cup':250, 'unit':'g', 'ratio':50}, 'message': 'V60 method', 'grind': {'value':50, 'unit': 'um', 'type': get_grind_type(50)},"warnings":[], 'ratio': 0.06, 'strength': get_brew_strength(0.06)} 660 | True 661 | >>> file.close() 662 | >>> args = parser.parse_args(["--method", 'v60', '--grind', '50', '--ignore-warnings', '--save-path', "save_test6.json", '--save-format', "json", "--mode", "coffee-to-water", "--coffee", "30", "--cups", "2"]) 663 | >>> run_program(args) 664 | __ __ _ _ ___ _____ ____ ____ ____ ____ 665 | ( \/ )( \/ ) / __)( _ )( ___)( ___)( ___)( ___) 666 | ) ( \ / ( (__ )(_)( )__) )__) )__) )__) 667 | (_/\/\_) (__) \___)(_____)(__) (__) (____)(____) 668 | 669 | 670 | 671 | 672 | ... 673 | 674 | Mode: Coffee --> Water 675 | 676 | Method: `v60` 677 | 678 | Cups: 2 679 | 680 | Coffee: 681 | - Cup: 30 g 682 | - Total: 60 g 683 | 684 | Water: 685 | 686 | - Cup: 500 g 687 | - Total: 1000 g 688 | 689 | Ratio: 3/50 (0.06) 690 | 691 | Strength: Medium 692 | 693 | Grind: 50 um (Extra-Fine) 694 | 695 | Temperature: 91 C 696 | 697 | Message: V60 method 698 | 699 | [Info] File saved successfully! 700 | >>> file = open("save_test6.json", "r") 701 | >>> save_test6_object = json.load(file) 702 | >>> _ = format_date(save_test6_object["date"]) 703 | >>> del save_test6_object["date"] 704 | >>> save_test6_object == {'mycoffee_version': MY_COFFEE_VERSION, "mode":"coffee-to-water", 'temperature': {'value':91, 'unit':'C'}, 'method': 'v60', 'coffee': {'total': 60, 'cup': 30, 'unit': 'g', 'ratio': 3}, 'cups': 2,'digits': 3, 'water': {'total':1000, 'cup':500, 'unit':'g', 'ratio':50}, 'message': 'V60 method', 'grind': {'value':50, 'unit': 'um', 'type': get_grind_type(50)},"warnings":[], 'ratio': 0.06, 'strength': get_brew_strength(0.06)} 705 | True 706 | >>> file.close() 707 | >>> args = parser.parse_args(["--method", 'v60', '--grind', '50', '--ignore-warnings', '--save-path', "save_test6.yaml", '--save-format', "YamL", "--mode", "coffee-to-water", "--coffee", "30", "--cups", "2"]) 708 | >>> run_program(args) 709 | __ __ _ _ ___ _____ ____ ____ ____ ____ 710 | ( \/ )( \/ ) / __)( _ )( ___)( ___)( ___)( ___) 711 | ) ( \ / ( (__ )(_)( )__) )__) )__) )__) 712 | (_/\/\_) (__) \___)(_____)(__) (__) (____)(____) 713 | 714 | 715 | 716 | 717 | ... 718 | 719 | Mode: Coffee --> Water 720 | 721 | Method: `v60` 722 | 723 | Cups: 2 724 | 725 | Coffee: 726 | - Cup: 30 g 727 | - Total: 60 g 728 | 729 | Water: 730 | 731 | - Cup: 500 g 732 | - Total: 1000 g 733 | 734 | Ratio: 3/50 (0.06) 735 | 736 | Strength: Medium 737 | 738 | Grind: 50 um (Extra-Fine) 739 | 740 | Temperature: 91 C 741 | 742 | Message: V60 method 743 | 744 | [Info] File saved successfully! 745 | >>> file = open("save_test6.yaml", "r") 746 | >>> save_test6_object = yaml.safe_load(file) 747 | >>> _ = format_date(save_test6_object["date"]) 748 | >>> del save_test6_object["date"] 749 | >>> save_test6_object == {'mycoffee_version': MY_COFFEE_VERSION, "mode":"coffee-to-water", 'temperature': {'value':91, 'unit':'C'}, 'method': 'v60', 'coffee': {'total': 60, 'cup': 30, 'unit': 'g', 'ratio': 3}, 'cups': 2,'digits': 3, 'water': {'total':1000, 'cup':500, 'unit':'g', 'ratio':50}, 'message': 'V60 method', 'grind': {'value':50, 'unit': 'um', 'type': get_grind_type(50)},"warnings":[], 'ratio': 0.06, 'strength': get_brew_strength(0.06)} 750 | True 751 | >>> file.close() 752 | >>> args = parser.parse_args(["--method", 'v60', '--grind', '50', '--ignore-warnings', '--save-path', "f://", '--save-format', "json"]) 753 | >>> run_program(args) 754 | __ __ _ _ ___ _____ ____ ____ ____ ____ 755 | ( \/ )( \/ ) / __)( _ )( ___)( ___)( ___)( ___) 756 | ) ( \ / ( (__ )(_)( )__) )__) )__) )__) 757 | (_/\/\_) (__) \___)(_____)(__) (__) (____)(____) 758 | 759 | 760 | 761 | 762 | ... 763 | 764 | Mode: Water --> Coffee 765 | 766 | Method: `v60` 767 | 768 | Cups: 1 769 | 770 | Coffee: 771 | - Cup: 15 g 772 | - Total: 15 g 773 | 774 | Water: 775 | 776 | - Cup: 250 g 777 | - Total: 250 g 778 | 779 | Ratio: 3/50 (0.06) 780 | 781 | Strength: Medium 782 | 783 | Grind: 50 um (Extra-Fine) 784 | 785 | Temperature: 91 C 786 | 787 | Message: V60 method 788 | 789 | [Error] Failed to save file! 790 | >>> args = parser.parse_args(["--method", 'V60', '--grind', '50', '--save-path', "save_test8.txt", '--mode', 'ratio']) 791 | >>> run_program(args) 792 | __ __ _ _ ___ _____ ____ ____ ____ ____ 793 | ( \/ )( \/ ) / __)( _ )( ___)( ___)( ___)( ___) 794 | ) ( \ / ( (__ )(_)( )__) )__) )__) )__) 795 | (_/\/\_) (__) \___)(_____)(__) (__) (____)(____) 796 | 797 | 798 | 799 | 800 | ... 801 | 802 | Mode: Water & Coffee --> Ratio 803 | 804 | Method: `v60` 805 | 806 | Cups: 1 807 | 808 | Coffee: 809 | 810 | - Cup: 15 g 811 | - Total: 15 g 812 | 813 | Water: 814 | 815 | - Cup: 250 g 816 | - Total: 250 g 817 | 818 | Ratio: 3/50 (0.06) 819 | 820 | Strength: Medium 821 | 822 | Grind: 50 um (Extra-Fine) 823 | 824 | Temperature: 91 C 825 | 826 | Message: V60 method 827 | 828 | [Warning] The grind size is not within the recommended range. For `v60`, the grind size can be anywhere between `400 um` and `700 um` 829 | [Info] File saved successfully! 830 | 831 | >>> file = open("save_test8.txt", "r") 832 | >>> print(file.read()) 833 | 834 | ... 835 | 836 | Mode: Water & Coffee --> Ratio 837 | 838 | Method: `v60` 839 | 840 | Cups: 1 841 | 842 | Coffee: 843 | 844 | - Cup: 15 g 845 | - Total: 15 g 846 | 847 | Water: 848 | 849 | - Cup: 250 g 850 | - Total: 250 g 851 | 852 | Ratio: 3/50 (0.06) 853 | 854 | Strength: Medium 855 | 856 | Grind: 50 um (Extra-Fine) 857 | 858 | Temperature: 91 C 859 | 860 | Message: V60 method 861 | 862 | [Warning] The grind size is not within the recommended range. For `v60`, the grind size can be anywhere between `400 um` and `700 um` 863 | 864 | >>> file.close() 865 | >>> args = parser.parse_args(["--method", 'v60', "--water-ratio", '500', "--coffee-ratio", '23', "--water", '5000']) 866 | >>> params = load_params(args) 867 | >>> params = get_result(params, enable_filter=False) 868 | >>> params["water"]["cup"] 869 | 5000.0 870 | >>> params["water"]["total"] 871 | 5000.0 872 | >>> params["water"]["ratio"] 873 | 500.0 874 | >>> params["coffee"]["ratio"] 875 | 23.0 876 | >>> params["method"] 877 | 'v60' 878 | >>> params = filter_params(params) 879 | >>> params["water"]["ratio"] 880 | 500 881 | >>> params["coffee"]["ratio"] 882 | 23 883 | >>> params["water"]["total"] 884 | 5000 885 | >>> params["cups"] 886 | 1 887 | >>> args = parser.parse_args(["--method", 'v60', "--water-ratio", '500', "--coffee-ratio", '23', "--coffee", '230', "--mode", "coffee-to-water"]) 888 | >>> params = load_params(args) 889 | >>> params = get_result(params, enable_filter=False) 890 | >>> params["coffee"]["cup"] 891 | 230.0 892 | >>> params["coffee"]["total"] 893 | 230.0 894 | >>> params["water"]["cup"] 895 | 5000.0 896 | >>> params["water"]["total"] 897 | 5000.0 898 | >>> params["water"]["ratio"] 899 | 500.0 900 | >>> params["coffee"]["ratio"] 901 | 23.0 902 | >>> params["method"] 903 | 'v60' 904 | >>> params = filter_params(params) 905 | >>> params["water"]["ratio"] 906 | 500 907 | >>> params["coffee"]["ratio"] 908 | 23 909 | >>> params["water"]["total"] 910 | 5000 911 | >>> params["coffee"]["total"] 912 | 230 913 | >>> params["cups"] 914 | 1 915 | >>> args = parser.parse_args(["--method", 'v60', "--water", '500', "--coffee", '230', "--mode", "ratio", "--cup", "2"]) 916 | >>> params = load_params(args) 917 | >>> params = get_result(params, enable_filter=False) 918 | >>> params["coffee"]["cup"] 919 | 230.0 920 | >>> params["coffee"]["total"] 921 | 460.0 922 | >>> params["water"]["cup"] 923 | 500.0 924 | >>> params["water"]["total"] 925 | 1000.0 926 | >>> params["water"]["ratio"] 927 | 50 928 | >>> params["coffee"]["ratio"] 929 | 23 930 | >>> params["method"] 931 | 'v60' 932 | >>> params = filter_params(params) 933 | >>> params["water"]["ratio"] 934 | 50 935 | >>> params["coffee"]["ratio"] 936 | 23 937 | >>> params["water"]["total"] 938 | 1000 939 | >>> params["coffee"]["total"] 940 | 460 941 | >>> params["cups"] 942 | 2 943 | >>> args = parser.parse_args(["--method", 'v60', "--water-ratio", '500', "--coffee-ratio", '23', "--water", '5000000', "--water-unit", "mg"]) 944 | >>> params = load_params(args) 945 | >>> params["water"] 946 | 5000000.0 947 | >>> params["water_ratio"] 948 | 500.0 949 | >>> params["coffee_ratio"] 950 | 23.0 951 | >>> params["method"] 952 | 'v60' 953 | >>> args = parser.parse_args(["--method", 'steep-and-release', "--digits", '1', "--water-unit", "mg"]) 954 | >>> params = load_params(args) 955 | >>> ratio = params["coffee_ratio"] / params["water_ratio"] 956 | >>> params["coffee"] = calculate_coffee(ratio=ratio, water=params["water"], water_unit=params["water_unit"], coffee_unit=params["coffee_unit"]) 957 | >>> params["water"] 958 | 255000 959 | >>> params["coffee"] 960 | 15.9375 961 | >>> params["water_ratio"] 962 | 16 963 | >>> params["coffee_ratio"] 964 | 1 965 | >>> params["method"] 966 | 'steep-and-release' 967 | >>> args = parser.parse_args(["--method", 'steep-and-release', "--digits", '1']) 968 | >>> params = load_params(args) 969 | >>> params = get_result(params, enable_filter=False) 970 | >>> params["water"]["cup"] 971 | 255 972 | >>> params["water"]["total"] 973 | 255 974 | >>> params["coffee"]["total"] 975 | 15.9375 976 | >>> params["coffee"]["cup"] 977 | 15.9375 978 | >>> params["water"]["ratio"] 979 | 16 980 | >>> params["coffee"]["ratio"] 981 | 1 982 | >>> params["method"] 983 | 'steep-and-release' 984 | >>> params = filter_params(params) 985 | >>> params["water"]["ratio"] 986 | 16 987 | >>> params["coffee"]["ratio"] 988 | 1 989 | >>> params["water"]["cup"] 990 | 255 991 | >>> params["water"]["total"] 992 | 255 993 | >>> params["coffee"]["total"] 994 | 15.9 995 | >>> params["coffee"]["cup"] 996 | 15.9 997 | >>> params["digits"] 998 | 1 999 | >>> params["cups"] 1000 | 1 1001 | >>> args = parser.parse_args(["--method", 'steep-and-release', "--digits", '1', "--cups", '3', "--temperature", '92', "--temperature-unit", 'F']) 1002 | >>> params = load_params(args) 1003 | >>> params = get_result(params, enable_filter=False) 1004 | >>> params["water"]["total"] 1005 | 765 1006 | >>> params["water"]["cup"] 1007 | 255 1008 | >>> params["coffee"]["total"] 1009 | 47.8125 1010 | >>> params["water"]["ratio"] 1011 | 16 1012 | >>> params["coffee"]["ratio"] 1013 | 1 1014 | >>> params["method"] 1015 | 'steep-and-release' 1016 | >>> params = filter_params(params) 1017 | >>> params["water"]["ratio"] 1018 | 16 1019 | >>> params["coffee"]["ratio"] 1020 | 1 1021 | >>> params["water"]["cup"] 1022 | 255 1023 | >>> params["water"]["total"] 1024 | 765 1025 | >>> params["coffee"]["total"] 1026 | 47.8 1027 | >>> params["digits"] 1028 | 1 1029 | >>> params["cups"] 1030 | 3 1031 | >>> params["temperature"]["value"] 1032 | 92 1033 | >>> params["temperature"]["unit"] 1034 | 'F' 1035 | >>> args = parser.parse_args(["--method", 'steep-and-release', "--digits", '1', "--cups", '3', "--temperature-unit", 'F']) 1036 | >>> params = load_params(args) 1037 | >>> params["temperature"] 1038 | 199.4 1039 | >>> params["temperature_unit"] 1040 | 'F' 1041 | >>> args = parser.parse_args(["--method", 'steep-and-release', "--digits", '1', "--cups", '3', "--coffee-unit", "oz"]) 1042 | >>> params = load_params(args) 1043 | >>> params = get_result(params, enable_filter=False) 1044 | >>> params["water"]["cup"] 1045 | 255 1046 | >>> params["water"]["total"] 1047 | 765 1048 | >>> params["coffee"]["total"] 1049 | 1.686536305734375 1050 | >>> params["water"]["ratio"] 1051 | 16 1052 | >>> params["coffee"]["ratio"] 1053 | 1 1054 | >>> params["method"] 1055 | 'steep-and-release' 1056 | >>> params = filter_params(params) 1057 | >>> params["water"]["ratio"] 1058 | 16 1059 | >>> params["coffee"]["ratio"] 1060 | 1 1061 | >>> params["water"]["cup"] 1062 | 255 1063 | >>> params["water"]["total"] 1064 | 765 1065 | >>> params["coffee"]["total"] 1066 | 1.7 1067 | >>> params["digits"] 1068 | 1 1069 | >>> params["cups"] 1070 | 3 1071 | >>> args = parser.parse_args(["--methods-list"]) 1072 | >>> run_program(args) 1073 | Methods list: 1074 | 1075 | 1. `aero-press` - AeroPress standard method 1076 | 2. `aero-press-conc` - AeroPress concentrate method 1077 | 3. `aero-press-inv` - AeroPress inverted method 1078 | 4. `auto-drip` - Auto drip method 1079 | 5. `chemex` - Chemex method 1080 | 6. `clever-dripper` - Clever dripper method 1081 | 7. `cold-brew` - Cold brew method 1082 | 8. `cold-brew-conc` - Cold brew concentrate method 1083 | 9. `cupping` - Cupping method 1084 | 10. `custom` - Custom brewing method 1085 | 11. `espresso` - Espresso method 1086 | 12. `french-press` - French press method 1087 | 13. `instant-coffee` - Instant coffee 1088 | 14. `kalita-wave` - Kalita wave method 1089 | 15. `lungo` - Lungo method 1090 | 16. `moka-pot` - Moka pot method 1091 | 17. `phin-filter` - Phin filter method 1092 | 18. `pour-over` - Pour-over method 1093 | 19. `ristretto` - Ristretto method 1094 | 20. `siphon` - Siphon method 1095 | 21. `steep-and-release` - Steep-and-release method 1096 | 22. `turkish` - Turkish method 1097 | 23. `v60` - V60 method 1098 | >>> args = parser.parse_args(["--coffee-units-list"]) 1099 | >>> run_program(args) 1100 | Coffee units list: 1101 | 1102 | 1. `cb` - coffee bean 1103 | 2. `ct` - carat 1104 | 3. `cup` - cup 1105 | 4. `dsp` - dessertspoon 1106 | 5. `dwt` - pennyweight 1107 | 6. `g` - gram 1108 | 7. `gr` - grain 1109 | 8. `kg` - kilogram 1110 | 9. `lb` - pound 1111 | 10. `mg` - milligram 1112 | 11. `oz` - ounce 1113 | 12. `t lb` - troy pound 1114 | 13. `t oz` - troy ounce 1115 | 14. `tbsp` - tablespoon 1116 | 15. `tsp` - teaspoon 1117 | >>> args = parser.parse_args(["--water-units-list"]) 1118 | >>> run_program(args) 1119 | Water units list: 1120 | 1121 | 1. `cc` - cubic centimeter 1122 | 2. `cl` - centiliter 1123 | 3. `ct` - carat 1124 | 4. `cup` - cup 1125 | 5. `dsp` - dessertspoon 1126 | 6. `dwt` - pennyweight 1127 | 7. `fl oz` - fluid ounce 1128 | 8. `g` - gram 1129 | 9. `gr` - grain 1130 | 10. `kg` - kilogram 1131 | 11. `l` - liter 1132 | 12. `lb` - pound 1133 | 13. `mg` - milligram 1134 | 14. `ml` - milliliter 1135 | 15. `oz` - ounce 1136 | 16. `pt` - pint 1137 | 17. `qt` - quart 1138 | 18. `t lb` - troy pound 1139 | 19. `t oz` - troy ounce 1140 | 20. `tbsp` - tablespoon 1141 | 21. `tsp` - teaspoon 1142 | >>> args = parser.parse_args(["--temperature-units-list"]) 1143 | >>> run_program(args) 1144 | Temperature units list: 1145 | 1146 | 1. `C` - Celsius 1147 | 2. `F` - Fahrenheit 1148 | 3. `K` - Kelvin 1149 | >>> os.remove("save_test2.txt") 1150 | >>> os.remove("save_test3.txt") 1151 | >>> os.remove("save_test5.txt") 1152 | >>> os.remove("save_test6.txt") 1153 | >>> os.remove("save_test8.txt") 1154 | >>> os.remove("save_test2.json") 1155 | >>> os.remove("save_test3.json") 1156 | >>> os.remove("save_test5.json") 1157 | >>> os.remove("save_test6.json") 1158 | >>> os.remove("save_test2.yaml") 1159 | >>> os.remove("save_test3.yaml") 1160 | >>> os.remove("save_test5.yaml") 1161 | >>> os.remove("save_test6.yaml") 1162 | """ 1163 | -------------------------------------------------------------------------------- /test/verified_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | >>> from fractions import Fraction 4 | >>> from mycoffee.functions import * 5 | >>> from mycoffee.params import METHODS_MAP 6 | >>> v60_params = load_method_params("v60") # https://www.origincoffee.co.uk/blogs/journal/brewing-at-home-v60 7 | >>> v60_params["coffee_ratio"] == 3 8 | True 9 | >>> v60_params["water_ratio"] == 50 10 | True 11 | >>> v60_params["water"] == 250 12 | True 13 | >>> v60_params["ratio"] = v60_params["coffee_ratio"] / v60_params["water_ratio"] 14 | >>> v60_coffee = calculate_coffee(ratio=v60_params["ratio"], water=v60_params["water"], water_unit=v60_params["water_unit"], coffee_unit=v60_params["coffee_unit"]) 15 | >>> v60_coffee == 15 16 | True 17 | >>> v60_coffee == METHODS_MAP["v60"]["coffee"] 18 | True 19 | >>> v60_water = round(calculate_water(ratio=v60_params["ratio"], coffee=v60_coffee, water_unit=v60_params["water_unit"], coffee_unit=v60_params["coffee_unit"]), 3) 20 | >>> v60_water == v60_params["water"] 21 | True 22 | >>> METHODS_MAP["v60"]["ratio_upper_limit"] == Fraction(1, 14) 23 | True 24 | >>> METHODS_MAP["v60"]["ratio_lower_limit"] == Fraction(1, 18) 25 | True 26 | >>> METHODS_MAP["v60"]["grind_upper_limit"] == 700 27 | True 28 | >>> METHODS_MAP["v60"]["grind_lower_limit"] == 400 29 | True 30 | >>> v60_params["grind"] == 550 # https://honestcoffeeguide.com/coffee-grind-size-chart/#v60 31 | True 32 | >>> METHODS_MAP["v60"]["temperature_upper_limit"] == 95 # https://honestcoffeeguide.com/best-temperature-to-brew-coffee/ 33 | True 34 | >>> METHODS_MAP["v60"]["temperature_lower_limit"] == 85 # https://honestcoffeeguide.com/best-temperature-to-brew-coffee/ 35 | True 36 | >>> v60_params["temperature"] == 91 # https://honestcoffeeguide.com/best-temperature-to-brew-coffee/ 37 | True 38 | >>> chemex_params = load_method_params("chemex") # https://honestcoffeeguide.com/coffee-to-water-ratio-calculator 39 | >>> chemex_params["coffee_ratio"] == 1 40 | True 41 | >>> chemex_params["water_ratio"] == 15 42 | True 43 | >>> chemex_params["water"] == 240 44 | True 45 | >>> chemex_params["ratio"] = chemex_params["coffee_ratio"] / chemex_params["water_ratio"] 46 | >>> chemex_coffee = calculate_coffee(ratio=chemex_params["ratio"], water=chemex_params["water"], water_unit=chemex_params["water_unit"], coffee_unit=chemex_params["coffee_unit"]) 47 | >>> chemex_coffee == 16 48 | True 49 | >>> chemex_water = round(calculate_water(ratio=chemex_params["ratio"], coffee=chemex_coffee, water_unit=chemex_params["water_unit"], coffee_unit=chemex_params["coffee_unit"]), 3) 50 | >>> chemex_water == chemex_params["water"] 51 | True 52 | >>> chemex_coffee == METHODS_MAP["chemex"]["coffee"] 53 | True 54 | >>> METHODS_MAP["chemex"]["ratio_upper_limit"] == Fraction(1, 10) 55 | True 56 | >>> METHODS_MAP["chemex"]["ratio_lower_limit"] == Fraction(1, 21) 57 | True 58 | >>> METHODS_MAP["chemex"]["grind_upper_limit"] == 930 59 | True 60 | >>> METHODS_MAP["chemex"]["grind_lower_limit"] == 410 61 | True 62 | >>> chemex_params["grind"] == 670 # https://honestcoffeeguide.com/coffee-grind-size-chart/#pourover 63 | True 64 | >>> METHODS_MAP["chemex"]["temperature_upper_limit"] == 95 # https://honestcoffeeguide.com/best-temperature-to-brew-coffee/ 65 | True 66 | >>> METHODS_MAP["chemex"]["temperature_lower_limit"] == 85 # https://honestcoffeeguide.com/best-temperature-to-brew-coffee/ 67 | True 68 | >>> chemex_params["temperature"] == 94 # https://honestcoffeeguide.com/best-temperature-to-brew-coffee/ 69 | True 70 | >>> espresso_params = load_method_params("espresso") # https://honestcoffeeguide.com/coffee-to-water-ratio-calculator 71 | >>> espresso_params["coffee_ratio"] == 1 72 | True 73 | >>> espresso_params["water_ratio"] == 2 74 | True 75 | >>> espresso_params["water"] == 36 76 | True 77 | >>> espresso_params["ratio"] = espresso_params["coffee_ratio"] / espresso_params["water_ratio"] 78 | >>> espresso_coffee = calculate_coffee(ratio=espresso_params["ratio"], water=espresso_params["water"], water_unit=espresso_params["water_unit"], coffee_unit=espresso_params["coffee_unit"]) 79 | >>> espresso_coffee == 18 80 | True 81 | >>> espresso_water = round(calculate_water(ratio=espresso_params["ratio"], coffee=espresso_coffee, water_unit=espresso_params["water_unit"], coffee_unit=espresso_params["coffee_unit"]), 3) 82 | >>> espresso_water == espresso_params["water"] 83 | True 84 | >>> espresso_coffee == METHODS_MAP["espresso"]["coffee"] 85 | True 86 | >>> METHODS_MAP["espresso"]["ratio_upper_limit"] == Fraction(2, 3) 87 | True 88 | >>> METHODS_MAP["espresso"]["ratio_lower_limit"] == Fraction(2, 5) 89 | True 90 | >>> METHODS_MAP["espresso"]["grind_upper_limit"] == 380 91 | True 92 | >>> METHODS_MAP["espresso"]["grind_lower_limit"] == 180 93 | True 94 | >>> espresso_params["grind"] == 280 # https://honestcoffeeguide.com/coffee-grind-size-chart/#espresso 95 | True 96 | >>> METHODS_MAP["espresso"]["temperature_upper_limit"] == 95 # https://honestcoffeeguide.com/best-temperature-to-brew-coffee/ 97 | True 98 | >>> METHODS_MAP["espresso"]["temperature_lower_limit"] == 85 # https://honestcoffeeguide.com/best-temperature-to-brew-coffee/ 99 | True 100 | >>> espresso_params["temperature"] == 92 # https://honestcoffeeguide.com/best-temperature-to-brew-coffee/ 101 | True 102 | >>> siphon_params = load_method_params("siphon") # https://bluebottlecoffee.com/us/eng/brew-guides/siphon 103 | >>> siphon_params["coffee_ratio"] == 1 104 | True 105 | >>> siphon_params["water_ratio"] == 15 106 | True 107 | >>> siphon_params["water"] == 240 108 | True 109 | >>> siphon_params["ratio"] = siphon_params["coffee_ratio"] / siphon_params["water_ratio"] 110 | >>> siphon_coffee = calculate_coffee(ratio=siphon_params["ratio"], water=siphon_params["water"], water_unit=siphon_params["water_unit"], coffee_unit=siphon_params["coffee_unit"]) 111 | >>> siphon_coffee == 16 112 | True 113 | >>> siphon_water = round(calculate_water(ratio=siphon_params["ratio"], coffee=siphon_coffee, water_unit=siphon_params["water_unit"], coffee_unit=siphon_params["coffee_unit"]), 3) 114 | >>> siphon_water == siphon_params["water"] 115 | True 116 | >>> siphon_coffee == METHODS_MAP["siphon"]["coffee"] 117 | True 118 | >>> METHODS_MAP["siphon"]["ratio_upper_limit"] == Fraction(1, 12) 119 | True 120 | >>> METHODS_MAP["siphon"]["ratio_lower_limit"] == Fraction(1, 16) 121 | True 122 | >>> METHODS_MAP["siphon"]["grind_upper_limit"] == 800 123 | True 124 | >>> METHODS_MAP["siphon"]["grind_lower_limit"] == 375 125 | True 126 | >>> siphon_params["grind"] == 588 # https://honestcoffeeguide.com/coffee-grind-size-chart/#siphon 127 | True 128 | >>> METHODS_MAP["siphon"]["temperature_upper_limit"] == 94 # https://unionroasted.com/blogs/brewing-guides/syphon 129 | True 130 | >>> METHODS_MAP["siphon"]["temperature_lower_limit"] == 91 # https://unionroasted.com/blogs/brewing-guides/syphon 131 | True 132 | >>> siphon_params["temperature"] == 93 # https://unionroasted.com/blogs/brewing-guides/syphon 133 | True 134 | >>> french_press_params = load_method_params("french-press") # https://useandcares.hamiltonbeach.com/files/840230401.pdf 135 | >>> french_press_params["coffee_ratio"] == 1 136 | True 137 | >>> french_press_params["water_ratio"] == 15 138 | True 139 | >>> french_press_params["water"] == 120 140 | True 141 | >>> french_press_params["ratio"] = french_press_params["coffee_ratio"] / french_press_params["water_ratio"] 142 | >>> french_press_coffee = calculate_coffee(ratio=french_press_params["ratio"], water=french_press_params["water"], water_unit=french_press_params["water_unit"], coffee_unit=french_press_params["coffee_unit"]) 143 | >>> french_press_coffee == 8 144 | True 145 | >>> french_press_water = round(calculate_water(ratio=french_press_params["ratio"], coffee=french_press_coffee, water_unit=french_press_params["water_unit"], coffee_unit=french_press_params["coffee_unit"]), 3) 146 | >>> french_press_water == french_press_params["water"] 147 | True 148 | >>> french_press_coffee == METHODS_MAP["french-press"]["coffee"] 149 | True 150 | >>> METHODS_MAP["french-press"]["ratio_upper_limit"] == Fraction(1, 12) 151 | True 152 | >>> METHODS_MAP["french-press"]["ratio_lower_limit"] == Fraction(1, 18) 153 | True 154 | >>> METHODS_MAP["french-press"]["grind_upper_limit"] == 1300 155 | True 156 | >>> METHODS_MAP["french-press"]["grind_lower_limit"] == 690 157 | True 158 | >>> french_press_params["grind"] == 995 # https://honestcoffeeguide.com/coffee-grind-size-chart/#french-press 159 | True 160 | >>> METHODS_MAP["french-press"]["temperature_upper_limit"] == 95 # https://honestcoffeeguide.com/best-temperature-to-brew-coffee/ 161 | True 162 | >>> METHODS_MAP["french-press"]["temperature_lower_limit"] == 85 # https://honestcoffeeguide.com/best-temperature-to-brew-coffee/ 163 | True 164 | >>> french_press_params["temperature"] == 90 # https://honestcoffeeguide.com/best-temperature-to-brew-coffee/ 165 | True 166 | >>> pour_over_params = load_method_params("pour-over") # https://www.nicolebattefeld.com/post/best-recipes-2022 167 | >>> pour_over_params["coffee_ratio"] == 1 168 | True 169 | >>> pour_over_params["water_ratio"] == 15 170 | True 171 | >>> pour_over_params["water"] == 240 172 | True 173 | >>> pour_over_params["ratio"] = pour_over_params["coffee_ratio"] / pour_over_params["water_ratio"] 174 | >>> pour_over_coffee = calculate_coffee(ratio=pour_over_params["ratio"], water=pour_over_params["water"], water_unit=pour_over_params["water_unit"], coffee_unit=pour_over_params["coffee_unit"]) 175 | >>> pour_over_coffee == 16 176 | True 177 | >>> pour_over_water = round(calculate_water(ratio=pour_over_params["ratio"], coffee=pour_over_coffee, water_unit=pour_over_params["water_unit"], coffee_unit=pour_over_params["coffee_unit"]), 3) 178 | >>> pour_over_water == pour_over_params["water"] 179 | True 180 | >>> pour_over_coffee == METHODS_MAP["pour-over"]["coffee"] 181 | True 182 | >>> METHODS_MAP["pour-over"]["ratio_upper_limit"] == Fraction(1, 14) 183 | True 184 | >>> METHODS_MAP["pour-over"]["ratio_lower_limit"] == Fraction(1, 16) 185 | True 186 | >>> METHODS_MAP["pour-over"]["grind_upper_limit"] == 930 187 | True 188 | >>> METHODS_MAP["pour-over"]["grind_lower_limit"] == 410 189 | True 190 | >>> pour_over_params["grind"] == 670 # https://honestcoffeeguide.com/coffee-grind-size-chart/#pourover 191 | True 192 | >>> METHODS_MAP["pour-over"]["temperature_upper_limit"] == 93 # https://honestcoffeeguide.com/best-temperature-to-brew-coffee/ 193 | True 194 | >>> METHODS_MAP["pour-over"]["temperature_lower_limit"] == 90 # https://honestcoffeeguide.com/best-temperature-to-brew-coffee/ 195 | True 196 | >>> pour_over_params["temperature"] == 92 # https://honestcoffeeguide.com/best-temperature-to-brew-coffee/ 197 | True 198 | >>> auto_drip_params = load_method_params("auto-drip") # https://wonderstate.com/pages/auto-drip 199 | >>> auto_drip_params["coffee_ratio"] == 1 200 | True 201 | >>> auto_drip_params["water_ratio"] == 16 202 | True 203 | >>> auto_drip_params["water"] == 128 204 | True 205 | >>> auto_drip_params["ratio"] = auto_drip_params["coffee_ratio"] / auto_drip_params["water_ratio"] 206 | >>> auto_drip_coffee = calculate_coffee(ratio=auto_drip_params["ratio"], water=auto_drip_params["water"], water_unit=auto_drip_params["water_unit"], coffee_unit=auto_drip_params["coffee_unit"]) 207 | >>> auto_drip_coffee == 8 208 | True 209 | >>> auto_drip_water = round(calculate_water(ratio=auto_drip_params["ratio"], coffee=auto_drip_coffee, water_unit=auto_drip_params["water_unit"], coffee_unit=auto_drip_params["coffee_unit"]), 3) 210 | >>> auto_drip_water == auto_drip_params["water"] 211 | True 212 | >>> auto_drip_coffee == METHODS_MAP["auto-drip"]["coffee"] 213 | True 214 | >>> METHODS_MAP["auto-drip"]["ratio_upper_limit"] == Fraction(1, 14) 215 | True 216 | >>> METHODS_MAP["auto-drip"]["ratio_lower_limit"] == Fraction(1, 17) 217 | True 218 | >>> METHODS_MAP["auto-drip"]["grind_upper_limit"] == 900 219 | True 220 | >>> METHODS_MAP["auto-drip"]["grind_lower_limit"] == 300 221 | True 222 | >>> auto_drip_params["grind"] == 600 # https://honestcoffeeguide.com/coffee-grind-size-chart/#filter-coffee-machine 223 | True 224 | >>> METHODS_MAP["auto-drip"]["temperature_upper_limit"] == 96 # https://counterculturecoffee.com/blogs/counter-culture-coffee/guide-to-home-coffee-makers 225 | True 226 | >>> METHODS_MAP["auto-drip"]["temperature_lower_limit"] == 90 # https://counterculturecoffee.com/blogs/counter-culture-coffee/guide-to-home-coffee-makers 227 | True 228 | >>> auto_drip_params["temperature"] == 93 # https://counterculturecoffee.com/blogs/counter-culture-coffee/guide-to-home-coffee-makers 229 | True 230 | >>> cold_brew_params = load_method_params("cold-brew") # https://counterculturecoffee.com/blogs/counter-culture-coffee/guide-to-cold-brew 231 | >>> cold_brew_params["coffee_ratio"] == 1 232 | True 233 | >>> cold_brew_params["water_ratio"] == 11 234 | True 235 | >>> cold_brew_params["water"] == 242 236 | True 237 | >>> cold_brew_params["ratio"] = cold_brew_params["coffee_ratio"] / cold_brew_params["water_ratio"] 238 | >>> cold_brew_coffee = calculate_coffee(ratio=cold_brew_params["ratio"], water=cold_brew_params["water"], water_unit=cold_brew_params["water_unit"], coffee_unit=cold_brew_params["coffee_unit"]) 239 | >>> cold_brew_coffee == 22 240 | True 241 | >>> cold_brew_water = round(calculate_water(ratio=cold_brew_params["ratio"], coffee=cold_brew_coffee, water_unit=cold_brew_params["water_unit"], coffee_unit=cold_brew_params["coffee_unit"]), 3) 242 | >>> cold_brew_water == cold_brew_params["water"] 243 | True 244 | >>> cold_brew_coffee == METHODS_MAP["cold-brew"]["coffee"] 245 | True 246 | >>> METHODS_MAP["cold-brew"]["ratio_upper_limit"] == Fraction(1, 8) 247 | True 248 | >>> METHODS_MAP["cold-brew"]["ratio_lower_limit"] == Fraction(1, 15) 249 | True 250 | >>> METHODS_MAP["cold-brew"]["grind_upper_limit"] == 1400 251 | True 252 | >>> METHODS_MAP["cold-brew"]["grind_lower_limit"] == 800 253 | True 254 | >>> cold_brew_params["grind"] == 1100 # https://honestcoffeeguide.com/coffee-grind-size-chart/#cold-brew 255 | True 256 | >>> METHODS_MAP["cold-brew"]["temperature_upper_limit"] == 40 # https://perfectdailygrind.com/2021/07/can-you-brew-coffee-with-warm-water 257 | True 258 | >>> METHODS_MAP["cold-brew"]["temperature_lower_limit"] == 0 # https://perfectdailygrind.com/2021/07/can-you-brew-coffee-with-warm-water 259 | True 260 | >>> cold_brew_params["temperature"] == 20 # https://perfectdailygrind.com/2021/07/can-you-brew-coffee-with-warm-water 261 | True 262 | >>> cold_brew_conc_params = load_method_params("cold-brew-conc") # https://www.thespruceeats.com/cold-brew-concentrate-recipe-5197494 263 | >>> cold_brew_conc_params["coffee_ratio"] == 1 264 | True 265 | >>> cold_brew_conc_params["water_ratio"] == 5 266 | True 267 | >>> cold_brew_conc_params["water"] == 120 268 | True 269 | >>> cold_brew_conc_params["ratio"] = cold_brew_conc_params["coffee_ratio"] / cold_brew_conc_params["water_ratio"] 270 | >>> cold_brew_conc_coffee = calculate_coffee(ratio=cold_brew_conc_params["ratio"], water=cold_brew_conc_params["water"], water_unit=cold_brew_conc_params["water_unit"], coffee_unit=cold_brew_conc_params["coffee_unit"]) 271 | >>> cold_brew_conc_coffee == 24 272 | True 273 | >>> cold_brew_conc_water = round(calculate_water(ratio=cold_brew_conc_params["ratio"], coffee=cold_brew_conc_coffee, water_unit=cold_brew_conc_params["water_unit"], coffee_unit=cold_brew_conc_params["coffee_unit"]), 3) 274 | >>> cold_brew_conc_water == cold_brew_conc_params["water"] 275 | True 276 | >>> cold_brew_conc_coffee == METHODS_MAP["cold-brew-conc"]["coffee"] 277 | True 278 | >>> METHODS_MAP["cold-brew-conc"]["ratio_upper_limit"] == Fraction(1, 4) 279 | True 280 | >>> METHODS_MAP["cold-brew-conc"]["ratio_lower_limit"] == Fraction(1, 6) 281 | True 282 | >>> METHODS_MAP["cold-brew-conc"]["grind_upper_limit"] == 1400 283 | True 284 | >>> METHODS_MAP["cold-brew-conc"]["grind_lower_limit"] == 800 285 | True 286 | >>> cold_brew_conc_params["grind"] == 1100 # https://honestcoffeeguide.com/coffee-grind-size-chart/#cold-brew 287 | True 288 | >>> METHODS_MAP["cold-brew-conc"]["temperature_upper_limit"] == 40 # https://perfectdailygrind.com/2021/07/can-you-brew-coffee-with-warm-water 289 | True 290 | >>> METHODS_MAP["cold-brew-conc"]["temperature_lower_limit"] == 0 # https://perfectdailygrind.com/2021/07/can-you-brew-coffee-with-warm-water 291 | True 292 | >>> cold_brew_conc_params["temperature"] == 20 # https://perfectdailygrind.com/2021/07/can-you-brew-coffee-with-warm-water 293 | True 294 | >>> moka_pot_params = load_method_params("moka-pot") # https://bakedbrewedbeautiful.com/how-to-make-coffee-in-moka-pot 295 | >>> moka_pot_params["coffee_ratio"] == 1 296 | True 297 | >>> moka_pot_params["water_ratio"] == 10 298 | True 299 | >>> moka_pot_params["water"] == 60 300 | True 301 | >>> moka_pot_params["ratio"] = moka_pot_params["coffee_ratio"] / moka_pot_params["water_ratio"] 302 | >>> moka_pot_coffee = calculate_coffee(ratio=moka_pot_params["ratio"], water=moka_pot_params["water"], water_unit=moka_pot_params["water_unit"], coffee_unit=moka_pot_params["coffee_unit"]) 303 | >>> moka_pot_coffee == 6 304 | True 305 | >>> moka_pot_water = round(calculate_water(ratio=moka_pot_params["ratio"], coffee=moka_pot_coffee, water_unit=moka_pot_params["water_unit"], coffee_unit=moka_pot_params["coffee_unit"]), 3) 306 | >>> moka_pot_water == moka_pot_params["water"] 307 | True 308 | >>> moka_pot_coffee == METHODS_MAP["moka-pot"]["coffee"] 309 | True 310 | >>> METHODS_MAP["moka-pot"]["ratio_upper_limit"] == Fraction(1, 7) 311 | True 312 | >>> METHODS_MAP["moka-pot"]["ratio_lower_limit"] == Fraction(1, 12) 313 | True 314 | >>> METHODS_MAP["moka-pot"]["grind_upper_limit"] == 660 315 | True 316 | >>> METHODS_MAP["moka-pot"]["grind_lower_limit"] == 360 317 | True 318 | >>> moka_pot_params["grind"] == 510 # https://honestcoffeeguide.com/coffee-grind-size-chart/#moka-pot 319 | True 320 | >>> METHODS_MAP["moka-pot"]["temperature_upper_limit"] == 95 # https://honestcoffeeguide.com/best-temperature-to-brew-coffee/ 321 | True 322 | >>> METHODS_MAP["moka-pot"]["temperature_lower_limit"] == 85 # https://honestcoffeeguide.com/best-temperature-to-brew-coffee/ 323 | True 324 | >>> moka_pot_params["temperature"] == 93 # https://honestcoffeeguide.com/what-temperature-to-brew-a-moka-pot 325 | True 326 | >>> ristretto_params = load_method_params("ristretto") # https://honestcoffeeguide.com/coffee-to-water-ratio-calculator 327 | >>> ristretto_params["coffee_ratio"] == 1 328 | True 329 | >>> ristretto_params["water_ratio"] == 1 330 | True 331 | >>> ristretto_params["water"] == 18 332 | True 333 | >>> ristretto_params["ratio"] = ristretto_params["coffee_ratio"] / ristretto_params["water_ratio"] 334 | >>> ristretto_coffee = calculate_coffee(ratio=ristretto_params["ratio"], water=ristretto_params["water"], water_unit=ristretto_params["water_unit"], coffee_unit=ristretto_params["coffee_unit"]) 335 | >>> ristretto_coffee == 18 336 | True 337 | >>> ristretto_water = round(calculate_water(ratio=ristretto_params["ratio"], coffee=ristretto_coffee, water_unit=ristretto_params["water_unit"], coffee_unit=ristretto_params["coffee_unit"]), 3) 338 | >>> ristretto_water == ristretto_params["water"] 339 | True 340 | >>> ristretto_coffee == METHODS_MAP["ristretto"]["coffee"] 341 | True 342 | >>> METHODS_MAP["ristretto"]["ratio_upper_limit"] == Fraction(1, 1) 343 | True 344 | >>> METHODS_MAP["ristretto"]["ratio_lower_limit"] == Fraction(2, 3) 345 | True 346 | >>> METHODS_MAP["ristretto"]["grind_upper_limit"] == 380 347 | True 348 | >>> METHODS_MAP["ristretto"]["grind_lower_limit"] == 180 349 | True 350 | >>> ristretto_params["grind"] == 280 # https://honestcoffeeguide.com/coffee-grind-size-chart/#espresso 351 | True 352 | >>> METHODS_MAP["ristretto"]["temperature_upper_limit"] == 95 # https://honestcoffeeguide.com/best-temperature-to-brew-coffee/ 353 | True 354 | >>> METHODS_MAP["ristretto"]["temperature_lower_limit"] == 85 # https://honestcoffeeguide.com/best-temperature-to-brew-coffee/ 355 | True 356 | >>> ristretto_params["temperature"] == 92 # https://honestcoffeeguide.com/best-temperature-to-brew-coffee/ 357 | True 358 | >>> lungo_params = load_method_params("lungo") # https://honestcoffeeguide.com/coffee-to-water-ratio-calculator 359 | >>> lungo_params["coffee_ratio"] == 1 360 | True 361 | >>> lungo_params["water_ratio"] == 4 362 | True 363 | >>> lungo_params["water"] == 72 364 | True 365 | >>> lungo_params["ratio"] = lungo_params["coffee_ratio"] / lungo_params["water_ratio"] 366 | >>> lungo_coffee = calculate_coffee(ratio=lungo_params["ratio"], water=lungo_params["water"], water_unit=lungo_params["water_unit"], coffee_unit=lungo_params["coffee_unit"]) 367 | >>> lungo_coffee == 18 368 | True 369 | >>> lungo_water = round(calculate_water(ratio=lungo_params["ratio"], coffee=lungo_coffee, water_unit=lungo_params["water_unit"], coffee_unit=lungo_params["coffee_unit"]), 3) 370 | >>> lungo_water == lungo_params["water"] 371 | True 372 | >>> lungo_coffee == METHODS_MAP["lungo"]["coffee"] 373 | True 374 | >>> METHODS_MAP["lungo"]["ratio_upper_limit"] == Fraction(2, 5) 375 | True 376 | >>> METHODS_MAP["lungo"]["ratio_lower_limit"] == Fraction(1, 4) 377 | True 378 | >>> METHODS_MAP["lungo"]["grind_upper_limit"] == 380 379 | True 380 | >>> METHODS_MAP["lungo"]["grind_lower_limit"] == 180 381 | True 382 | >>> lungo_params["grind"] == 280 # https://honestcoffeeguide.com/coffee-grind-size-chart/#espresso 383 | True 384 | >>> METHODS_MAP["lungo"]["temperature_upper_limit"] == 95 # https://honestcoffeeguide.com/best-temperature-to-brew-coffee/ 385 | True 386 | >>> METHODS_MAP["lungo"]["temperature_lower_limit"] == 85 # https://honestcoffeeguide.com/best-temperature-to-brew-coffee/ 387 | True 388 | >>> lungo_params["temperature"] == 92 # https://honestcoffeeguide.com/best-temperature-to-brew-coffee/ 389 | True 390 | >>> turkish_params = load_method_params("turkish") # https://www.drinktrade.com/blogs/education/how-to-make-turkish-coffee 391 | >>> turkish_params["coffee_ratio"] == 1 392 | True 393 | >>> turkish_params["water_ratio"] == 10 394 | True 395 | >>> turkish_params["water"] == 50 396 | True 397 | >>> turkish_params["ratio"] = turkish_params["coffee_ratio"] / turkish_params["water_ratio"] 398 | >>> turkish_coffee = calculate_coffee(ratio=turkish_params["ratio"], water=turkish_params["water"], water_unit=turkish_params["water_unit"], coffee_unit=turkish_params["coffee_unit"]) 399 | >>> turkish_coffee == 5 400 | True 401 | >>> turkish_water = round(calculate_water(ratio=turkish_params["ratio"], coffee=turkish_coffee, water_unit=turkish_params["water_unit"], coffee_unit=turkish_params["coffee_unit"]), 3) 402 | >>> turkish_water == turkish_params["water"] 403 | True 404 | >>> turkish_coffee == METHODS_MAP["turkish"]["coffee"] 405 | True 406 | >>> METHODS_MAP["turkish"]["ratio_upper_limit"] == Fraction(1, 8) 407 | True 408 | >>> METHODS_MAP["turkish"]["ratio_lower_limit"] == Fraction(1, 12) 409 | True 410 | >>> METHODS_MAP["turkish"]["grind_upper_limit"] == 220 411 | True 412 | >>> METHODS_MAP["turkish"]["grind_lower_limit"] == 40 413 | True 414 | >>> turkish_params["grind"] == 130 # https://honestcoffeeguide.com/coffee-grind-size-chart/#turkish-coffee 415 | True 416 | >>> METHODS_MAP["turkish"]["temperature_upper_limit"] == 95 # https://ravecoffee.co.uk/blogs/news/how-to-brew-coffee-using-an-ibrik-cezve-for-turkish-style-coffee 417 | True 418 | >>> METHODS_MAP["turkish"]["temperature_lower_limit"] == 90 # https://ravecoffee.co.uk/blogs/news/how-to-brew-coffee-using-an-ibrik-cezve-for-turkish-style-coffee 419 | True 420 | >>> turkish_params["temperature"] == 90 # https://ravecoffee.co.uk/blogs/news/how-to-brew-coffee-using-an-ibrik-cezve-for-turkish-style-coffee 421 | True 422 | >>> cupping_params = load_method_params("cupping") # https://www.horshamcoffeeroaster.co.uk/pages/how-to-cup-coffee 423 | >>> cupping_params["coffee_ratio"] == 11 424 | True 425 | >>> cupping_params["water_ratio"] == 200 426 | True 427 | >>> cupping_params["water"] == 150 428 | True 429 | >>> cupping_params["ratio"] = cupping_params["coffee_ratio"] / cupping_params["water_ratio"] 430 | >>> cupping_coffee = calculate_coffee(ratio=cupping_params["ratio"], water=cupping_params["water"], water_unit=cupping_params["water_unit"], coffee_unit=cupping_params["coffee_unit"]) 431 | >>> cupping_coffee == 8.25 432 | True 433 | >>> cupping_water = round(calculate_water(ratio=cupping_params["ratio"], coffee=cupping_coffee, water_unit=cupping_params["water_unit"], coffee_unit=cupping_params["coffee_unit"]), 3) 434 | >>> cupping_water == cupping_params["water"] 435 | True 436 | >>> cupping_coffee == METHODS_MAP["cupping"]["coffee"] 437 | True 438 | >>> METHODS_MAP["cupping"]["ratio_upper_limit"] == Fraction(1, 17) 439 | True 440 | >>> METHODS_MAP["cupping"]["ratio_lower_limit"] == Fraction(1, 19) 441 | True 442 | >>> METHODS_MAP["cupping"]["grind_upper_limit"] == 850 443 | True 444 | >>> METHODS_MAP["cupping"]["grind_lower_limit"] == 460 445 | True 446 | >>> cupping_params["grind"] == 655 # https://honestcoffeeguide.com/coffee-grind-size-chart/#cupping 447 | True 448 | >>> METHODS_MAP["cupping"]["temperature_upper_limit"] == 95 # https://honestcoffeeguide.com/best-temperature-to-brew-coffee/ 449 | True 450 | >>> METHODS_MAP["cupping"]["temperature_lower_limit"] == 85 # https://honestcoffeeguide.com/best-temperature-to-brew-coffee/ 451 | True 452 | >>> cupping_params["temperature"] == 93 # https://www.diffordsguide.com/g/1113/coffee/cupping 453 | True 454 | >>> aero_press_params = load_method_params("aero-press") # https://aeroprecipe.com/recipes/tetsu-kasuya-aeropress-recipe 455 | >>> aero_press_params["coffee_ratio"] == 1 456 | True 457 | >>> aero_press_params["water_ratio"] == 15 458 | True 459 | >>> aero_press_params["water"] == 135 460 | True 461 | >>> aero_press_params["ratio"] = aero_press_params["coffee_ratio"] / aero_press_params["water_ratio"] 462 | >>> aero_press_coffee = calculate_coffee(ratio=aero_press_params["ratio"], water=aero_press_params["water"], water_unit=aero_press_params["water_unit"], coffee_unit=aero_press_params["coffee_unit"]) 463 | >>> aero_press_coffee == 9 464 | True 465 | >>> aero_press_water = round(calculate_water(ratio=aero_press_params["ratio"], coffee=aero_press_coffee, water_unit=aero_press_params["water_unit"], coffee_unit=aero_press_params["coffee_unit"]), 3) 466 | >>> aero_press_water == aero_press_params["water"] 467 | True 468 | >>> aero_press_coffee == METHODS_MAP["aero-press"]["coffee"] 469 | True 470 | >>> METHODS_MAP["aero-press"]["ratio_upper_limit"] == Fraction(1, 12) 471 | True 472 | >>> METHODS_MAP["aero-press"]["ratio_lower_limit"] == Fraction(1, 18) 473 | True 474 | >>> METHODS_MAP["aero-press"]["grind_upper_limit"] == 960 475 | True 476 | >>> METHODS_MAP["aero-press"]["grind_lower_limit"] == 320 477 | True 478 | >>> aero_press_params["grind"] == 640 # https://honestcoffeeguide.com/coffee-grind-size-chart/#aeropress 479 | True 480 | >>> METHODS_MAP["aero-press"]["temperature_upper_limit"] == 95 # https://honestcoffeeguide.com/best-temperature-to-brew-coffee/ 481 | True 482 | >>> METHODS_MAP["aero-press"]["temperature_lower_limit"] == 90 # https://honestcoffeeguide.com/best-temperature-to-brew-coffee/ 483 | True 484 | >>> aero_press_params["temperature"] == 93 # https://honestcoffeeguide.com/best-temperature-to-brew-coffee/ 485 | True 486 | >>> aero_press_conc_params = load_method_params("aero-press-conc") # https://www.seattlecoffeegear.com/pages/product-resource/aero-press-product-resources 487 | >>> aero_press_conc_params["coffee_ratio"] == 1 488 | True 489 | >>> aero_press_conc_params["water_ratio"] == 6 490 | True 491 | >>> aero_press_conc_params["water"] == 90 492 | True 493 | >>> aero_press_conc_params["ratio"] = aero_press_conc_params["coffee_ratio"] / aero_press_conc_params["water_ratio"] 494 | >>> aero_press_conc_coffee = calculate_coffee(ratio=aero_press_conc_params["ratio"], water=aero_press_conc_params["water"], water_unit=aero_press_conc_params["water_unit"], coffee_unit=aero_press_conc_params["coffee_unit"]) 495 | >>> aero_press_conc_coffee == 15 496 | True 497 | >>> aero_press_conc_water = round(calculate_water(ratio=aero_press_conc_params["ratio"], coffee=aero_press_conc_coffee, water_unit=aero_press_conc_params["water_unit"], coffee_unit=aero_press_conc_params["coffee_unit"]), 3) 498 | >>> aero_press_conc_water == aero_press_conc_params["water"] 499 | True 500 | >>> aero_press_conc_coffee == METHODS_MAP["aero-press-conc"]["coffee"] 501 | True 502 | >>> METHODS_MAP["aero-press-conc"]["ratio_upper_limit"] == Fraction(1, 5) 503 | True 504 | >>> METHODS_MAP["aero-press-conc"]["ratio_lower_limit"] == Fraction(1, 7) 505 | True 506 | >>> METHODS_MAP["aero-press-conc"]["grind_upper_limit"] == 960 507 | True 508 | >>> METHODS_MAP["aero-press-conc"]["grind_lower_limit"] == 320 509 | True 510 | >>> aero_press_conc_params["grind"] == 640 # https://honestcoffeeguide.com/coffee-grind-size-chart/#aeropress 511 | True 512 | >>> METHODS_MAP["aero-press-conc"]["temperature_upper_limit"] == 95 # https://honestcoffeeguide.com/best-temperature-to-brew-coffee/ 513 | True 514 | >>> METHODS_MAP["aero-press-conc"]["temperature_lower_limit"] == 90 # https://honestcoffeeguide.com/best-temperature-to-brew-coffee/ 515 | True 516 | >>> aero_press_conc_params["temperature"] == 93 # https://honestcoffeeguide.com/best-temperature-to-brew-coffee/ 517 | True 518 | >>> aero_press_inv_params = load_method_params("aero-press-inv") # https://aeroprecipe.com/recipes/all-about-the-intervals 519 | >>> aero_press_inv_params["coffee_ratio"] == 1 520 | True 521 | >>> aero_press_inv_params["water_ratio"] == 12 522 | True 523 | >>> aero_press_inv_params["water"] == 132 524 | True 525 | >>> aero_press_inv_params["ratio"] = aero_press_inv_params["coffee_ratio"] / aero_press_inv_params["water_ratio"] 526 | >>> aero_press_inv_coffee = calculate_coffee(ratio=aero_press_inv_params["ratio"], water=aero_press_inv_params["water"], water_unit=aero_press_inv_params["water_unit"], coffee_unit=aero_press_inv_params["coffee_unit"]) 527 | >>> aero_press_inv_coffee == 11 528 | True 529 | >>> aero_press_inv_water = round(calculate_water(ratio=aero_press_inv_params["ratio"], coffee=aero_press_inv_coffee, water_unit=aero_press_inv_params["water_unit"], coffee_unit=aero_press_inv_params["coffee_unit"]), 3) 530 | >>> aero_press_inv_water == aero_press_inv_params["water"] 531 | True 532 | >>> aero_press_inv_coffee == METHODS_MAP["aero-press-inv"]["coffee"] 533 | True 534 | >>> METHODS_MAP["aero-press-inv"]["ratio_upper_limit"] == Fraction(1, 10) 535 | True 536 | >>> METHODS_MAP["aero-press-inv"]["ratio_lower_limit"] == Fraction(1, 14) 537 | True 538 | >>> METHODS_MAP["aero-press-inv"]["grind_upper_limit"] == 960 539 | True 540 | >>> METHODS_MAP["aero-press-inv"]["grind_lower_limit"] == 320 541 | True 542 | >>> aero_press_inv_params["grind"] == 640 # https://honestcoffeeguide.com/coffee-grind-size-chart/#aeropress 543 | True 544 | >>> METHODS_MAP["aero-press-inv"]["temperature_upper_limit"] == 95 # https://honestcoffeeguide.com/best-temperature-to-brew-coffee/ 545 | True 546 | >>> METHODS_MAP["aero-press-inv"]["temperature_lower_limit"] == 90 # https://honestcoffeeguide.com/best-temperature-to-brew-coffee/ 547 | True 548 | >>> aero_press_inv_params["temperature"] == 93 # https://honestcoffeeguide.com/best-temperature-to-brew-coffee/ 549 | True 550 | >>> steep_and_release_params = load_method_params("steep-and-release") # https://squaremileblog.com/brew-guide-clever-dripper/ 551 | >>> steep_and_release_params["coffee_ratio"] == 1 552 | True 553 | >>> steep_and_release_params["water_ratio"] == 16 554 | True 555 | >>> steep_and_release_params["water"] == 255 556 | True 557 | >>> steep_and_release_params["ratio"] = steep_and_release_params["coffee_ratio"] / steep_and_release_params["water_ratio"] 558 | >>> steep_and_release_coffee = calculate_coffee(ratio=steep_and_release_params["ratio"], water=steep_and_release_params["water"], water_unit=steep_and_release_params["water_unit"], coffee_unit=steep_and_release_params["coffee_unit"]) 559 | >>> steep_and_release_coffee == 15.9375 560 | True 561 | >>> steep_and_release_water = round(calculate_water(ratio=steep_and_release_params["ratio"], coffee=steep_and_release_coffee, water_unit=steep_and_release_params["water_unit"], coffee_unit=steep_and_release_params["coffee_unit"]), 3) 562 | >>> steep_and_release_water == steep_and_release_params["water"] 563 | True 564 | >>> steep_and_release_coffee == METHODS_MAP["steep-and-release"]["coffee"] 565 | True 566 | >>> METHODS_MAP["steep-and-release"]["ratio_upper_limit"] == Fraction(1, 14) 567 | True 568 | >>> METHODS_MAP["steep-and-release"]["ratio_lower_limit"] == Fraction(1, 17) 569 | True 570 | >>> METHODS_MAP["steep-and-release"]["grind_upper_limit"] == 825 571 | True 572 | >>> METHODS_MAP["steep-and-release"]["grind_lower_limit"] == 450 573 | True 574 | >>> steep_and_release_params["grind"] == 638 # https://honestcoffeeguide.com/coffee-grind-size-chart/#steepandrelease 575 | True 576 | >>> METHODS_MAP["steep-and-release"]["temperature_upper_limit"] == 95 # https://honestcoffeeguide.com/best-temperature-to-brew-coffee/ 577 | True 578 | >>> METHODS_MAP["steep-and-release"]["temperature_lower_limit"] == 85 # https://honestcoffeeguide.com/best-temperature-to-brew-coffee/ 579 | True 580 | >>> steep_and_release_params["temperature"] == 93 # https://honestcoffeeguide.com/best-temperature-to-brew-coffee/ 581 | True 582 | >>> clever_dripper_params = load_method_params("clever-dripper") 583 | >>> clever_dripper_params["coffee_ratio"] == 1 584 | True 585 | >>> clever_dripper_params["water_ratio"] == 16.67 # https://coffee-coach.netlify.app/clever-by-james-hoffman/ 586 | True 587 | >>> clever_dripper_params["water"] == 250 # https://coffee-coach.netlify.app/clever-by-james-hoffman/ 588 | True 589 | >>> clever_dripper_params["ratio"] = clever_dripper_params["coffee_ratio"] / clever_dripper_params["water_ratio"] 590 | >>> clever_dripper_coffee = calculate_coffee(ratio=clever_dripper_params["ratio"], water=clever_dripper_params["water"], water_unit=clever_dripper_params["water_unit"], coffee_unit=clever_dripper_params["coffee_unit"]) 591 | >>> round(clever_dripper_coffee, 1) == 15 # https://coffee-coach.netlify.app/clever-by-james-hoffman/ 592 | True 593 | >>> clever_dripper_water = round(calculate_water(ratio=clever_dripper_params["ratio"], coffee=clever_dripper_coffee, water_unit=clever_dripper_params["water_unit"], coffee_unit=clever_dripper_params["coffee_unit"]), 3) 594 | >>> clever_dripper_water == clever_dripper_params["water"] 595 | True 596 | >>> round(clever_dripper_coffee, 3) == METHODS_MAP["clever-dripper"]["coffee"] 597 | True 598 | >>> METHODS_MAP["clever-dripper"]["ratio_upper_limit"] == Fraction(1, 15) # https://sablebrew.com/blogs/news/the-latest-method-to-brew-coffee-with-your-clever-dripper 599 | True 600 | >>> METHODS_MAP["clever-dripper"]["ratio_lower_limit"] == Fraction(1, 20) # https://sablebrew.com/blogs/news/the-latest-method-to-brew-coffee-with-your-clever-dripper 601 | True 602 | >>> METHODS_MAP["clever-dripper"]["grind_upper_limit"] == 800 603 | True 604 | >>> METHODS_MAP["clever-dripper"]["grind_lower_limit"] == 400 605 | True 606 | >>> clever_dripper_params["grind"] == 600 607 | True 608 | >>> METHODS_MAP["clever-dripper"]["temperature_upper_limit"] == 96 # https://sablebrew.com/blogs/news/the-latest-method-to-brew-coffee-with-your-clever-dripper 609 | True 610 | >>> METHODS_MAP["clever-dripper"]["temperature_lower_limit"] == 91 # https://sablebrew.com/blogs/news/the-latest-method-to-brew-coffee-with-your-clever-dripper 611 | True 612 | >>> clever_dripper_params["temperature"] == 93 613 | True 614 | >>> phin_filter_params = load_method_params("phin-filter") 615 | >>> phin_filter_params["coffee_ratio"] == 1 616 | True 617 | >>> phin_filter_params["water_ratio"] == 2 618 | True 619 | >>> phin_filter_params["water"] == 72 620 | True 621 | >>> phin_filter_params["ratio"] = phin_filter_params["coffee_ratio"] / phin_filter_params["water_ratio"] 622 | >>> phin_filter_coffee = calculate_coffee(ratio=phin_filter_params["ratio"], water=phin_filter_params["water"], water_unit=phin_filter_params["water_unit"], coffee_unit=phin_filter_params["coffee_unit"]) 623 | >>> phin_filter_coffee == 36 624 | True 625 | >>> phin_filter_water = round(calculate_water(ratio=phin_filter_params["ratio"], coffee=phin_filter_coffee, water_unit=phin_filter_params["water_unit"], coffee_unit=phin_filter_params["coffee_unit"]), 3) 626 | >>> phin_filter_water == phin_filter_params["water"] 627 | True 628 | >>> phin_filter_coffee == METHODS_MAP["phin-filter"]["coffee"] 629 | True 630 | >>> METHODS_MAP["phin-filter"]["ratio_upper_limit"] == Fraction(1, 2) # https://cafely.com/blogs/coffee-brew-guide/vietnamese-phin-drip 631 | True 632 | >>> METHODS_MAP["phin-filter"]["ratio_lower_limit"] == Fraction(1, 4) # https://cafely.com/blogs/coffee-brew-guide/vietnamese-phin-drip 633 | True 634 | >>> METHODS_MAP["phin-filter"]["grind_upper_limit"] == 400 635 | True 636 | >>> METHODS_MAP["phin-filter"]["grind_lower_limit"] == 200 637 | True 638 | >>> phin_filter_params["grind"] == 300 # https://cafely.com/blogs/coffee-brew-guide/vietnamese-phin-drip 639 | True 640 | >>> METHODS_MAP["phin-filter"]["temperature_upper_limit"] == 96 # https://cafely.com/blogs/coffee-brew-guide/vietnamese-phin-drip 641 | True 642 | >>> METHODS_MAP["phin-filter"]["temperature_lower_limit"] == 90 # https://cafely.com/blogs/coffee-brew-guide/vietnamese-phin-drip 643 | True 644 | >>> phin_filter_params["temperature"] == 93 # https://cafely.com/blogs/coffee-brew-guide/vietnamese-phin-drip 645 | True 646 | >>> kalita_wave_params = load_method_params("kalita-wave") 647 | >>> kalita_wave_params["coffee_ratio"] == 1 648 | True 649 | >>> kalita_wave_params["water_ratio"] == 16 650 | True 651 | >>> kalita_wave_params["water"] == 400 652 | True 653 | >>> kalita_wave_params["ratio"] = kalita_wave_params["coffee_ratio"] / kalita_wave_params["water_ratio"] 654 | >>> kalita_wave_coffee = calculate_coffee(ratio=kalita_wave_params["ratio"], water=kalita_wave_params["water"], water_unit=kalita_wave_params["water_unit"], coffee_unit=kalita_wave_params["coffee_unit"]) 655 | >>> kalita_wave_coffee == 25 656 | True 657 | >>> kalita_wave_water = round(calculate_water(ratio=kalita_wave_params["ratio"], coffee=kalita_wave_coffee, water_unit=kalita_wave_params["water_unit"], coffee_unit=kalita_wave_params["coffee_unit"]), 3) 658 | >>> kalita_wave_water == kalita_wave_params["water"] 659 | True 660 | >>> kalita_wave_coffee == METHODS_MAP["kalita-wave"]["coffee"] 661 | True 662 | >>> METHODS_MAP["kalita-wave"]["ratio_upper_limit"] == Fraction(1, 15) # https://littlewaves.coffee/products/pour-over-brew-guide 663 | True 664 | >>> METHODS_MAP["kalita-wave"]["ratio_lower_limit"] == Fraction(1, 17) # https://littlewaves.coffee/products/pour-over-brew-guide 665 | True 666 | >>> METHODS_MAP["kalita-wave"]["grind_upper_limit"] == 1000 667 | True 668 | >>> METHODS_MAP["kalita-wave"]["grind_lower_limit"] == 800 669 | True 670 | >>> kalita_wave_params["grind"] == 900 # https://littlewaves.coffee/products/pour-over-brew-guide 671 | True 672 | >>> METHODS_MAP["kalita-wave"]["temperature_upper_limit"] == 96 # https://littlewaves.coffee/products/pour-over-brew-guide 673 | True 674 | >>> METHODS_MAP["kalita-wave"]["temperature_lower_limit"] == 90 # https://littlewaves.coffee/products/pour-over-brew-guide 675 | True 676 | >>> kalita_wave_params["temperature"] == 93 # https://littlewaves.coffee/products/pour-over-brew-guide 677 | True 678 | >>> instant_coffee_params = load_method_params("instant-coffee") 679 | >>> instant_coffee_params["coffee_ratio"] == 1 # https://athome.starbucks.com/brewing-guide/how-make-perfect-instant-coffee-hot-or-iced 680 | True 681 | >>> instant_coffee_params["water_ratio"] == 35 # https://athome.starbucks.com/brewing-guide/how-make-perfect-instant-coffee-hot-or-iced 682 | True 683 | >>> instant_coffee_params["water"] == 175 684 | True 685 | >>> instant_coffee_params["ratio"] = instant_coffee_params["coffee_ratio"] / instant_coffee_params["water_ratio"] 686 | >>> instant_coffee_coffee = calculate_coffee(ratio=instant_coffee_params["ratio"], water=instant_coffee_params["water"], water_unit=instant_coffee_params["water_unit"], coffee_unit=instant_coffee_params["coffee_unit"]) 687 | >>> instant_coffee_coffee == 5 688 | True 689 | >>> instant_coffee_coffee == METHODS_MAP["instant-coffee"]["coffee"] 690 | True 691 | >>> instant_coffee_water = round(calculate_water(ratio=instant_coffee_params["ratio"], coffee=instant_coffee_coffee, water_unit=instant_coffee_params["water_unit"], coffee_unit=instant_coffee_params["coffee_unit"]), 3) 692 | >>> instant_coffee_water == instant_coffee_params["water"] 693 | True 694 | >>> METHODS_MAP["instant-coffee"]["ratio_upper_limit"] == Fraction(1, 15) 695 | True 696 | >>> METHODS_MAP["instant-coffee"]["ratio_lower_limit"] == Fraction(1, 50) 697 | True 698 | >>> instant_coffee_params["grind"] == 0 699 | True 700 | >>> METHODS_MAP["instant-coffee"]["temperature_upper_limit"] == 93 701 | True 702 | >>> METHODS_MAP["instant-coffee"]["temperature_lower_limit"] == 80 703 | True 704 | >>> instant_coffee_params["temperature"] == 85 # https://athome.starbucks.com/brewing-guide/how-make-perfect-instant-coffee-hot-or-iced 705 | True 706 | >>> custom_params = load_method_params("custom") 707 | >>> custom_params["coffee_ratio"] == 1 708 | True 709 | >>> custom_params["water_ratio"] == 17 710 | True 711 | >>> custom_params["water"] == 240 712 | True 713 | >>> custom_params["coffee_unit"] == "g" 714 | True 715 | >>> custom_params["ratio"] = custom_params["coffee_ratio"] / custom_params["water_ratio"] 716 | >>> custom_coffee_g = calculate_coffee(ratio=custom_params["ratio"], water=custom_params["water"], water_unit=custom_params["water_unit"], coffee_unit=custom_params["coffee_unit"]) 717 | >>> custom_coffee_g == 14.117647058823529 718 | True 719 | >>> custom_params["coffee_unit"] = "oz" # https://www.rapidtables.com/convert/weight/gram-to-ounce.html?x=14.117647058823529 720 | >>> custom_coffee_oz = calculate_coffee(ratio=custom_params["ratio"], water=custom_params["water"], water_unit=custom_params["water_unit"], coffee_unit=custom_params["coffee_unit"]) 721 | >>> custom_coffee_oz == 0.4979853451764706 722 | True 723 | >>> custom_params["coffee_unit"] = "lb" # https://www.rapidtables.com/convert/weight/gram-to-pound.html?x=14.117647058823529 724 | >>> custom_coffee_lb = calculate_coffee(ratio=custom_params["ratio"], water=custom_params["water"], water_unit=custom_params["water_unit"], coffee_unit=custom_params["coffee_unit"]) 725 | >>> custom_coffee_lb == 0.03112408407317647 726 | True 727 | >>> custom_params["coffee_unit"] = "mg" # https://www.rapidtables.com/convert/weight/gram-to-mg.html?x=14.117647058823529 728 | >>> custom_coffee_mg = calculate_coffee(ratio=custom_params["ratio"], water=custom_params["water"], water_unit=custom_params["water_unit"], coffee_unit=custom_params["coffee_unit"]) 729 | >>> custom_coffee_mg == 14117.64705882353 730 | True 731 | >>> custom_params["coffee_unit"] = "kg" # https://www.rapidtables.com/convert/weight/gram-to-kg.html?x=14.117647058823529 732 | >>> custom_coffee_kg = calculate_coffee(ratio=custom_params["ratio"], water=custom_params["water"], water_unit=custom_params["water_unit"], coffee_unit=custom_params["coffee_unit"]) 733 | >>> custom_coffee_kg == 0.01411764705882353 734 | True 735 | >>> custom_params["coffee_unit"] = "cb" # https://honestcoffeeguide.com/whole-bean-to-ground-coffee-ratio/ 736 | >>> custom_coffee_cb = calculate_coffee(ratio=custom_params["ratio"], water=custom_params["water"], water_unit=custom_params["water_unit"], coffee_unit=custom_params["coffee_unit"]) 737 | >>> custom_coffee_cb == 107 738 | True 739 | >>> custom_params["coffee_unit"] = "tbsp" # https://www.howmany.wiki/wv/ 740 | >>> custom_coffee_tbsp = calculate_coffee(ratio=custom_params["ratio"], water=custom_params["water"], water_unit=custom_params["water_unit"], coffee_unit=custom_params["coffee_unit"]) 741 | >>> custom_coffee_tbsp == 2.6157176470588235 742 | True 743 | >>> custom_params["coffee_unit"] = "tsp" # https://www.howmany.wiki/wv/ 744 | >>> custom_coffee_tsp = calculate_coffee(ratio=custom_params["ratio"], water=custom_params["water"], water_unit=custom_params["water_unit"], coffee_unit=custom_params["coffee_unit"]) 745 | >>> custom_coffee_tsp == 7.847294117647058 746 | True 747 | >>> custom_params["coffee_unit"] = "dsp" # https://www.howmany.wiki/wv/ 748 | >>> custom_coffee_dsp = calculate_coffee(ratio=custom_params["ratio"], water=custom_params["water"], water_unit=custom_params["water_unit"], coffee_unit=custom_params["coffee_unit"]) 749 | >>> custom_coffee_dsp == 3.923576470588235 750 | True 751 | >>> custom_params["coffee_unit"] = "cup" # https://www.howmany.wiki/wv/ 752 | >>> custom_coffee_cup = calculate_coffee(ratio=custom_params["ratio"], water=custom_params["water"], water_unit=custom_params["water_unit"], coffee_unit=custom_params["coffee_unit"]) 753 | >>> custom_coffee_cup == 0.16348235294117647 754 | True 755 | >>> convert_water(240, "g") == 240 # https://www.calculator.net/weight-calculator.html 756 | True 757 | >>> convert_water(240, "g", True) == 240.0 # https://www.calculator.net/weight-calculator.html 758 | True 759 | >>> convert_water(240, "kg") == 0.24 # https://www.calculator.net/weight-calculator.html 760 | True 761 | >>> convert_water(240, "kg", True) == 240000.0 # https://www.calculator.net/weight-calculator.html 762 | True 763 | >>> convert_water(240, "mg") == 240000 # https://www.calculator.net/weight-calculator.html 764 | True 765 | >>> convert_water(240, "mg", True) == 0.24 # https://www.calculator.net/weight-calculator.html 766 | True 767 | >>> convert_water(240, "oz") == 8.465750868 # https://www.calculator.net/weight-calculator.html 768 | True 769 | >>> convert_water(240, "oz", True) == 6803.885549919067 # https://www.calculator.net/weight-calculator.html 770 | True 771 | >>> convert_water(240, "lb") == 0.5291094292440001 # https://www.calculator.net/weight-calculator.html 772 | True 773 | >>> convert_water(240, "lb", True) == 108862.16879993954 # https://www.calculator.net/weight-calculator.html 774 | True 775 | >>> convert_water(240, "ml") == 240 # https://www.howmany.wiki/wv/ 776 | True 777 | >>> convert_water(240, "ml", True) == 240.0 # https://www.howmany.wiki/wv/ 778 | True 779 | >>> convert_water(240, "l") == 0.24 # https://www.howmany.wiki/wv/ 780 | True 781 | >>> convert_water(240, "l", True) == 240000.0 # https://www.howmany.wiki/wv/ 782 | True 783 | >>> convert_water(240, "tbsp") == 16.230719999999998 # https://www.howmany.wiki/wv/ 784 | True 785 | >>> convert_water(240, "tbsp", True) == 3548.8259300881296 # https://www.howmany.wiki/wv/ 786 | True 787 | >>> convert_water(240, "tsp") == 48.6912 # https://www.howmany.wiki/wv/ 788 | True 789 | >>> convert_water(240, "tsp", True) == 1182.9652996845425 # https://www.howmany.wiki/wv/ 790 | True 791 | >>> convert_water(240, "dsp") == 24.3456 # https://www.howmany.wiki/wv/ 792 | True 793 | >>> convert_water(240, "dsp", True) == 2365.930599369085 # https://www.howmany.wiki/wv/ 794 | True 795 | >>> convert_water(240, "cup") == 1.014432 # https://www.howmany.wiki/wv/ 796 | True 797 | >>> convert_water(240, "cup", True) == 56780.54320052995 # https://www.howmany.wiki/wv/ 798 | True 799 | >>> round(convert_water(240, "pt"), 5) == 0.50721 # https://www.inchcalculator.com/convert/milliliter-to-pint/ 800 | True 801 | >>> int(convert_water(240, "pt", True)) == 113562 # https://www.inchcalculator.com/convert/pint-to-milliliter/ 802 | True 803 | >>> round(convert_water(240, "qt"), 4) == 0.2536 # https://www.inchcalculator.com/convert/milliliter-to-quart/ 804 | True 805 | >>> int(convert_water(240, "qt", True)) == 227124 # https://www.inchcalculator.com/convert/quart-to-milliliter/ 806 | True 807 | >>> round(convert_water(240, "fl oz"), 5) == 8.11536 # https://www.inchcalculator.com/convert/milliliter-to-fluid-ounce/ 808 | True 809 | >>> round(convert_water(240, "fl oz", True), 2) == 7097.65 # https://www.inchcalculator.com/convert/fluid-ounce-to-milliliter/ 810 | True 811 | >>> round(convert_water(240, "t oz"), 4) == 7.7162 # https://www.metric-conversions.org/weight/grams-to-troy-ounces.htm 812 | True 813 | >>> round(convert_water(240, "t oz", True), 1) == 7464.8 # https://www.metric-conversions.org/weight/troy-ounces-to-grams.htm 814 | True 815 | >>> round(convert_coffee(240, "t oz"), 4) == 7.7162 # https://www.metric-conversions.org/weight/grams-to-troy-ounces.htm 816 | True 817 | >>> round(convert_coffee(240, "gr"), 1) == 3703.8 # https://www.metric-conversions.org/weight/grams-to-grains.htm 818 | True 819 | >>> round(convert_water(240, "gr"), 1) == 3703.8 # https://www.metric-conversions.org/weight/grams-to-grains.htm 820 | True 821 | >>> round(convert_water(240, "gr", True), 3) == 15.552 # https://www.metric-conversions.org/weight/grains-to-grams.htm 822 | True 823 | >>> round(convert_coffee(240, "ct"), 1) == 1200.0 # https://www.metric-conversions.org/weight/grams-to-carats.htm 824 | True 825 | >>> round(convert_water(240, "ct"), 1) == 1200.0 # https://www.metric-conversions.org/weight/grams-to-carats.htm 826 | True 827 | >>> round(convert_water(240, "ct", True), 1) == 48.0 # https://www.metric-conversions.org/weight/carats-to-grams.htm 828 | True 829 | >>> round(convert_water(240, "cc"), 1) == 240.0 # https://www.metric-conversions.org/volume/milliliters-to-cubic-centimeters.htm 830 | True 831 | >>> round(convert_water(240, "cc", True), 1) == 240.0 # https://www.metric-conversions.org/volume/cubic-centimeters-to-milliliters.htm 832 | True 833 | >>> round(convert_water(240, "cl"), 1) == 24.0 # https://www.metric-conversions.org/volume/milliliters-to-centiliters.htm 834 | True 835 | >>> round(convert_water(240, "cl", True), 1) == 2400.0 # https://www.metric-conversions.org/volume/centiliters-to-milliliters.htm 836 | True 837 | >>> round(convert_coffee(240, "t lb"), 3) == 0.643 # https://www.metric-conversions.org/weight/grams-to-troy-pounds.htm 838 | True 839 | >>> round(convert_water(240, "t lb"), 3) == 0.643 # https://www.metric-conversions.org/weight/grams-to-troy-pounds.htm 840 | True 841 | >>> round(convert_water(240, "t lb", True), 1) == 89578.0 # https://www.metric-conversions.org/weight/troy-pounds-to-grams.htm 842 | True 843 | >>> round(convert_coffee(240, "dwt"), 2) == 154.32 # https://www.metric-conversions.org/weight/grams-to-pennyweights.htm 844 | True 845 | >>> round(convert_water(240, "dwt"), 2) == 154.32 # https://www.metric-conversions.org/weight/grams-to-pennyweights.htm 846 | True 847 | >>> round(convert_water(240, "dwt", True), 2) == 373.24 # https://www.metric-conversions.org/weight/pennyweights-to-grams.htm 848 | True 849 | >>> get_grind_type(100) == "Extra-Fine" # https://honestcoffeeguide.com/coffee-grind-size-chart/ 850 | True 851 | >>> get_grind_type(300) == "Fine" # https://honestcoffeeguide.com/coffee-grind-size-chart/ 852 | True 853 | >>> get_grind_type(500) == "Medium-Fine" # https://honestcoffeeguide.com/coffee-grind-size-chart/ 854 | True 855 | >>> get_grind_type(700) == "Medium" # https://honestcoffeeguide.com/coffee-grind-size-chart/ 856 | True 857 | >>> get_grind_type(900) == "Medium-Coarse" # https://honestcoffeeguide.com/coffee-grind-size-chart/ 858 | True 859 | >>> get_grind_type(1100) == "Coarse" # https://honestcoffeeguide.com/coffee-grind-size-chart/ 860 | True 861 | >>> get_grind_type(1300) == "Extra-Coarse" # https://honestcoffeeguide.com/coffee-grind-size-chart/ 862 | True 863 | >>> convert_temperature(60, "C", "C") 864 | 60 865 | >>> convert_temperature(60.0, "F", "F") 866 | 60 867 | >>> convert_temperature(60, "K", "K") 868 | 60 869 | >>> convert_temperature(62.2, "K", "K") 870 | 62.2 871 | >>> convert_temperature(60, "C", "F") # https://www.inchcalculator.com/convert/temperature/ 872 | 140 873 | >>> convert_temperature(60, "C", "K") # https://www.inchcalculator.com/convert/temperature/ 874 | 333.15 875 | >>> convert_temperature(60, "F", "C") # https://www.inchcalculator.com/convert/temperature/ 876 | 15.556 877 | >>> convert_temperature(60, "F", "K") # https://www.inchcalculator.com/convert/temperature/ 878 | 288.706 879 | >>> convert_temperature(60, "K", "C") # https://www.inchcalculator.com/convert/temperature/ 880 | -213.15 881 | >>> convert_temperature(60, "K", "F") # https://www.inchcalculator.com/convert/temperature/ 882 | -351.67 883 | """ 884 | --------------------------------------------------------------------------------