diff --git a/.gitignore b/.gitignore index 17da1fd..9fb491e 100644 --- a/.gitignore +++ b/.gitignore @@ -52,11 +52,19 @@ venv/ ENV/ env.bak/ venv.bak/ +.envrc # ide .vscode .idea +# poetry +/poetry.toml + # user -link.sh -info.plist.bak +/link.sh +/info.plist.bak +/*/.site-packages +/build/ +/requirements.txt +*.alfredworkflow diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 8c59060..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "alfred-workflow"] - path = alfred-workflow - url = https://github.com/bskim45/alfred-workflow.git diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..7f94c63 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,24 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.1.0 + hooks: + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace + - id: check-executables-have-shebangs + + - repo: https://gitlab.com/pycqa/flake8 + rev: 3.9.2 + hooks: + - id: flake8 + additional_dependencies: [flake8-bugbear] + + - repo: https://github.com/pycqa/isort + rev: 5.10.1 + hooks: + - id: isort + + - repo: https://github.com/psf/black + rev: 22.3.0 + hooks: + - id: black diff --git a/.versionrc.json b/.versionrc.json index 256fa8a..aae93d8 100644 --- a/.versionrc.json +++ b/.versionrc.json @@ -13,7 +13,7 @@ } ], "scripts": { - "postbump": "sed -i.bak \"s#[0-9]*.[0-9]*.[0-9]*#$(cat version)#\" info.plist", + "postbump": "poetry version $(cat version) && sed -i.bak \"s#[0-9]*.[0-9]*.[0-9]*#$(cat version)#\" info.plist", "precommit": "git add info.plist" } diff --git a/LICENSE b/LICENSE old mode 100755 new mode 100644 diff --git a/Makefile b/Makefile index aadd6a4..b9da36a 100644 --- a/Makefile +++ b/Makefile @@ -1,24 +1,55 @@ NAME := alfred-coin-ticker WORKFLOW_FILENAME := $(NAME).alfredworkflow VERSION_FILE := version -TARGET_FILES := $(shell cat includes.list) +TARGET_FILES := $(shell cat include.list) -default: build +# must use system python +PYTHON := /usr/bin/python3 + +default: clean build + +.PHONY: .venv +.venv: + poetry env use $(PYTHON) + poetry install .PHONY: build build: $(WORKFLOW_FILENAME) -$(WORKFLOW_FILENAME): $(TARGET_FILES) +requirements.txt: + poetry export --without-hashes > requirements.txt + +%/.site-packages: + $(PYTHON) -m pip install \ + --prefer-binary \ + --upgrade \ + --target=$@ \ + ${PACKAGES} + +deps: requirements.txt +deps: PACKAGES=-r requirements.txt +deps: build/.site-packages +deps: $(TARGET_FILES) + +$(WORKFLOW_FILENAME): deps @echo "> Packaging..." - ./build.sh + rm -f $(WORKFLOW_FILENAME) + BUILD_DIR=$(BUILD_DIR) WORKFLOW_FILENAME=$(WORKFLOW_FILENAME) ./build.sh .PHONY: test test: python -m unittest discover -s tests -v +install: $(WORKFLOW_FILENAME) + open $(WORKFLOW_FILENAME) + .PHONY: clean clean: - rm -f $(WORKFLOW_FILENAME) + rm -rf \ + requirements.txt \ + ./build/ \ + .mypy_cache/ \ + $(WORKFLOW_FILENAME) .PHONY: bump_version bump_version: @@ -29,6 +60,7 @@ bump_version: @echo Current version: $(shell cat $(VERSION_FILE)) @(read -e -p "Bump to version $(version)? [y/N]: " ans && case "$$ans" in [yY]) true;; *) false;; esac) @echo $(version) > version + @potry version $(version) @sed -i.bak 's#[0-9]*.[0-9]*.[0-9]*#$(version)#' info.plist .PHONY: release @@ -46,3 +78,7 @@ release-minor: .PHONY: release-patch release-patch: @standard-version -a -s -t "" --release-as patch + +EXECUTABLES = $(PYTHON) plutil open rsync poetry +K := $(foreach exec,$(EXECUTABLES),\ + $(if $(shell which $(exec)),some string,$(error "No $(exec) in PATH"))) diff --git a/README.md b/README.md old mode 100755 new mode 100644 index bb11ff3..c8ee070 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Coin Ticker for Alfred Workflow -An [Alfred Workflow](http://www.alfredapp.com/) that provides the current price +An [Alfred Workflow](http://www.alfredapp.com/) that provides the current price and status about cryptocurrency from [cryptocompare.com]. Supports Alfred 3 and Alfred 4 on macOS 10.7+ (Python 2.7). @@ -52,8 +52,8 @@ Please use with caution. The code is released under the MIT license. See [LICENSE](LICENSE) for details. -Awesome [alfred-workflow](https://github.com/deanishe/alfred-workflow) library -by [@deanishe](https://github.com/deanishe) is also released under +Awesome [alfred-workflow](https://github.com/deanishe/alfred-workflow) library +by [@deanishe](https://github.com/deanishe) is also released under [MIT License](alfred-workflow/LICENCE.txt). [cryptocompare.com]: https://www.cryptocompare.com/ diff --git a/alfred-workflow b/alfred-workflow deleted file mode 160000 index 8110e88..0000000 --- a/alfred-workflow +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8110e885e1ac3028bfd82d19f3bd59de78752201 diff --git a/api.py b/api.py index 7c88350..2c97921 100755 --- a/api.py +++ b/api.py @@ -1,34 +1,35 @@ -#!/usr/bin/python +#!/usr/bin/env python3 # encoding: utf-8 # # Copyright (c) 2022 Bumsoo Kim # # MIT Licence http://opensource.org/licenses/MIT -from __future__ import print_function, unicode_literals +from __future__ import annotations import os from collections import namedtuple +import requests as requests + from utils import clean_price_string -from workflow import web class CoinTick(object): def __init__( self, - ticker, # type: unicode - symbol, # type: unicode - image_url, # type: unicode - fiat, # type: unicode - fiat_symbol, # type: unicode - price, # type: unicode - price_24h_high, # type: unicode - price_24h_low, # type: unicode - price_change_24h, # type: unicode - price_change_24h_percent, # type: unicode - total_volume_24h, # type: unicode - total_volume_24h_fiat, # type: unicode + ticker, # type: str + symbol, # type: str + image_url, # type: str + fiat, # type: str + fiat_symbol, # type: str + price, # type: str + price_24h_high, # type: str + price_24h_low, # type: str + price_change_24h, # type: str + price_change_24h_percent, # type: str + total_volume_24h, # type: str + total_volume_24h_fiat, # type: str ): self.ticker = ticker self.symbol = symbol @@ -47,32 +48,31 @@ def __eq__(self, o): if not isinstance(o, CoinTick): return False - return all(( - self.ticker == o.ticker, - self.symbol == o.symbol, - self.image_url == o.image_url, - self.fiat == o.fiat, - self.fiat_symbol == o.fiat_symbol, - self.price == o.price, - self.price_24h_high == o.price_24h_high, - self.price_24h_low == o.price_24h_low, - self.price_change_24h == o.price_change_24h, - self.price_change_24h_percent == o.price_change_24h_percent, - self.total_volume_24h == o.total_volume_24h, - self.total_volume_24h_fiat == o.total_volume_24h_fiat, - )) - - def __unicode__(self): - return '{0} {1} ({2}{3})'.format( - self.symbol, self.ticker, self.fiat_symbol, self.price) + return all( + ( + self.ticker == o.ticker, + self.symbol == o.symbol, + self.image_url == o.image_url, + self.fiat == o.fiat, + self.fiat_symbol == o.fiat_symbol, + self.price == o.price, + self.price_24h_high == o.price_24h_high, + self.price_24h_low == o.price_24h_low, + self.price_change_24h == o.price_change_24h, + self.price_change_24h_percent == o.price_change_24h_percent, + self.total_volume_24h == o.total_volume_24h, + self.total_volume_24h_fiat == o.total_volume_24h_fiat, + ) + ) def __str__(self): - return unicode(self).encode('utf-8') + return '{0} {1} ({2}{3})'.format( + self.symbol, self.ticker, self.fiat_symbol, self.price + ) CoinInfo = namedtuple( - 'CoinInfo', - ['name', 'ticker', 'symbol', 'image_url', 'url'] + 'CoinInfo', ['name', 'ticker', 'symbol', 'image_url', 'url'] ) @@ -84,31 +84,30 @@ class TickClient(object): @classmethod def get_cache_key(cls, query=None): - return '{0}-{1}'.format(cls.CACHE_KEY, query) \ - .replace(os.sep, '_') + return '{0}-{1}'.format(cls.CACHE_KEY, query).replace(os.sep, '_') def get(self, path, params): - # type: (unicode, dict) -> dict - r = web.get(self.API_BASE_URL + path, params) + # type: (str, dict) -> dict + r = requests.get(self.API_BASE_URL + path, params) r.raise_for_status() result = r.json() return result def get_coin_prices(self, tickers, fiat): - # type: (list[unicode], unicode) -> list[CoinTick] # noqa + # type: (list[str], str) -> list[CoinTick] # noqa raise NotImplementedError() def get_top_market_cap(self, limit, fiat): - # type: (int, unicode) -> list[CoinTick] # noqa + # type: (int, str) -> list[CoinTick] # noqa raise NotImplementedError() def get_coin_info(self, ticker): - # type: (unicode) -> CoinInfo # noqa + # type: (str) -> CoinInfo # noqa raise NotImplementedError() def get_ticker_web_url(self, ticker, fiat): - # type: (unicode, unicode) -> unicode + # type: (str, str) -> str raise NotImplementedError() @@ -168,9 +167,11 @@ def get_ticker_web_url(self, ticker, fiat): def coin_info_from_api_repr(cls, data): # type: (dict) -> CoinInfo return CoinInfo( - data['CoinName'], data['Symbol'], None, + data['CoinName'], + data['Symbol'], + None, cls.WEB_BASE_URL + data['ImageUrl'], - cls.WEB_BASE_URL + data['Url'] + cls.WEB_BASE_URL + data['Url'], ) @classmethod @@ -183,9 +184,11 @@ def tick_from_api_repr(cls, raw, display): fiat_symbol = display.get('TOSYMBOL') return CoinTick( - ticker, symbol, + ticker, + symbol, cls.WEB_BASE_URL + display.get('IMAGEURL'), - fiat, fiat_symbol, + fiat, + fiat_symbol, clean_price_string(fiat_symbol, display.get('PRICE')), clean_price_string(fiat_symbol, display.get('HIGH24HOUR')), clean_price_string(fiat_symbol, display.get('LOW24HOUR')), @@ -222,5 +225,5 @@ def get_coin_web_url(cls, coin_name): @staticmethod def normalize_to_underscore(name): - # type: (unicode) -> unicode + # type: (str) -> str return name.replace('.', '-').replace(' ', '-') diff --git a/build.sh b/build.sh index 1989695..5398204 100755 --- a/build.sh +++ b/build.sh @@ -1,25 +1,36 @@ #!/usr/bin/env bash - -WORKFLOW_NAME=alfred-coin-ticker.alfredworkflow +set -euo pipefail SCRIPT_HOME=$(dirname "$(realpath "$0")") VERSION=$(cat version) +: "${BUILD_DIR:="$SCRIPT_HOME/build"}" +: "${WORKFLOW_FILENAME:=alfred-coin-ticker.alfredworkflow}" echo "Home path: $SCRIPT_HOME" echo "Version: $VERSION" +echo "Build dir: $BUILD_DIR" +echo "Workflow: $WORKFLOW_FILENAME" + +#read -p "Continue? [y/N]" -n 1 -r +#echo # new line +#if [[ $REPLY =~ ^[Yy]$ ]]; then +# # pass +# echo "Building..." +#else +# echo "abort" +# exit 1 +#fi -cd "$SCRIPT_HOME/alfred-workflow/workflow" || exit -git clean -nx +rsync --archive --verbose \ + --filter '- *.pyc' \ + --filter '- *.egg-info' \ + --filter '- *.dist-info' \ + --filter '- __pycache__' \ + "$BUILD_DIR/.site-packages/" "$BUILD_DIR/" -read -p "Continue? [y/N]" -n 1 -r -echo # new line -if [[ $REPLY =~ ^[Yy]$ ]]; then - git clean -fx -else - echo "abort" - exit 1 -fi +# shellcheck disable=SC2046 +cp $(runningsubtext fetching... script - python main.py {query} + /usr/bin/env python3 main.py {query} scriptargtype 0 scriptfile diff --git a/main.py b/main.py index b955105..09ab624 100644 --- a/main.py +++ b/main.py @@ -1,50 +1,70 @@ -#!/usr/bin/python +#!/usr/bin/env python3 # encoding: utf-8 # # Copyright (c) 2022 Bumsoo Kim # # MIT Licence http://opensource.org/licenses/MIT -from __future__ import print_function, unicode_literals, division +from __future__ import annotations import multiprocessing import sys from multiprocessing.pool import ThreadPool -from api import CryptoCompareClient, CoinTick, CoinInfo, CoinMarketCapClient -from utils import create_workflow, cached_file, get_display_change_string, \ - cached_file_fresh -from workflow import Workflow3, ICON_INFO, ICON_WEB, web, \ - ICON_SETTINGS, ICON_ERROR, ICON_TRASH +import requests +from workflow import ( + ICON_ERROR, + ICON_INFO, + ICON_SETTINGS, + ICON_TRASH, + ICON_WEB, + Workflow3, +) + +from api import CoinInfo, CoinMarketCapClient, CoinTick, CryptoCompareClient +from utils import ( + cached_file, + cached_file_fresh, + create_workflow, + get_display_change_string, +) TICKER_CACHE_AGE_SECONDS = 14 * 24 * 60 * 60 # 2 weeks class Command(object): - def __init__(self, command_id, alfred_command, help_message, icon=None): - # type: (unicode, unicode, unicode, unicode) -> Command + def __init__( + self, + command_id: str, + alfred_command: str, + help_message: str, + icon: str = None, + ): self.command_id = command_id self.alfred_command = alfred_command self.help_message = help_message self.icon = icon - def is_command(self, command): - # type: (unicode) -> bool + def is_command(self, command: str) -> bool: return command == self.alfred_command CMD_LIST_FAVORITES = Command('list_favorites', '', 'List favorite coins.') -CMD_LIST_RANKINGS = Command('list_rankings', 'list', - 'Toplist by Market Cap.') -CMD_SET_CURRENCY = Command('set_currency', 'set currency', 'Set currency', - ICON_SETTINGS) +CMD_LIST_RANKINGS = Command('list_rankings', 'list', 'Toplist by Market Cap.') +CMD_SET_CURRENCY = Command( + 'set_currency', 'set currency', 'Set currency', ICON_SETTINGS +) CMD_HELP = Command('show_help', 'help', 'Show help', ICON_INFO) -CMD_RESET = Command('reset', 'reset', - 'Reset all settings and delete all caches/data.', - ICON_TRASH) +CMD_RESET = Command( + 'reset', + 'reset', + 'Reset all settings and delete all caches/data.', + ICON_TRASH, +) CMD_ADD_COIN = Command('add_coin', 'add', 'Add new coin to the favorites.') -CMD_REMOVE_COIN = Command('remove_coin', 'remove', - 'Remove a coin from the favorites.') +CMD_REMOVE_COIN = Command( + 'remove_coin', 'remove', 'Remove a coin from the favorites.' +) COMMAND_LIST = [ CMD_LIST_FAVORITES, @@ -91,10 +111,10 @@ def get_info(ticker): def get_coin_image_multi(ticker_and_image_urls): - # type: (list[tuple[unicode, unicode]]) -> list[tuple[unicode, str]] # noqa + # type: (list[tuple[str, str]]) -> list[tuple[str, str]] # noqa def get_image(ticker_and_image_url): ticker, image_url = ticker_and_image_url - return ticker, web.get(image_url).content + return ticker, requests.get(image_url).content pool = ThreadPool(multiprocessing.cpu_count() // 2) ticker_and_images = pool.map(get_image, ticker_and_image_urls) @@ -102,13 +122,13 @@ def get_image(ticker_and_image_url): def get_coin_info_from_tickers(wf, tickers): - # type: (Workflow3, list[unicode]) -> dict[unicode, CoinInfo] # noqa + # type: (Workflow3, list[str]) -> dict[str, CoinInfo] # noqa ticker_map = {} unknown_tickers = [] - def get_cache_key(ticker): - return '{0}_info'.format(ticker.lower()).encode('utf-8') + def get_cache_key(ticker) -> str: + return '{0}_info'.format(ticker.lower()) for ticker in tickers: is_cached = wf.cached_data_fresh( @@ -116,8 +136,9 @@ def get_cache_key(ticker): ) # type: CoinInfo if is_cached: - ticker_info = wf.cached_data(get_cache_key(ticker), None, - TICKER_CACHE_AGE_SECONDS) + ticker_info = wf.cached_data( + get_cache_key(ticker), None, TICKER_CACHE_AGE_SECONDS + ) ticker_map[ticker] = ticker_info else: unknown_tickers.append(ticker) @@ -127,42 +148,48 @@ def get_cache_key(ticker): for ticker_info in unknown_coin_info_list: # replace web link to coinmarketcap ticker_info = ticker_info._replace( # noqa - url=CoinMarketCapClient.get_coin_web_url( - ticker_info.name.lower()) + url=CoinMarketCapClient.get_coin_web_url(ticker_info.name.lower()) + ) + wf.cached_data( + get_cache_key(ticker_info.ticker), + lambda: ticker_info, + TICKER_CACHE_AGE_SECONDS, ) - wf.cached_data(get_cache_key(ticker_info.ticker), lambda: ticker_info, - TICKER_CACHE_AGE_SECONDS) ticker_map[ticker_info.ticker] = ticker_info return ticker_map def get_coin_image_from_tickers(wf, ticker_and_image_list): - # type: (Workflow3, list[tuple[unicode, unicode]]) -> dict[unicode, unicode] # noqa + # type: (Workflow3, list[tuple[str, str]]) -> dict[str, str] # noqa ticker_image_path_map = {} unknown_ticker_and_image_urls = [] def get_cache_key(ticker): - return '{0}_image'.format(ticker.lower()).encode('utf-8') + return '{0}_image'.format(ticker.lower()) for ticker, image_url in ticker_and_image_list: - is_cached = cached_file_fresh(wf, get_cache_key(ticker), - TICKER_CACHE_AGE_SECONDS) + is_cached = cached_file_fresh( + wf, get_cache_key(ticker), TICKER_CACHE_AGE_SECONDS + ) if is_cached: - image_path = cached_file(wf, get_cache_key(ticker), None, - TICKER_CACHE_AGE_SECONDS) + image_path = cached_file( + wf, get_cache_key(ticker), None, TICKER_CACHE_AGE_SECONDS + ) ticker_image_path_map[ticker] = image_path else: unknown_ticker_and_image_urls.append((ticker, image_url)) unknown_ticker_image_list = get_coin_image_multi( - unknown_ticker_and_image_urls) + unknown_ticker_and_image_urls + ) for ticker, image in unknown_ticker_image_list: - image_path = cached_file(wf, get_cache_key(ticker), lambda: image, - TICKER_CACHE_AGE_SECONDS) + image_path = cached_file( + wf, get_cache_key(ticker), lambda: image, TICKER_CACHE_AGE_SECONDS + ) ticker_image_path_map[ticker] = image_path return ticker_image_path_map @@ -181,25 +208,29 @@ def add_ticks_to_workflow(wf, ticks): for tick in ticks: item = wf.add_item( - title='{ticker:<5}\t{fiat}{price:10}\t({change_pct}%) \t{symbol} {volume}'.format( - # noqa - ticker=tick.ticker, fiat=tick.fiat_symbol, price=tick.price, + title='{ticker:<5}\t{fiat}{price:10}\t({change_pct}%) \t{symbol} {volume}'.format( # noqa pylint: disable=line-too-long + ticker=tick.ticker, + fiat=tick.fiat_symbol, + price=tick.price, change_pct=get_display_change_string( - tick.price_change_24h_percent), + tick.price_change_24h_percent + ), symbol=tick.symbol, volume=tick.total_volume_24h, ), subtitle='24h High {0}{1} | Low {0}{2} | Change {3}'.format( tick.fiat_symbol, - tick.price_24h_high, tick.price_24h_low, + tick.price_24h_high, + tick.price_24h_low, get_display_change_string(tick.price_change_24h), ), arg=coin_info_map[tick.ticker].url, valid=True, icon=coin_image_path_map[tick.ticker], ) - item.add_modifier('cmd', 'Copy ticker price', - arg=tick.price.replace(',', '')) + item.add_modifier( + 'cmd', 'Copy ticker price', arg=tick.price.replace(',', '') + ) item.add_modifier('alt', 'Copy ticker', arg=tick.ticker) return len(ticks) > 0 @@ -216,12 +247,14 @@ def _get(): add_ticks_to_workflow( wf, - wf.cached_data(b'market_cap_rankings_10', _get, max_age=3, session=True) + wf.cached_data( + 'market_cap_rankings_10', _get, max_age=3, session=True + ), ) def list_tickers(wf, tickers): - # type: (Workflow3, list[unicode]) -> () # noqa + # type: (Workflow3, list[str]) -> () # noqa if not tickers: return @@ -233,8 +266,12 @@ def _get(): is_not_empty = add_ticks_to_workflow( wf, - wf.cached_data(b'tickers_{0}'.format('_'.join(tickers)), _get, - max_age=3, session=True), + wf.cached_data( + 'tickers_{0}'.format('_'.join(tickers)), + _get, + max_age=3, + session=True, + ), ) return is_not_empty @@ -284,10 +321,10 @@ def show_add_favorites_help(wf): def add_favorites_prompt(wf, ticker, position_str=None): - # type: (Workflow3, unicode, Optional[unicode]) -> () # noqa + # type: (Workflow3, str, Optional[str]) -> () # noqa ticker = ticker.upper() - favorites = wf.settings['favorites'] # type: list[unicode] # noqa + favorites = wf.settings['favorites'] # type: list[str] # noqa if ticker in favorites: wf.add_item( @@ -307,26 +344,26 @@ def add_favorites_prompt(wf, ticker, position_str=None): if position: wf.add_item( title="Add '{0}' to the favorites at position {1}.".format( - ticker, position), + ticker, position + ), subtitle='press ENTER to proceed', autocomplete='add_commit {0} {1}'.format(ticker, position), icon=ICON_INFO, ) else: wf.add_item( - title="Add '{0}' to the end of the favorites.".format( - ticker, position), + title="Add '{0}' to the end of the favorites.".format(ticker), subtitle='press ENTER to proceed', - autocomplete='add_commit {0}'.format(ticker, position), + autocomplete='add_commit {0}'.format(ticker), icon=ICON_INFO, ) def add_favorites(wf, ticker, position_str=None): - # type: (Workflow3, unicode, Optional[unicode]) -> () # noqa + # type: (Workflow3, str, Optional[str]) -> () # noqa ticker = ticker.upper() - favorites = wf.settings['favorites'] # type: list[unicode] # noqa + favorites = wf.settings['favorites'] # type: list[str] # noqa position = int(position_str) if position_str else None if position: @@ -346,10 +383,10 @@ def add_favorites(wf, ticker, position_str=None): def remove_favorites(wf, ticker): - # type: (Workflow3, unicode) -> () # noqa + # type: (Workflow3, str) -> () # noqa ticker = ticker.upper() - favorites = wf.settings['favorites'] # type: list[unicode] # noqa + favorites = wf.settings['favorites'] # type: list[str] # noqa if ticker in favorites: favorites.remove(ticker) @@ -365,7 +402,7 @@ def remove_favorites(wf, ticker): def set_currency(wf, fiat): - # type: (Workflow3, Optional[unicode]) -> () # noqa + # type: (Workflow3, Optional[str]) -> () # noqa if not fiat: wf.warn_empty( title='Set currency', @@ -377,8 +414,10 @@ def set_currency(wf, fiat): fiat = fiat.upper() if len(fiat) != 3: - wf.warn_empty(title="'{0}' is invalid currency.".format(fiat), - icon=ICON_ERROR) + wf.warn_empty( + title="'{0}' is invalid currency.".format(fiat), + icon=ICON_ERROR, + ) else: wf.settings['currency'] = fiat wf.add_item( @@ -499,10 +538,12 @@ def main(wf): show_default_items = True if wf.update_available: - wf.add_item('New version is available', - subtitle='Click to install the update', - autocomplete='workflow:update', - icon=ICON_INFO) + wf.add_item( + 'New version is available', + subtitle='Click to install the update', + autocomplete='workflow:update', + icon=ICON_INFO, + ) if show_default_items: add_default_item(wf) diff --git a/poetry.lock b/poetry.lock index 6d4f92c..71ddd00 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,11 +1,49 @@ [[package]] -name = "funcsigs" -version = "1.0.2" -description = "Python function signatures from PEP362 for Python 2.6, 2.7 and 3.2+" -category = "dev" +name = "Alfred-Workflow" +version = "1.40.0" +description = "Full-featured helper library for writing Alfred 2/3/4 workflows" +category = "main" +optional = false +python-versions = "*" +develop = false + +[package.dependencies] +requests = ">=2.25,<3" +six = "*" + +[package.source] +type = "git" +url = "https://github.com/NorthIsUp/alfred-workflow-py3" +reference = "master" +resolved_reference = "b1b76f025ce8317cac3de435836c7194f12964fe" + +[[package]] +name = "certifi" +version = "2021.10.8" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" optional = false python-versions = "*" +[[package]] +name = "charset-normalizer" +version = "2.0.12" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.5.0" + +[package.extras] +unicode_backport = ["unicodedata2"] + +[[package]] +name = "idna" +version = "3.3" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" + [[package]] name = "mock" version = "3.0.5" @@ -15,7 +53,6 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.dependencies] -funcsigs = {version = ">=1", markers = "python_version < \"3.3\""} six = "*" [package.extras] @@ -23,29 +60,77 @@ build = ["twine", "wheel", "blurb"] docs = ["sphinx"] test = ["pytest", "pytest-cov"] +[[package]] +name = "requests" +version = "2.27.1" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} +idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] + [[package]] name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +[[package]] +name = "urllib3" +version = "1.26.9" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + [metadata] lock-version = "1.1" -python-versions = "~2.7" -content-hash = "83d66b7817e8858047aa6e8ac2ffdbffea50014a72e2ec5fad80eaa86ba0cef5" +python-versions = "~3.8" +content-hash = "3a9365d0076e262323747b73cf2cfe786d3b39d4a305cae87db87e16bdc392c1" [metadata.files] -funcsigs = [ - {file = "funcsigs-1.0.2-py2.py3-none-any.whl", hash = "sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca"}, - {file = "funcsigs-1.0.2.tar.gz", hash = "sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50"}, +Alfred-Workflow = [] +certifi = [ + {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, + {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, +] +charset-normalizer = [ + {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, + {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, +] +idna = [ + {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, + {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, ] mock = [ {file = "mock-3.0.5-py2.py3-none-any.whl", hash = "sha256:d157e52d4e5b938c550f39eb2fd15610db062441a9c2747d3dbfa9298211d0f8"}, {file = "mock-3.0.5.tar.gz", hash = "sha256:83657d894c90d5681d62155c82bda9c1187827525880eda8ff5df4ec813437c3"}, ] +requests = [ + {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, + {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, +] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +urllib3 = [ + {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"}, + {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, +] diff --git a/pyproject.toml b/pyproject.toml index 3ad4b99..e154616 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,14 @@ [tool.poetry] name = "alfred-coin-ticker" -version = "1.0.0" +version = "1.1.0" description = "" authors = ["Bumsoo Kim "] license = "MIT" [tool.poetry.dependencies] -python = "~2.7" +python = "~3.8" +requests = "^2.27.1" +alfred-workflow = {git = "https://github.com/NorthIsUp/alfred-workflow-py3", rev = "master"} [tool.poetry.dev-dependencies] mock = "~3.0.5" @@ -14,3 +16,20 @@ mock = "~3.0.5" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" + +[tool.isort] +profile = "black" +line_length = 79 + +[tool.black] +line-length = 79 +skip-string-normalization = true + +[tool.pytest.ini_options] +addopts = "-p no:warnings" + +[tool.coverage.run] +parallel = true +omit = [ + "tests/*", +] diff --git a/tests/test_api_coinmarketcap.py b/tests/test_api_coinmarketcap.py index 1e8bb83..843823f 100644 --- a/tests/test_api_coinmarketcap.py +++ b/tests/test_api_coinmarketcap.py @@ -5,7 +5,7 @@ # # MIT Licence http://opensource.org/licenses/MIT -from __future__ import print_function, unicode_literals +from __future__ import annotations from unittest import TestCase diff --git a/tests/test_api_cointick.py b/tests/test_api_cointick.py index 799a982..5213175 100644 --- a/tests/test_api_cointick.py +++ b/tests/test_api_cointick.py @@ -5,7 +5,7 @@ # # MIT Licence http://opensource.org/licenses/MIT -from __future__ import print_function, unicode_literals +from __future__ import annotations from unittest import TestCase @@ -15,20 +15,32 @@ class CoinTickTest(TestCase): def test_operator_eq(self): a = CoinTick( - 'BTC', 'Ƀ', + 'BTC', + 'Ƀ', 'https://www.cryptocompare.com/media/37746251/btc.png', - 'USD', '$', - '43,022.2', '43,355.5', '41,761.2', - '-253.25', '-0.59', - '158.53 K', '6.81 B', + 'USD', + '$', + '43,022.2', + '43,355.5', + '41,761.2', + '-253.25', + '-0.59', + '158.53 K', + '6.81 B', ) b = CoinTick( - 'BTC', 'Ƀ', + 'BTC', + 'Ƀ', 'https://www.cryptocompare.com/media/37746251/btc.png', - 'USD', '$', - '43,022.2', '43,355.5', '41,761.2', - '-253.25', '-0.59', - '158.53 K', '6.81 B', + 'USD', + '$', + '43,022.2', + '43,355.5', + '41,761.2', + '-253.25', + '-0.59', + '158.53 K', + '6.81 B', ) self.assertEqual(a, b) diff --git a/tests/test_api_cryptocompare.py b/tests/test_api_cryptocompare.py index 2249f0f..9a8a702 100644 --- a/tests/test_api_cryptocompare.py +++ b/tests/test_api_cryptocompare.py @@ -5,14 +5,14 @@ # # MIT Licence http://opensource.org/licenses/MIT -from __future__ import print_function, unicode_literals +from __future__ import annotations import json from unittest import TestCase import mock -from api import CryptoCompareClient, CoinTick, CoinInfo +from api import CoinInfo, CoinTick, CryptoCompareClient def _load_example(path): @@ -24,12 +24,15 @@ def _load_example(path): class CryptoCompareClientTest(TestCase): def setUp(self): - self.example_market_cap = \ - _load_example('examples/cryptocompare/mktcapfull_10.json') - self.example_tick_prices_btc_eth = \ - _load_example('examples/cryptocompare/pricemultifull_btceth.json') - self.example_coinlist_eth = \ - _load_example('examples/cryptocompare/coinlist_eth.json') + self.example_market_cap = _load_example( + 'examples/cryptocompare/mktcapfull_10.json' + ) + self.example_tick_prices_btc_eth = _load_example( + 'examples/cryptocompare/pricemultifull_btceth.json' + ) + self.example_coinlist_eth = _load_example( + 'examples/cryptocompare/coinlist_eth.json' + ) @mock.patch('api.web.get', autospec=True) def test_get_tick_prices( @@ -47,21 +50,33 @@ def test_get_tick_prices( expected = [ CoinTick( - 'BTC', 'Ƀ', + 'BTC', + 'Ƀ', 'https://www.cryptocompare.com/media/37746251/btc.png', - 'USD', '$', - '43,022.2', '43,355.5', '41,761.2', - '-253.25', '-0.59', - '158.53 K', '6.81 B', + 'USD', + '$', + '43,022.2', + '43,355.5', + '41,761.2', + '-253.25', + '-0.59', + '158.53 K', + '6.81 B', ), CoinTick( - 'ETH', 'Ξ', + 'ETH', + 'Ξ', 'https://www.cryptocompare.com/media/37746238/eth.png', - 'USD', '$', - '3,271.17', '3,313.47', '3,189.17', - '-31.25', '-0.95', - '1.37 M', '4.49 B', - ) + 'USD', + '$', + '3,271.17', + '3,313.47', + '3,189.17', + '-31.25', + '-0.95', + '1.37 M', + '4.49 B', + ), ] self.assertListEqual(res, expected) @@ -93,8 +108,10 @@ def test_get_coin_info( res = api.get_coin_info('ETH') expected = CoinInfo( - 'Ethereum', 'ETH', None, + 'Ethereum', + 'ETH', + None, 'https://www.cryptocompare.com/media/37746238/eth.png', - 'https://www.cryptocompare.com/coins/eth/overview' + 'https://www.cryptocompare.com/coins/eth/overview', ) self.assertEqual(res, expected) diff --git a/tests/test_utils.py b/tests/test_utils.py index 87744d8..c898e26 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -5,7 +5,7 @@ # # MIT Licence http://opensource.org/licenses/MIT -from __future__ import print_function, unicode_literals +from __future__ import annotations from unittest import TestCase diff --git a/utils.py b/utils.py index 26cd5f8..2e41402 100755 --- a/utils.py +++ b/utils.py @@ -1,11 +1,11 @@ -#!/usr/bin/python +#!/usr/bin/env python3 # encoding: utf-8 # # Copyright (c) 2022 Bumsoo Kim # # MIT Licence http://opensource.org/licenses/MIT -from __future__ import print_function, unicode_literals +from __future__ import annotations import os import time @@ -20,21 +20,29 @@ def create_workflow(): default_settings={ 'currency': 'USD', 'favorites': [ - 'BTC', 'ETH', 'BNB', 'SOL', 'ADA', - 'XRP', 'LUNA', 'AVAX', 'MATIC', + 'BTC', + 'ETH', + 'BNB', + 'SOL', + 'ADA', + 'XRP', + 'LUNA', + 'AVAX', + 'MATIC', ], }, update_settings={ 'github_slug': 'bskim45/alfred-coin-ticker', 'frequency': 7, # once a week - }) + }, + ) wf.settings.save() return wf def clean_price_string(symbol, price_str): - # type: (unicode, unicode) -> unicode + # type: (str, str) -> str if not symbol or not price_str: return price_str @@ -42,7 +50,7 @@ def clean_price_string(symbol, price_str): def get_display_change_string(change_str): - # type: (unicode) -> unicode + # type: (str) -> str if not change_str.startswith('-') and not change_str.startswith('+'): return '📈' + change_str elif change_str.startswith('+'): @@ -57,7 +65,7 @@ def cached_file_age(wf, name): if not os.path.exists(cache_path): return 0 - return time.time() - os.stat(cache_path).st_mtime + return int(time.time() - os.stat(cache_path).st_mtime) def cached_file_fresh(wf, name, max_age): diff --git a/version b/version old mode 100755 new mode 100644 index 1cc5f65..9084fa2 --- a/version +++ b/version @@ -1 +1 @@ -1.1.0 \ No newline at end of file +1.1.0 diff --git a/workflow b/workflow deleted file mode 120000 index d09e423..0000000 --- a/workflow +++ /dev/null @@ -1 +0,0 @@ -alfred-workflow/workflow \ No newline at end of file