diff --git a/.github/workflows/pythonpublish.yml b/.github/workflows/publish.yml similarity index 100% rename from .github/workflows/pythonpublish.yml rename to .github/workflows/publish.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..f859a5b --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,46 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: CI + +on: [push, pull_request] + +jobs: + build: + + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macOS-latest] + python-version: [3.5, 3.6, 3.7, 3.8, 3.9] + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install requirements + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: First test + run: | + python setup.py install + python -m nafas test + - name: Install dev-requirements + run: | + pip install --upgrade --upgrade-strategy=only-if-needed -r dev-requirements.txt + - name: Version check + run: | + python otherfiles/version_check.py + - name: Test with pytest + run: | + python -m pytest test --cov=nafas --cov-report=term + - name: Other tests + run: | + python -m vulture nafas/ setup.py --min-confidence 65 --exclude=__init__.py --sort-by-size + python -m bandit -r nafas -s B311 + python -m pydocstyle --match-dir=nafas -v + - name: Codecov + run: | + codecov diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 11766d5..0000000 --- a/.travis.yml +++ /dev/null @@ -1,45 +0,0 @@ -language: python - -matrix: - include: - - os: linux - python: 3.8 - dist: xenial - - os: linux - python: 3.7 - dist: xenial - - os: linux - python: 3.6 - - os: linux - python: 3.5 - - os: osx - language: generic - env: TOXENV=py36 - - os: linux - python: 3.7-dev - dist: xenial - - os: linux - python: 3.8-dev - dist: xenial - - os: linux - python: 3.9-dev - dist: xenial - - os: linux - python: nightly - dist: xenial - allow_failures: - - os: linux - python: 3.9-dev - dist: xenial - - os: linux - python: nightly - dist: xenial -install: - - chmod +x .travis/install.sh - - .travis/install.sh -before_script: - - chmod +x .travis/test.sh -script: - - .travis/test.sh -after_success: - - codecov \ No newline at end of file diff --git a/.travis/install.sh b/.travis/install.sh deleted file mode 100644 index 89f741e..0000000 --- a/.travis/install.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash - set -e - set -x - - PYTHON_COMMAND=python - PIP_COMMAND=pip - if [ "$TRAVIS_OS_NAME" == "osx" ] - then - PYTHON_COMMAND=python3 - PIP_COMMAND=pip3 - fi - - $PIP_COMMAND install -r requirements.txt - $PYTHON_COMMAND setup.py install - $PYTHON_COMMAND -m nafas test - - if [ "$TRAVIS_OS_NAME" == "osx" ] - then - $PIP_COMMAND install --upgrade --upgrade-strategy=only-if-needed -r dev-requirements.txt --user - else - $PIP_COMMAND install --upgrade --upgrade-strategy=only-if-needed -r dev-requirements.txt - fi - - diff --git a/.travis/test.sh b/.travis/test.sh deleted file mode 100644 index fe3fd85..0000000 --- a/.travis/test.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash - set -e - set -x - IS_IN_TRAVIS=false - PYTHON_COMMAND=python - - if [ "$TRAVIS_OS_NAME" == "osx" ] - then - PYTHON_COMMAND=python3 - fi - $PYTHON_COMMAND -m pytest test --cov=nafas --cov-report=term - $PYTHON_COMMAND otherfiles/version_check.py - - if [ "$CI" = 'true' ] && [ "$TRAVIS" = 'true' ] - then - IS_IN_TRAVIS=true - fi - - if [ "$IS_IN_TRAVIS" = 'false' ] || [ "$TRAVIS_PYTHON_VERSION" = '3.6' ] - then - $PYTHON_COMMAND -m vulture nafas/ setup.py --min-confidence 65 --exclude=__init__.py --sort-by-size - $PYTHON_COMMAND -m bandit -r nafas -s B311 - $PYTHON_COMMAND -m pydocstyle --match-dir=nafas -v - fi - diff --git a/CHANGELOG.md b/CHANGELOG.md index 85b703e..06daa39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,24 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [0.2] - 2021-01-29 +### Added +- `_playsound_async` function +- `play_sound` function +- `inhale.wav` +- `exhale.wav` +- `sustain.wav` +- `retain.wav` +- `get_sound_path` function +- `program_description_print` function +- `time_convert` function +### Changed +- Menu optimized +- Test system modified +- `get_program_dict` function renamed to `get_program_data` +- `program_dict` parameter renamed to `program_data` +- `input_dict` parameter renamed to `input_data` +- `README.md` updated ## [0.1] - 2020-10-30 ### Added - Clear Mind program @@ -15,7 +33,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Anti-Appetite program - Cigarette Replace program -[Unreleased]: https://github.com/sepandhaghighi/nafas/compare/v0.1...dev +[Unreleased]: https://github.com/sepandhaghighi/nafas/compare/v0.2...dev +[0.2]: https://github.com/sepandhaghighi/nafas/compare/v0.1...v0.2 [0.1]: https://github.com/sepandhaghighi/nafas/compare/c58087a...v0.1 diff --git a/MANIFEST.in b/MANIFEST.in index 7861065..cb97552 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,4 +3,5 @@ include *.md include *.spec include *.txt include *.yml -include *.ini \ No newline at end of file +include *.ini +include nafas/sounds/*.wav \ No newline at end of file diff --git a/NAFAS.spec b/NAFAS.spec index c97a726..be36c6e 100644 --- a/NAFAS.spec +++ b/NAFAS.spec @@ -6,7 +6,7 @@ block_cipher = None a = Analysis(['nafas/__main__.py'], pathex=['nafas'], binaries=[], - datas=[], + datas=[('nafas/sounds/*.wav', 'nafas/sounds')], hiddenimports=[], hookspath=[], runtime_hooks=[], diff --git a/README.md b/README.md index c8b78e1..2c281e6 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Breathing gymnastics is a system of breathing exercises that focuses on the trea Nafas is a collection of breathing gymnastics designed to reduce the exhaustion of long working hours. With multiple breathing patterns, Nafas helps you find your way to a detoxified energetic workday and also improves your concentration by increasing the oxygen level. No need to walk away to take a break, just sit comfortably, run Nafas and let the journey begin. +**Nafas** means breath in Persian.
dev | |||||
Travis | -- | - | |||
AppVeyor | -- | + | CI | ++ |
7- Martarelli, Daniele, Mario Cocchioni, Stefania Scuri, and Pierluigi Pompei. "Diaphragmatic breathing reduces exercise-induced oxidative stress." Evidence-Based Complementary and Alternative Medicine 2011 (2011).- +
8- Free Text-To-Speech and Text-to-MP3 for US English+ + ## Donate to our project diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 67fcd58..0000000 --- a/appveyor.yml +++ /dev/null @@ -1,44 +0,0 @@ -build: false - -environment: - matrix: - - - PYTHON: "C:\\Python35" - PYTHON_VERSION: "3.5.2" - PYTHON_ARCH: "32" - - PYTHON: "C:\\Python35" - PYTHON_VERSION: "3.5.2" - PYTHON_ARCH: "64" - - PYTHON: "C:\\Python37" - PYTHON_VERSION: "3.7.0" - PYTHON_ARCH: "64" - - PYTHON: "C:\\Python38" - PYTHON_VERSION: "3.8.0" - PYTHON_ARCH: "64" - - PYTHON: "C:\\Python38" - PYTHON_VERSION: "3.8.0" - PYTHON_ARCH: "32" - - PYTHON: "C:\\Python37" - PYTHON_VERSION: "3.7.0" - PYTHON_ARCH: "32" - - PYTHON: "C:\\Python36" - PYTHON_VERSION: "3.6.0" - PYTHON_ARCH: "64" - - PYTHON: "C:\\Python36" - PYTHON_VERSION: "3.6.0" - PYTHON_ARCH: "32" - - - -init: - - "ECHO %PYTHON% %PYTHON_VERSION% %PYTHON_ARCH%" - -install: - - "%PYTHON%/Scripts/pip.exe install -r requirements.txt" - - "%PYTHON%/python.exe setup.py install" - -test_script: - - "%PYTHON%/python.exe -m nafas test" - - "%PYTHON%/Scripts/pip.exe install -r dev-requirements.txt" - - "%PYTHON%/python.exe -m pytest test --cov=nafas --cov-report=term" - - "%PYTHON%/python.exe otherfiles/version_check.py" \ No newline at end of file diff --git a/dev-requirements.txt b/dev-requirements.txt index 4fcaff7..4ffbb1b 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,4 +1,5 @@ -art==4.9 +art==5.1 +playsound==1.2.2 codecov>=2.0.15 pytest>=4.3.1 pytest-cov>=2.6.1 diff --git a/nafas/__main__.py b/nafas/__main__.py index d820021..c0ed308 100644 --- a/nafas/__main__.py +++ b/nafas/__main__.py @@ -2,7 +2,7 @@ """nafas main.""" import sys -from nafas.functions import description_print, get_input_standard, input_filter, get_program_dict, run +from nafas.functions import description_print, get_input_standard, input_filter, get_program_data, program_description_print, run from nafas.params import NAFAS_VERSION from art import tprint @@ -14,5 +14,6 @@ if len(args) < 2: input_data = get_input_standard() filtered_data = input_filter(input_data) - program_data = get_program_dict(filtered_data) + program_name, level, program_data = get_program_data(filtered_data) + program_description_print(program_name, level, program_data) run(program_data) diff --git a/nafas/functions.py b/nafas/functions.py index 6f5f48f..a2d75a2 100644 --- a/nafas/functions.py +++ b/nafas/functions.py @@ -2,7 +2,10 @@ """nafas functions.""" import time -from nafas.params import DESCRIPTION, STANDARD_MENU, STEP_MAP, PROGRAMS +from nafas.params import DESCRIPTION, STANDARD_MENU, STEP_MAP, PROGRAMS, PROGRAM_DESCRIPTION, SOUND_MAP, SOUND_ERROR_MESSAGE +import playsound +import threading +import os def line(num=70, char="#"): @@ -18,6 +21,24 @@ def line(num=70, char="#"): print(num * char) +def time_convert(input_time): + """ + Convert input time from sec to MM,SS format. + + :param input_time: input time in sec + :type input_time: float + :return: converted time as str + """ + sec = float(input_time) + _days, sec = divmod(sec, 24 * 3600) + _hours, sec = divmod(sec, 3600) + minutes, sec = divmod(sec, 60) + return ", ".join([ + "{:02.0f} minutes".format(minutes), + "{:02.0f} seconds".format(sec), + ]) + + def left_justify(words, width): """ Left justify words. @@ -75,20 +96,56 @@ def description_print(): print("\n") -def input_filter(input_dict): +def program_description_print(program_name, level, program_data): + """ + Print program description. + + :param program_name: program name + :type program_name: str + :param level: program level + :type level: str + :param program_data: program data + :type program_data: dict + :return: None + """ + cycle = program_data["cycle"] + ratio = program_data["ratio"] + unit = program_data["unit"] + pre = program_data["pre"] + unit_time = 0 + sequence = [] + for index, item in enumerate(ratio): + unit_time += item * unit + if item != 0: + sequence.append(STEP_MAP[index]) + sequence = ", ".join(sequence) + total_time = (unit_time * cycle) + pre + line() + print( + PROGRAM_DESCRIPTION.format( + program_name, + level, + str(cycle), + time_convert(total_time), + sequence)) + line() + time.sleep(1) + + +def input_filter(input_data): """ Filter input data. - :param input_dict: input data - :type input_dict: dict + :param input_data: input data + :type input_data: dict :return: filtered data as dict """ - filtered_dict = input_dict.copy() - if filtered_dict["program"] not in STANDARD_MENU["program"].keys(): - filtered_dict["program"] = 1 - if filtered_dict["level"] not in STANDARD_MENU["level"].keys(): - filtered_dict["level"] = 1 - return filtered_dict + filtered_data = input_data.copy() + if filtered_data["program"] not in STANDARD_MENU["program"].keys(): + filtered_data["program"] = 1 + if filtered_data["level"] not in STANDARD_MENU["level"].keys(): + filtered_data["level"] = 1 + return filtered_data def get_input_standard(input_func=input): @@ -99,7 +156,7 @@ def get_input_standard(input_func=input): :type input_func : function object :return: input data as dict """ - input_dict = {"program": 1, "level": 1} + input_data = {"program": 1, "level": 1} for item in sorted(STANDARD_MENU.keys()): exit_flag = False sorted_list = sorted(list(STANDARD_MENU[item].keys())) @@ -108,24 +165,36 @@ def get_input_standard(input_func=input): print(str(i) + "- " + STANDARD_MENU[item][i]) while not exit_flag: try: - input_dict[item] = int(input_func("")) + input_data[item] = int(input_func("")) exit_flag = True except Exception: print("[Error] Bad Input!") - return input_dict + return input_data -def get_program_dict(input_dict): +def get_program_data(input_data): """ - Get program dictionary. + Get program data. - :param input_dict: input data - :type input_dict: dict - :return: program data as dict + :param input_data: input data + :type input_data: dict + :return: program name, level and program data as tuple """ - program_name = STANDARD_MENU["program"][input_dict["program"]] - level = STANDARD_MENU["level"][input_dict["level"]] - return PROGRAMS[program_name][level] + program_name = STANDARD_MENU["program"][input_data["program"]] + level = STANDARD_MENU["level"][input_data["level"]] + return program_name, level, PROGRAMS[program_name][level] + + +def get_sound_path(sound_name): + """ + Return sound path. + + :param sound_name: .wav sound name + :type sound_name: str + :return: direct path to sound + """ + cd, _ = os.path.split(__file__) + return os.path.join(cd, "sounds", sound_name) def graphic_counter(delay_time): @@ -146,18 +215,52 @@ def graphic_counter(delay_time): print() -def run(program_dict): +def _playsound_async(sound_path, debug): + """ + Play sound asynchronous in a thread. + + :param sound_path: sound path + :type sound_path: str + :param debug: debug mode flag + :type debug: bool + :return: None + """ + try: + playsound.playsound(sound_path) + except Exception: + if debug: + print(SOUND_ERROR_MESSAGE) + + +def play_sound(sound_path, debug=False): + """ + Play inputted sound file. + + :param sound_path: sound path + :type sound_path: str + :param debug: debug mode flag + :type debug: bool + :return: new thread as threading.Thread object + """ + new_thread = threading.Thread( + target=_playsound_async, args=( + sound_path, debug,), daemon=True) + new_thread.start() + return new_thread + + +def run(program_data): """ Run program. - :param program_dict: program data - :type program_dict: dict + :param program_data: program data + :type program_data: dict :return: None """ - cycle = program_dict["cycle"] - ratio = program_dict["ratio"] - unit = program_dict["unit"] - pre = program_dict["pre"] + cycle = program_data["cycle"] + ratio = program_data["ratio"] + unit = program_data["unit"] + pre = program_data["pre"] print("Preparing ", end="") graphic_counter(pre) line() @@ -171,13 +274,16 @@ def run(program_dict): time.sleep(unit / 2) for index, item in enumerate(ratio): if item != 0: + item_name = STEP_MAP[index] + sound_thread = play_sound(get_sound_path(SOUND_MAP[item_name])) print( "- " + - STEP_MAP[index] + + item_name + " for {0} sec".format( unit * item)) graphic_counter(item * unit) + sound_thread.join() time.sleep(1) line() print("End!") diff --git a/nafas/params.py b/nafas/params.py index 4a5d72a..cb54c47 100644 --- a/nafas/params.py +++ b/nafas/params.py @@ -8,7 +8,22 @@ No need to walk away to take a break, just sit comfortably, run Nafas and let the journey begin. """ -NAFAS_VERSION = "0.1" +NAFAS_VERSION = "0.2" + +SOUND_ERROR_MESSAGE = "ERROR : Unable to play sound." + +PROGRAM_DESCRIPTION = """Program Details : + +Name : {0} + +Level : {1} + +Number of Cycles : {2} + +Total Time : {3} + +Sequence : {4} +""" STANDARD_MENU = { "program": { @@ -172,3 +187,11 @@ "Harmony": {"Beginner": HARMONY_BEGINNER, "Medium": HARMONY_MEDIUM, "Advanced": HARMONY_ADVANCED}} + + +SOUND_MAP = { + "Inhale": "inhale.wav", + "Exhale": "exhale.wav", + "Retain": "retain.wav", + "Sustain": "sustain.wav" +} diff --git a/nafas/sounds/exhale.wav b/nafas/sounds/exhale.wav new file mode 100644 index 0000000..079c2a6 Binary files /dev/null and b/nafas/sounds/exhale.wav differ diff --git a/nafas/sounds/inhale.wav b/nafas/sounds/inhale.wav new file mode 100644 index 0000000..50c2484 Binary files /dev/null and b/nafas/sounds/inhale.wav differ diff --git a/nafas/sounds/retain.wav b/nafas/sounds/retain.wav new file mode 100644 index 0000000..fdac516 Binary files /dev/null and b/nafas/sounds/retain.wav differ diff --git a/nafas/sounds/sustain.wav b/nafas/sounds/sustain.wav new file mode 100644 index 0000000..d9b47d9 Binary files /dev/null and b/nafas/sounds/sustain.wav differ diff --git a/otherfiles/Version.rc b/otherfiles/Version.rc index 91f31fc..9432a49 100644 --- a/otherfiles/Version.rc +++ b/otherfiles/Version.rc @@ -1,7 +1,7 @@ VSVersionInfo( ffi=FixedFileInfo( - filevers=(0, 1, 0, 0), - prodvers=(0, 1, 0, 0), + filevers=(0, 2, 0, 0), + prodvers=(0, 2, 0, 0), mask=0x3f, flags=0x0, OS=0x40004, @@ -16,12 +16,12 @@ VSVersionInfo( u'040904B0', [StringStruct(u'CompanyName', u'Sepand Haghighi'), StringStruct(u'FileDescription', u'NAFAS.exe'), - StringStruct(u'FileVersion', u'0.1.0.0'), + StringStruct(u'FileVersion', u'0.2.0.0'), StringStruct(u'InternalName', u'NAFAS.exe'), - StringStruct(u'LegalCopyright', u'Copyright (c) 2020 Sepand Haghighi'), + StringStruct(u'LegalCopyright', u'Copyright (c) 2021 Sepand Haghighi'), StringStruct(u'OriginalFilename', u'NAFAS.exe'), StringStruct(u'ProductName', u'NAFAS'), - StringStruct(u'ProductVersion', u'0, 1, 0, 0')]) + StringStruct(u'ProductVersion', u'0, 2, 0, 0')]) ]), VarFileInfo([VarStruct(u'Translation', [1033, 1200])]) ] diff --git a/otherfiles/version_check.py b/otherfiles/version_check.py index 0c2f166..5eea010 100644 --- a/otherfiles/version_check.py +++ b/otherfiles/version_check.py @@ -4,7 +4,7 @@ import sys import codecs Failed = 0 -VERSION = "0.1" +VERSION = "0.2" VERSION_1 = VERSION.split(".")[0] VERSION_2 = str(int(float(VERSION) * 10 - int(VERSION_1) * 10)) diff --git a/requirements.txt b/requirements.txt index 9276530..bfb003f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ art>=1.8 - +playsound>=1.0.0 diff --git a/setup.py b/setup.py index fd9d904..6f4d6a3 100644 --- a/setup.py +++ b/setup.py @@ -29,14 +29,14 @@ def read_description(): setup( name='nafas', packages=['nafas'], - version='0.1', + version='0.2', description='Breathing gymnastics application', long_description=read_description(), long_description_content_type='text/markdown', author='Sepand Haghighi', author_email='sepand@pyrgg.ir', url='https://github.com/sepandhaghighi/nafas', - download_url='https://github.com/sepandhaghighi/nafas/tarball/v0.1', + download_url='https://github.com/sepandhaghighi/nafas/tarball/v0.2', keywords="python3 python breath breathing meditation", project_urls={ 'Source': 'https://github.com/sepandhaghighi/nafas', @@ -44,7 +44,7 @@ def read_description(): install_requires=get_requires(), python_requires='>=3.5', classifiers=[ - 'Development Status :: 1 - Planning', + 'Development Status :: 3 - Alpha', 'Intended Audience :: Developers', 'Natural Language :: English', 'License :: OSI Approved :: MIT License', diff --git a/test/test.py b/test/test.py index d7c33d3..8d6ecfd 100644 --- a/test/test.py +++ b/test/test.py @@ -42,27 +42,42 @@ True >>> input_data["level"] == 1 True ->>> program_data = get_program_dict({"program":1,"level":1}) +>>> program_name,level,program_data = get_program_data({"program":1,"level":1}) >>> program_data["pre"] == 3 True >>> program_data["unit"] == 3 True +>>> program_description_print("Clear Mind","Beginner",{"ratio": [1, 0, 3, 0], "unit": 3, "pre": 3, "cycle": 35}) +###################################################################### +Program Details : +