├── 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 | | Title |
8 | Code |
9 | Ratio |
10 | Ratio-LL |
11 | Ratio-UL |
12 | Grind-LL(um) |
13 | Grind-UL(um) |
14 | Temperature-LL(C) |
15 | Temperature-UL(C) |
16 | Water(g) |
17 | Coffee(g) |
18 | Version |
19 |
20 |
21 | | Custom |
22 | custom |
23 | 1/17 |
24 | -- |
25 | -- |
26 | -- |
27 | -- |
28 | -- |
29 | -- |
30 | 240 |
31 | 14.118 |
32 | >=0.1 |
33 |
34 |
35 | | V60 |
36 | v60 |
37 | 3/50 |
38 | 1/18 |
39 | 1/14 |
40 | 400 |
41 | 700 |
42 | 85 |
43 | 95 |
44 | 250 |
45 | 15 |
46 | >=0.1 |
47 |
48 |
49 | | Chemex |
50 | chemex |
51 | 1/15 |
52 | 1/21 |
53 | 1/10 |
54 | 410 |
55 | 930 |
56 | 85 |
57 | 95 |
58 | 240 |
59 | 16 |
60 | >=0.1 |
61 |
62 |
63 | | Espresso |
64 | espresso |
65 | 1/2 |
66 | 1/2.5 |
67 | 1/1.5 |
68 | 180 |
69 | 380 |
70 | 85 |
71 | 95 |
72 | 36 |
73 | 18 |
74 | >=0.1 |
75 |
76 |
77 | | French press |
78 | french-press |
79 | 1/15 |
80 | 1/18 |
81 | 1/12 |
82 | 690 |
83 | 1300 |
84 | 85 |
85 | 95 |
86 | 120 |
87 | 8 |
88 | >=0.1 |
89 |
90 |
91 | | Siphon |
92 | siphon |
93 | 1/15 |
94 | 1/16 |
95 | 1/12 |
96 | 375 |
97 | 800 |
98 | 91 |
99 | 94 |
100 | 240 |
101 | 16 |
102 | >=0.1 |
103 |
104 |
105 | | Pour-over |
106 | pour-over |
107 | 1/15 |
108 | 1/16 |
109 | 1/14 |
110 | 410 |
111 | 930 |
112 | 90 |
113 | 93 |
114 | 240 |
115 | 16 |
116 | >=0.2 |
117 |
118 |
119 | | Auto drip |
120 | auto-drip |
121 | 1/16 |
122 | 1/17 |
123 | 1/14 |
124 | 300 |
125 | 900 |
126 | 90 |
127 | 96 |
128 | 128 |
129 | 8 |
130 | >=0.2 |
131 |
132 |
133 | | Cold brew |
134 | cold-brew |
135 | 1/11 |
136 | 1/15 |
137 | 1/8 |
138 | 800 |
139 | 1400 |
140 | 0 |
141 | 40 |
142 | 242 |
143 | 22 |
144 | >=0.2 |
145 |
146 |
147 | | Cold brew concentrate |
148 | cold-brew-conc |
149 | 1/5 |
150 | 1/6 |
151 | 1/4 |
152 | 800 |
153 | 1400 |
154 | 0 |
155 | 40 |
156 | 120 |
157 | 24 |
158 | >=0.2 |
159 |
160 |
161 | | Moka pot |
162 | moka-pot |
163 | 1/10 |
164 | 1/12 |
165 | 1/7 |
166 | 360 |
167 | 660 |
168 | 85 |
169 | 95 |
170 | 60 |
171 | 6 |
172 | >=0.2 |
173 |
174 |
175 | | Ristretto |
176 | ristretto |
177 | 1/1 |
178 | 1/1.5 |
179 | 1/1 |
180 | 180 |
181 | 380 |
182 | 85 |
183 | 95 |
184 | 18 |
185 | 18 |
186 | >=0.3 |
187 |
188 |
189 | | Lungo |
190 | lungo |
191 | 1/4 |
192 | 1/4 |
193 | 1/2.5 |
194 | 180 |
195 | 380 |
196 | 85 |
197 | 95 |
198 | 72 |
199 | 18 |
200 | >=0.3 |
201 |
202 |
203 | | Turkish |
204 | turkish |
205 | 1/10 |
206 | 1/12 |
207 | 1/8 |
208 | 40 |
209 | 220 |
210 | 90 |
211 | 95 |
212 | 50 |
213 | 5 |
214 | >=0.3 |
215 |
216 |
217 | | Cupping |
218 | cupping |
219 | 11/200 |
220 | 1/19 |
221 | 1/17 |
222 | 460 |
223 | 850 |
224 | 85 |
225 | 95 |
226 | 150 |
227 | 8.25 |
228 | >=0.3 |
229 |
230 |
231 | | AeroPress standard |
232 | aero-press |
233 | 1/15 |
234 | 1/18 |
235 | 1/12 |
236 | 320 |
237 | 960 |
238 | 90 |
239 | 95 |
240 | 135 |
241 | 9 |
242 | >=0.4 |
243 |
244 |
245 | | AeroPress concentrate |
246 | aero-press-conc |
247 | 1/6 |
248 | 1/7 |
249 | 1/5 |
250 | 320 |
251 | 960 |
252 | 90 |
253 | 95 |
254 | 90 |
255 | 15 |
256 | >=0.4 |
257 |
258 |
259 | | AeroPress inverted |
260 | aero-press-inv |
261 | 1/12 |
262 | 1/14 |
263 | 1/10 |
264 | 320 |
265 | 960 |
266 | 90 |
267 | 95 |
268 | 132 |
269 | 11 |
270 | >=0.4 |
271 |
272 |
273 | | Steep-and-release |
274 | steep-and-release |
275 | 1/16 |
276 | 1/17 |
277 | 1/14 |
278 | 450 |
279 | 825 |
280 | 85 |
281 | 95 |
282 | 255 |
283 | 15.9375 |
284 | >=0.4 |
285 |
286 |
287 | | Clever dripper |
288 | clever-dripper |
289 | 1/16.67 |
290 | 1/20 |
291 | 1/15 |
292 | 400 |
293 | 800 |
294 | 91 |
295 | 96 |
296 | 250 |
297 | 14.997 |
298 | >=1.9 |
299 |
300 |
301 | | Phin filter |
302 | phin-filter |
303 | 1/2 |
304 | 1/4 |
305 | 1/2 |
306 | 200 |
307 | 400 |
308 | 90 |
309 | 96 |
310 | 72 |
311 | 36 |
312 | >=1.9 |
313 |
314 |
315 | | Kalita wave |
316 | kalita-wave |
317 | 1/16 |
318 | 1/17 |
319 | 1/15 |
320 | 800 |
321 | 1000 |
322 | 90 |
323 | 96 |
324 | 400 |
325 | 25 |
326 | >=2.1 |
327 |
328 |
329 | | Instant coffee |
330 | instant-coffee |
331 | 1/35 |
332 | 1/50 |
333 | 1/15 |
334 | -- |
335 | -- |
336 | 80 |
337 | 93 |
338 | 175 |
339 | 5 |
340 | >=2.1 |
341 |
342 |
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 |

