Add two new apps.

This commit is contained in:
Martin Fitzpatrick 2019-07-27 21:56:29 +02:00
parent 9e1cd8129a
commit ec275faf03
9 changed files with 470 additions and 1 deletions

View File

@ -0,0 +1,59 @@
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import os
app = QApplication([])
app.setQuitOnLastWindowClosed(False)
# Create the icon
icon = QIcon(os.path.join("images","color.png"))
clipboard = QApplication.clipboard()
dialog = QColorDialog()
def copy_color_hex():
if dialog.exec_():
color = dialog.currentColor()
clipboard.setText(color.name())
def copy_color_rgb():
if dialog.exec_():
color = dialog.currentColor()
clipboard.setText("rgb(%d, %d, %d)" % (
color.red(), color.green(), color.blue()
))
def copy_color_hsv():
if dialog.exec_():
color = dialog.currentColor()
clipboard.setText("hsv(%d, %d, %d)" % (
color.hue(), color.saturation(), color.value()
))
# Create the tray
tray = QSystemTrayIcon()
tray.setIcon(icon)
tray.setVisible(True)
# Create the menu
menu = QMenu()
action1 = QAction("Hex")
action1.triggered.connect(copy_color_hex)
menu.addAction(action1)
action2 = QAction("RGB")
action2.triggered.connect(copy_color_rgb)
menu.addAction(action2)
action3 = QAction("HSV")
action3.triggered.connect(copy_color_hsv)
menu.addAction(action3)
# Add the menu to the tray
tray.setContextMenu(menu)
app.exec_()

BIN
colorpicker/images/color.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

35
crypto/README.md Normal file
View File