6 |

7 |

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 | | PyPI Counter |
20 |  |
21 |
22 |
23 | | Github Stars |
24 |  |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | | Branch |
33 | main |
34 | dev |
35 |
36 |
37 | | CI |
38 |  |
39 |  |
40 |
41 |
42 |
43 |
44 |
45 |
46 | | Code Quality |
47 |  |
48 |  |
49 |
50 |
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 | | Parameter |
326 | Description |
327 | Type |
328 | Default |
329 |
330 |
331 |
332 |
333 | --mode |
334 | Specifies the conversion mode |
335 | String |
336 | water-to-coffee |
337 |
338 |
339 | --method |
340 | Specifies the coffee brewing method |
341 | String |
342 | custom |
343 |
344 |
345 | --water |
346 | Sets the amount of water in each cup |
347 | Positive float |
348 | 240 |
349 |
350 |
351 | --coffee |
352 | Sets the amount of coffee in each cup |
353 | Positive float |
354 | 14.118 |
355 |
356 |
357 | --cups |
358 | Indicates the number of cups |
359 | Positive integer |
360 | 1 |
361 |
362 |
363 | --grind |
364 | Grind size (um) |
365 | Positive integer |
366 | 700 |
367 |
368 |
369 | --temperature |
370 | Brewing temperature |
371 | Float |
372 | 90 |
373 |
374 |
375 | --coffee-ratio |
376 | Coefficient for the coffee component in the ratio |
377 | Positive float |
378 | 1 |
379 |
380 |
381 | --water-ratio |
382 | Coefficient for the water component in the ratio |
383 | Positive float |
384 | 17 |
385 |
386 |
387 | --message |
388 | Extra information about the brewing method |
389 | String |
390 | Custom brewing method |
391 |
392 |
393 | --digits |
394 | Number of digits up to which the result is rounded |
395 | Integer |
396 | 3 |
397 |
398 |
399 | --coffee-unit |
400 | Coffee unit |
401 | String |
402 | g |
403 |
404 |
405 | --water-unit |
406 | Water unit |
407 | String |
408 | g |
409 |
410 |
411 | --temperature-unit |
412 | Temperature unit |
413 | String |
414 | C |
415 |
416 |
417 | --save-path |
418 | File path to save the output |
419 | String |
420 | -- |
421 |
422 |
423 | --save-format |
424 | Format to save the output |
425 | String |
426 | text |
427 |
428 |
429 |
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 |