@ -0,0 +1,35 @@
# Doughnut — An exchange rate tracker for people nuts about dough, in PyQt.
This is a simple currency exchange rate tracker implemented in PyQt, using the [fixer.io](http://fixer.io) API
for data. The default setup shows currency data for the preceding 180 days.
![Doughnut](screenshot-currency1.jpg)
Data is loaded progressively, with increasing resolution. Currency rates for a given date are shown in the right
hand panel and updated to follow the position of the mouse.
![Doughnut](screenshot-currency2.jpg)
> If you think this app is neat and want to learn more about
PyQt in general, take a look at my [free PyQt tutorials](https://www.learnpyqt.com)
which cover everything you need to know to start building your own applications with PyQt.
## Code notes
### Data handling
The interface presents a tracking plot (using PyQtGraph) of rates over the past 180 days. Since we don't want to
spam a free service, requests to the API are rate-limited to 1-per-second, giving a full-data-load time of 180s (3 min).
To avoid waiting each time, we use `requests_cache` which uses a local sqlite database to store the result of recent
requests. The requests for data use a progressive 'search' approach: where there is a gap in the data, the middle
point is filled first, and it prefers to load the most recent timepoints first. This means the whole plot gradually
increases in resolution over time, rather than working backwards only.
### Conversions
By default the app retrieves EUR rates and shows conversions to this base currency. If you change base currency
it will retrieve all data again for that new currency. This is daft, since if we have rates vs. EUR we can calculate
any other currency->currency conversion via EUR (with a small loss of accuracy).

370
crypto/crypto.py Normal file
View File

@ -0,0 +1,370 @@
from datetime import datetime, timedelta, date
from itertools import cycle
import os
import sys
import traceback
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import numpy as np
import pyqtgraph as pg
import requests
import requests_cache
# CryptoCompare.com API Key
CRYPTOCOMPARE_API_KEY = ''
# Define a requests http cache to minimise API requests.
requests_cache.install_cache(os.path.expanduser('~/.goodforbitcoin'))
# Base currency is used to retrieve rates from bitcoinaverage.
DEFAULT_BASE_CURRENCY = 'USD'
AVAILABLE_BASE_CURRENCIES = ['USD', 'EUR', 'GBP']
# The crypto currencies to retrieve data about.
AVAILABLE_CRYPTO_CURRENCIES = ['BTC', 'ETH', 'LTC', 'EOS', 'XRP', 'BCH' ] #
DEFAULT_DISPLAY_CURRENCIES = ['BTC', 'ETH', 'LTC']
# Number of historic timepoints to plot (days).
NUMBER_OF_TIMEPOINTS = 150
# Colour cycle to use for plotting currencies.
BREWER12PAIRED = cycle(['#a6cee3', '#1f78b4', '#b2df8a', '#33a02c', '#fb9a99', '#e31a1c', '#fdbf6f', '#ff7f00',
'#cab2d6', '#6a3d9a', '#ffff99', '#b15928' ])
# Base PyQtGraph configuration.
pg.setConfigOption('background', 'w')
pg.setConfigOption('foreground', 'k')
class WorkerSignals(QObject):
"""
Defines the signals available from a running worker thread.
"""
finished = pyqtSignal()
error = pyqtSignal(tuple)
progress = pyqtSignal(int)
data = pyqtSignal(dict, list)
cancel = pyqtSignal()
class UpdateWorker(QRunnable):
"""
Worker thread for updating currency.
"""
signals = WorkerSignals()
def __init__(self, base_currency):
super(UpdateWorker, self).__init__()
self.is_interrupted = False
self.base_currency = base_currency
self.signals.cancel.connect(self.cancel)
@pyqtSlot()
def run(self):
auth_header = {
'Apikey': CRYPTOCOMPARE_API_KEY
}
try:
rates = {}
for n, crypto in enumerate(AVAILABLE_CRYPTO_CURRENCIES, 1):
url = 'https://min-api.cryptocompare.com/data/histoday?fsym={fsym}&tsym={tsym}&limit={limit}'
r = requests.get(
url.format(**{
'fsym': crypto,
'tsym': self.base_currency,
'limit': NUMBER_OF_TIMEPOINTS-1,
'extraParams': 'www.learnpyqt.com',
'format': 'json',
}),
headers=auth_header,
)
r.raise_for_status()
rates[crypto] = r.json().get('Data')
self.signals.progress.emit(int(100 * n / len(AVAILABLE_CRYPTO_CURRENCIES)))
if self.is_interrupted:
# Stop without emitting finish signals.
return
url = 'https://min-api.cryptocompare.com/data/exchange/histoday?tsym={tsym}&limit={limit}'
r = requests.get(
url.format(**{
'tsym': self.base_currency,
'limit': NUMBER_OF_TIMEPOINTS-1,
'extraParams': 'www.learnpyqt.com',
'format': 'json',
}),
headers=auth_header,
)
r.raise_for_status()
volume = [d['volume'] for d in r.json().get('Data')]
except Exception as e:
self.signals.error.emit((e, traceback.format_exc()))
return
self.signals.data.emit(rates, volume)
self.signals.finished.emit()
def cancel(self):
self.is_interrupted = True
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
layout = QHBoxLayout()
self.ax = pg.PlotWidget()
self.ax.showGrid(True, True)
self.line = pg.InfiniteLine(
pos=-20,
pen=pg.mkPen('k', width=3),
movable=False # We have our own code to handle dragless moving.
)
self.ax.addItem(self.line)
self.ax.setLabel('left', text='Rate')
self.p1 = self.ax.getPlotItem()
self.p1.scene().sigMouseMoved.connect(self.mouse_move_handler)
# Add the right-hand axis for the market activity.
self.p2 = pg.ViewBox()
self.p2.enableAutoRange(axis=pg.ViewBox.XYAxes, enable=True)
self.p1.showAxis('right')
self.p1.scene().addItem(self.p2)
self.p2.setXLink(self.p1)
self.ax2 = self.p1.getAxis('right')
self.ax2.linkToView(self.p2)
self.ax2.setGrid(False)
self.ax2.setLabel(text='Volume')
self._market_activity = pg.PlotCurveItem(
np.arange(NUMBER_OF_TIMEPOINTS), np.arange(NUMBER_OF_TIMEPOINTS),
pen=pg.mkPen('k', style=Qt.DashLine, width=1)
)
self.p2.addItem(self._market_activity)
# Automatically rescale our twinned Y axis.
self.p1.vb.sigResized.connect(self.update_plot_scale)
self.base_currency = DEFAULT_BASE_CURRENCY
# Store a reference to lines on the plot, and items in our
# data viewer we can update rather than redraw.
self._data_lines = dict()
self._data_items = dict()
self._data_colors = dict()
self._data_visible = DEFAULT_DISPLAY_CURRENCIES
self.listView = QTableView()
self.model = QStandardItemModel()
self.model.setHorizontalHeaderLabels(["Currency", "Rate"])
self.model.itemChanged.connect(self.check_check_state)
self.listView.setModel(self.model)
self.threadpool = QThreadPool()
self.worker = False
layout.addWidget(self.ax)
layout.addWidget(self.listView)
widget = QWidget()
widget.setLayout(layout)
self.setCentralWidget(widget)
self.listView.setFixedSize(226, 400)
self.setFixedSize(650, 400)
toolbar = QToolBar("Main")
self.addToolBar(toolbar)
self.currencyList = QComboBox()
toolbar.addWidget(self.currencyList)
self.update_currency_list(AVAILABLE_BASE_CURRENCIES)
self.currencyList.setCurrentText(self.base_currency)
self.currencyList.currentTextChanged.connect(self.change_base_currency)
self.progress = QProgressBar()
self.progress.setRange(0, 100)
toolbar.addWidget(self.progress)
self.refresh_historic_rates()
self.setWindowTitle("Goodforbitcoin")
self.show()
def update_currency_list(self, currencies):
for currency in currencies:
self.currencyList.addItem(currency)
self.currencyList.model().sort(0)
def check_check_state(self, i):
if not i.isCheckable(): # Skip data columns.
return
currency = i.text()
checked = i.checkState() == Qt.Checked
if currency in self._data_visible:
if not checked:
self._data_visible.remove(currency)
self.redraw()
else:
if checked:
self._data_visible.append(currency)
self.redraw()
def get_currency_color(self, currency):
if currency not in self._data_colors:
self._data_colors[currency] = next(BREWER12PAIRED)
return self._data_colors[currency]
def get_or_create_data_row(self, currency):
if currency not in self._data_items:
self._data_items[currency] = self.add_data_row(currency)
return self._data_items[currency]
def add_data_row(self, currency):
citem = QStandardItem()
citem.setText(currency)
citem.setForeground(QBrush(QColor(
self.get_currency_color(currency)
)))
citem.setColumnCount(2)
citem.setCheckable(True)
if currency in DEFAULT_DISPLAY_CURRENCIES:
citem.setCheckState(Qt.Checked)
vitem = QStandardItem()
vitem.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
self.model.setColumnCount(2)
self.model.appendRow([citem, vitem])
self.model.sort(0)
return citem, vitem
def mouse_move_handler(self, pos):
pos = self.ax.getViewBox().mapSceneToView(pos)
self.line.setPos(pos.x())
self.update_data_viewer(int(pos.x()))
def update_data_viewer(self, i):
if i not in range(NUMBER_OF_TIMEPOINTS):
return
for currency, data in self.data.items():
self.update_data_row(currency, data[i])
def update_data_row(self, currency, data):
citem, vitem = self.get_or_create_data_row(currency)
vitem.setText("%.4f" % data['close'])
def change_base_currency(self, currency):
self.base_currency = currency
self.refresh_historic_rates()
def refresh_historic_rates(self):
if self.worker:
# If we have a current worker, send a kill signal
self.worker.signals.cancel.emit()
# Prefill our data store with None ('no data')
self.data = {}
self.volume = []
self.worker = UpdateWorker(self.base_currency)
# Handle callbacks with data and trigger refresh.
self.worker.signals.data.connect(self.result_data_callback)
self.worker.signals.finished.connect(self.refresh_finished)
self.worker.signals.progress.connect(self.progress_callback)
self.worker.signals.error.connect(self.notify_error)
self.threadpool.start(self.worker)
def result_data_callback(self, rates, volume):
self.data = rates
self.volume = volume
self.redraw()
self.update_data_viewer(NUMBER_OF_TIMEPOINTS-1)
def progress_callback(self, progress):
self.progress.setValue(progress)
def refresh_finished(self):
self.worker = False
self.redraw()
def notify_error(self, error):
e, tb = error
msg = QMessageBox()
msg.setIcon(QMessageBox.Warning)
msg.setText(e.__class__.__name__)
msg.setInformativeText(str(e))
msg.setDetailedText(tb)
msg.exec_()
def update_plot_scale(self):
self.p2.setGeometry(self.p1.vb.sceneBoundingRect())
def redraw(self):
y_min, y_max = sys.maxsize, 0
x = np.arange(NUMBER_OF_TIMEPOINTS)
# Pre-process data into lists of x, y values.
for currency, data in self.data.items():
if data:
_, close, high, low = zip(*[
(v['time'], v['close'], v['high'], v['low'])
for v in data
])
if currency in self._data_visible:
# This line should be visible, if it's not drawn draw it.
if currency not in self._data_lines:
self._data_lines[currency] = {}
self._data_lines[currency]['high'] = self.ax.plot(
x, high, # Unpack a list of tuples into two lists, passed as individual args.
pen=pg.mkPen(self.get_currency_color(currency), width=2, style=Qt.DotLine)
)
self._data_lines[currency]['low'] = self.ax.plot(
x, low, # Unpack a list of tuples into two lists, passed as individual args.
pen=pg.mkPen(self.get_currency_color(currency), width=2, style=Qt.DotLine)
)
self._data_lines[currency]['close'] = self.ax.plot(
x, close, # Unpack a list of tuples into two lists, passed as individual args.
pen=pg.mkPen(self.get_currency_color(currency), width=3)
)
else:
self._data_lines[currency]['high'].setData(x, high)
self._data_lines[currency]['low'].setData(x, low)
self._data_lines[currency]['close'].setData(x, close)
y_min, y_max = min(y_min, *low), max(y_max, *high)
else:
# This line should not be visible, if it is delete it.
if currency in self._data_lines:
self._data_lines[currency]['high'].clear()
self._data_lines[currency]['low'].clear()
self._data_lines[currency]['close'].clear()
self.ax.setLimits(yMin=y_min * 0.9, yMax=y_max * 1.1, xMin=min(x), xMax=max(x))
self._market_activity.setData(x, self.volume)
self.p2.setYRange(0, max(self.volume))
if __name__ == '__main__':
app = QApplication([])
window = MainWindow()
app.exec_()

5
crypto/requirements.txt Normal file
View File

@ -0,0 +1,5 @@
PyQt5>=5.6
sip
requests>=2.0.0
requests_cache>=0.4.13
pyqtgraph>=0.10

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

BIN
crypto/resources/icon.icns Normal file

Binary file not shown.

BIN
crypto/resources/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -14,7 +14,7 @@ import sys
import time
import traceback
requests_cache.install_cache('fixerio_cache')
requests_cache.install_cache('http_cache')
# Base currency is used to retrieve rates from fixer.io.
# If we change currency we re-request, though it would