Codementor: Why is there an f before this string? An introduction to f-strings and string formatting
IslandT: Get a number of top coins by their total volume across all markets in the last 24 hours
In this chapter, we will continue to develop the cryptocurrency application by retrieving a number of top coins by their total volume across all markets in the last 24 hours.
We will perform these steps:
- Create a button which will call the search coin function to retrieve the above mentioned data.
- Create the search coin function to retrieve and to display the market data.
# get top 100 coins by their total volume across all markets in the last 24 hours action_coin_volume = tk.Button(selectorFrame, text="Trade Volume", command=search_coin, state=DISABLED) # button used to get the top currency/cryptocurrency pair data action_coin_volume.pack(side=LEFT)
def search_coin(): # display all market data of the currency/cryptocurrency pair sell_buy = '' try: currency_symbol = based.get() # get the currency symbol url = "https://min-api.cryptocompare.com/data/top/totalvolfull" data = {'tsym': currency_symbol, 'limit': 100} r = requests.get(url, params=data) exchange_rate_s = json.loads(json.dumps(r.json())) except: print("An exception occurred") count = 0 # used to control new line for each symbol for value in exchange_rate_s['Data']: # populate exchange rate string and the currency tuple for key, value1 in value.items(): for key2, value2 in value1.items(): if (type(value2) == dict): for key3, value3 in value2.items(): if (key3 == "IMAGEURL"): continue sell_buy += str(key3) + " : " + str(value3) + "\n" count += 1 if count == 2: sell_buy += "\n" count = 0 text_widget.delete('1.0', END) # clear all those previous text first s.set(sell_buy) text_widget.insert(INSERT, s.get()) # populate the text widget with new exchange rate data
The above function will retrieve the market data of the top 100 cryptocurrencies vs the selected currency within a period of 24 hours.

Below are the steps to display those data:
- Click on the Load data button to load the top currencies data to the currency’s combo box.
- Select the currency which you wish to compare from the combo box, in this example we select EURO.
- Click on the trade volume button to display the top 100 cryptocurrency market data vs the selected currency which is EUR0.
If you have missed out the previous chapter don’t forget to read them because the code in this chapter is interrelated with the previous one.
PSF GSoC students blogs: Coding period: week #9
This is a blog post about trying to clean up the code that I have written for a feature that had merged and marked as experimental.
What did I do this week?
For the past few weeks, I have been working on an interesting feature called `unshelve --continue`. My mentor was actively giving reviews on the old patch even after getting that merged. So, I had to send a follow-up stack following the merged patch. I started with a patch[1] to modify the help text on verbose mode to say the user why this feature is still experimental and it's problems. Then, I sent a new stack:
[2]: D6679unshelve: store information about interactive mode in shelvedstate
[3]: D6687unshelve: create a matcher only if required on creating unshelve ctx
[4]: D6694unshelve: fix bug on a partial unshelve with --continue
[5]: D6697cmdutil: add allowunfinished to prevent checkunfinished() on docommit()
[6]: D6685unshelve: changes how date is set on interactive mode
[7]: D6686unshelve: handle stripping changesets on interactive mode
[8]: D6683unshelve: unify logic around creating an unshelve changeset
[9]: D6699tests: add test for unshelve --interactive --keep
What is coming up next?
Since I have been working on `unshelve --interactive` for a long time, my priority should be this only. I will try to clean the code and modify the tests to account for all cases possible. If that is getting over on time, I will work on an interesting issue[10].
Did you get stuck anywhere?
As usual, I got problems in rebasing and evolving changesets, I had to make the DAG linear after every time after resolving enormous conflicts. But, I got used to it and it wasn't much a problem per se.
Catalin George Festila: Python 3.7.3 : About pytweening python module.
Doug Hellmann: New PyMOTW site logo
Continue reading"New PyMOTW site logo"
PSF GSoC students blogs: Check in: Week 9
1. What did you do this week?
This week I worked on the new Kurucz line list parser. Also, I finished the CMFGEN pipeline (merged PR #143).
NIST (PR #144) and Knox Long's recombination zeta (PR #145) are almost finished too.
2. What is coming up next?
I will continue working on the Kurucz parser.
3. Did you get stuck anywhere?
Kurucz parser is a really difficult one! It's taking me longer than I expected.
IslandT: Get a number of top coins by their market cap
In this chapter, we will get a number of top 100 coins by their market cap with python. If you have read the previous chapter of this cryptocurrency project then you will find out that in this chapter we have only changed the GET’s url content a little bit to achieve the above-mentioned outcome. Below is the url we need to use to retrieve the above-mentioned outcome.
url = "https://min-api.cryptocompare.com/data/top/mktcapfull" data = {'tsym': currency_symbol, 'limit': 100}
Besides that, we have also created a button to start the top_coin_by_cap method. Below is the full program up until now.
import json from tkinter import * import tkinter.ttk as tk import requests from Loader import Loader win = Tk() # Create tk instance win.title("Crypto Calculator") # Add a title win.resizable(0, 0) # Disable resizing the GUI win.configure(background='white') # change window background color selectorFrame = Frame(win, background="white") # create top frame to hold search button and combobox selectorFrame.pack(anchor="nw", pady=2, padx=10) currency_label = Label(selectorFrame, text="Select market / currency pair :", background="white") currency_label.pack(anchor="w") # create currency frame and text widget to display the incoming exchange rate data s = StringVar() # create string variable currencyFrame = Frame(win) currencyFrame.pack(side=TOP) currency = Label(currencyFrame) currency.pack() text_widget = Text(currency, fg='white', background='black') text_widget.pack() s.set("Click the find button below to load the crypto currency - currency exchange rate from CCCAGG market") text_widget.insert(END, s.get()) buttonFrame = Frame(win) # create a bottom frame to hold the load button buttonFrame.pack(side=BOTTOM, fill=X, pady=6) # Create a combo box for crypto currency crypto = tk.Combobox(buttonFrame) crypto.pack(side=LEFT, padx=3) # Create a combo box for markets market = tk.Combobox(selectorFrame) market.pack(side=LEFT, padx=3) # create loader object then load the data into the crypto and market combo box loader = Loader(crypto, market) loader.load_data() def top_coin_by_cap(): # Get a number of top 100 coins by their market cap sell_buy = '' try: currency_symbol = based.get() # get the currency symbol url = "https://min-api.cryptocompare.com/data/top/mktcapfull" data = {'tsym': currency_symbol, 'limit': 100} r = requests.get(url, params=data) exchange_rate_s = json.loads(json.dumps(r.json())) except: print("An exception occurred") count = 0 # used to control new line for each symbol for value in exchange_rate_s['Data']: # populate exchange rate string and the currency tuple for key, value1 in value.items(): for key2, value2 in value1.items(): if (type(value2) == dict): for key3, value3 in value2.items(): if (key3 == "IMAGEURL"): continue sell_buy += str(key3) + " : " + str(value3) + "\n" count += 1 if count == 2: sell_buy += "\n" count = 0 text_widget.delete('1.0', END) # clear all those previous text first s.set(sell_buy) text_widget.insert(INSERT, s.get()) # populate the text widget with new exchange rate data def search_coin(): # display all market data of the currency/cryptocurrency pair sell_buy = '' try: currency_symbol = based.get() # get the currency symbol url = "https://min-api.cryptocompare.com/data/top/totalvolfull" data = {'tsym': currency_symbol, 'limit': 100} r = requests.get(url, params=data) exchange_rate_s = json.loads(json.dumps(r.json())) except: print("An exception occurred") count = 0 # used to control new line for each symbol for value in exchange_rate_s['Data']: # populate exchange rate string and the currency tuple for key, value1 in value.items(): for key2, value2 in value1.items(): if (type(value2) == dict): for key3, value3 in value2.items(): if (key3 == "IMAGEURL"): continue sell_buy += str(key3) + " : " + str(value3) + "\n" count += 1 if count == 2: sell_buy += "\n" count = 0 text_widget.delete('1.0', END) # clear all those previous text first s.set(sell_buy) text_widget.insert(INSERT, s.get()) # populate the text widget with new exchange rate data def search_currency(): # display all market data of the cryptocurrency-currency pair sell_buy = '' try: base_crypto = crypto.get() # get the desired crypto currency currency_symbol = based.get() # get the currency symbol market_exchange = market.get() # get the market url = "https://min-api.cryptocompare.com/data/generateAvg" data = {'fsym': base_crypto, 'tsym': currency_symbol, 'e': market_exchange} r = requests.get(url, params=data) exchange_rate_s = json.loads(json.dumps(r.json())) except: print("An exception occurred") for key, value in exchange_rate_s.items(): # populate exchange rate string and the currency tuple for key1, value1 in value.items(): sell_buy += str(key1) + " : " + str(value1) + "\n" text_widget.delete('1.0', END) # clear all those previous text first s.set(sell_buy) text_widget.insert(INSERT, s.get()) # populate the text widget with new exchange rate data # Create currency combo box base_currency = StringVar() # create a string variable based = tk.Combobox(selectorFrame, textvariable=base_currency) based.pack(side=LEFT, padx=3) action_search = tk.Button(selectorFrame, text="Search", command=search_currency, state=DISABLED) # button used to get the cryptocurrency/currency pair exchange rate action_search.pack(side=LEFT) # get top 100 coins by their total volume across all markets in the last 24 hours action_coin_volume = tk.Button(selectorFrame, text="Trade Volume", command=search_coin, state=DISABLED) # button used to get the top currency/cryptocurrency pair data action_coin_volume.pack(side=LEFT) # get top 100 coins by their market cap action_coin_market_cap = tk.Button(selectorFrame, text="Market Cap", command=top_coin_by_cap, state=DISABLED) # button used to get the top currency/cryptocurrency pair data action_coin_market_cap.pack(side=LEFT) def get_exchange_rate(): # this method will display the incoming exchange rate data after the api called global exchange_rate global base_crypto # read the currency file c_string = '' with open('currency.txt') as fp: for currency in fp.readlines(): c_string += currency[:-1] + "," c_string = c_string[:-1] base_crypto = crypto.get() # get the desired crypto currency try: url = "https://min-api.cryptocompare.com/data/price" # url for API call data = {'fsym': base_crypto, 'tsyms': c_string} r = requests.get(url, params=data) exchange_rate_s = json.loads(json.dumps(r.json())) except: print("An exception occurred") curr1 = tuple() # the tuple which will be populated by currency sell_buy = '' for key, value in exchange_rate_s.items(): # populate exchange rate string and the currency tuple sell_buy += base_crypto + ":" + key + " " + str(value) + "\n" curr1 += (key,) # fill up combo boxes for both currency and cryptocurrency based['values'] = curr1 based.current(0) text_widget.delete('1.0', END) # clear all those previous text first s.set(sell_buy) text_widget.insert(INSERT, s.get()) # populate the text widget with new exchange rate data # enable all buttons action_search.config(state=NORMAL) action_coin_volume.config(state=NORMAL) action_coin_market_cap.config(state=NORMAL) action_vid = tk.Button(buttonFrame, text="Load", command=get_exchange_rate) # button used to load the exchange rate of currency pairs action_vid.pack() win.iconbitmap(r'ico.ico') win.mainloop()

This project will continue for a while and we might rearrange the button and combo box in the future to suit the layout of the user interface.
Learn PyQt: Building a Bitcoin market tracker with Python and Qt5
Goodforbitcoin is a simple cryptocurrency market-tracker. It displays daily market rates, including high, low and close valuations, alongside market trade volume for a range of popular cryptocurrencies. It comes with built-in support for BTC
, ETH
, LTC
, EOS
, XRP
and BCH
currencies, with EUR
, USD
and GBP
as base currencies for valuations.
The only Bitcoin I own I was given by some random chap on the internet. I am by no means knowledgeable about cryptocurrencies, this app is just for fun.
Read on for an overview of how the application is put together, including interacting with APIs from PyQt5, plotting data with PyQtGraph and packaging apps with PyInstaller.
The app is powered by the CryptoCompare.com API from which we retrieve per-day high, low, open and close values, alongside market trading volume amounts. The resulting exchange rates are plotted using PyQtGraph along with a currency exchange list-view which is updated as you move your mouse through the plotted timeline. The bundled app is available for Windows and Mac.
Working with the API
The first thing we need is a data source. Here we're using CryptoCompare.com which offers free developer API access for non-commercial purposes, including historic data.
The API calls
We're using two separate API calls to plot our graphs —
- The daily historic exchange values for all supported cryptocurrencies (
BTC
,ETH
,LTC
,EOS
,XRP
andBCH
) against a set of base-currencies (EUR
,USD
andGBP
). - The daily market volume data, giving the amount of trades occurring.
The two API calls we are doing are...
https://min-api.cryptocompare.com/data/histoday?fsym={fsym}&tsym={tsym}&limit={limit} https://min-api.cryptocompare.com/data/exchange/histoday?tsym={tsym}&limit={limit}
In the URLs fsym
is the from symbol the currency converting from, tsym
is to symbol the currency we're converting to, and limit
which is the number of results to return on the request — since we're calling /histoday
this is the number of days data to return.
The requests are performed with requests
, passing a per-application key in an authentication Apikey
header, e.g.
auth_header={'Apikey':CRYPTOCOMPARE_API_KEY}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,)
Performing API requests in threads
Requests to APIs take time to complete. If we make the request directly in the GUI thread it will block the rest of the application executing — including responding to user input. The application would become unresponsive (spinning wheel of death, faded window).
We can avoid this problem quite easily by performing the API requests in a separate thread.
For a complete overview this QWorker
approach see the PyQt5 threads tutorial.
First we define a signals QObject
which contains the signals we want to emit from our worker thread. This includes signals to emit finished, error, progress (how much is complete) and data (the returned data).
classWorkerSignals(QObject):""" Defines the signals available from a running worker thread."""finished=pyqtSignal()error=pyqtSignal(tuple)progress=pyqtSignal(int)data=pyqtSignal(dict,list)cancel=pyqtSignal()
We also add a cancel signal which allows the parent app to signal to an active worker thread when a new request has been queued before the first one completes. This signal sets a flag is_interrupted
on the worker, which is checked before each currency's data is downloaded. If True
it will return without emitting the finished signal.
classUpdateWorker(QRunnable):""" Worker thread for updating currency."""signals=WorkerSignals()def__init__(self,base_currency):super(UpdateWorker,self).__init__()self.is_interrupted=Falseself.base_currency=base_currencyself.signals.cancel.connect(self.cancel)@pyqtSlot()defrun(self):auth_header={'Apikey':CRYPTOCOMPARE_API_KEY}try:rates={}forn,cryptoinenumerate(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)))ifself.is_interrupted:# Stop without emitting finish signals.returnurl='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']fordinr.json().get('Data')]exceptExceptionase:self.signals.error.emit((e,traceback.format_exc()))returnself.signals.data.emit(rates,volume)self.signals.finished.emit()defcancel(self):self.is_interrupted=True
A separate API reqeust is performed for each cryptocurrency, updating the progress bar (emitting (int(100 * n / len(AVAILABLE_CRYPTO_CURRENCIES)))
on each iteration) and then a final request is made to retrieve the volume information. Once all requests are finished the resulting data is emitted using the earlier defined signals.
Caching
The free API comes with a generous limit of 100,000 calls/month, which you're unlikely to hit. However, it's still polite not to waste other people's bandwidth if you can avoid it. Since we're retrieving daily rates, there isn't any reason to download >1 time per day.
As we're performing the API calls using the requests
library we can use requests_cache
to automatically cache all our API requests transparently. This uses a simple SQLite file database to store the results of previous requests.
importrequests_cacherequests_cache.install_cache(os.path.expanduser('~/.goodforbitcoin'))
With the cache enabled API responses will be cached and subsequent requests to the same URL will fetch from the cache (until it expires, set to 1 day by the API).
You can put the cache wherever you want on disk, the only requirement is that it is user-writeable (so it continues to work after the app is packaged).
Plotting the data
The API calls return high, low, open and close values for each day and for each cryptocurrency in addition to a separate market volume value. These are plotted as a series of lines, with each cryptocurrency close value plotted in a different colour, with high and low values drawed as dotted lines either side. The open value is not plotted.

The currency axis
The currency values are all plotted on the same scale, using the same axis. We only plot the currency lines only once we have the data back from the API (in case any currencies are not activated) so the initial setup is just of the axis and grid. We also set the axis names, and add an infinite vertical line, which is just to track through the plot to get per-day currency conversion rates.
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)
The axis' mouse move signal is connected to the custom mouse_move_handler
slot, which moves the infinite line and updates the current rates shown in the rates table (see later).
The volume axis
The volume axis is plotted on a separate scale, as a dotted black line. This can be zoomed vertically independently of the currencies. This is a bit tricky to achieve in PyQtGraph, requiring you to manually create a ViewBox
object and connect it up to the main axis.
# 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')
Unlike for the currencies we do add the curve here, since we know it will always be present. The initial state is a diagonal line (for no reason).
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)
We need connect the resized signal from the primary axis to our custom update_plot_scale
slot, to automatically update the secondary axis dimensions.
defupdate_plot_scale(self):self.p2.setGeometry(self.p1.vb.sceneBoundingRect())
Now the two axes are defined, we can draw our plot lines.
Updating the plot
The plot is updated in response to the data being returned by the API request worker. This triggers the .redraw()
method, which uses the data (available on self.data
) to either add, or update lines to the plot.
defredraw(self):y_min,y_max=sys.maxsize,0x=np.arange(NUMBER_OF_TIMEPOINTS)# Pre-process data into lists of x, y values.forcurrency,datainself.data.items():ifdata:_,close,high,low=zip(*[(v['time'],v['close'],v['high'],v['low'])forvindata])ifcurrencyinself._data_visible:# This line should be visible, if it's not drawn draw it.ifcurrencynotinself._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))else:self._data_lines[currency]['high'].setData(x,high)y_min,y_max=min(y_min,*low),max(y_max,*high)else:# This line should not be visible, if it is delete it.ifcurrencyinself._data_lines:self._data_lines[currency]['high'].clear()...
References to plotted lines are kept in a dictionary self._data_lines
keyed by the cryptocurrency identifier. This allows us to check on each update whether we already have a line defined, and update it rather than recreating it. We can also remove lines for currencies that we no longer want to draw (if they've been deselected in the currency list).
The market activity (volume) plot however is always there, so we can just perform a simple update to the existing line.
self._market_activity.setData(x,self.volume)
In addition to the plotted lines, we also show a list with the currency conversion rates for all cryptocurrencies at the currently position in the graph. As you move your pointer back and forward these rates update automatically.
Rates table
The rates table is a QTableView
widget, using the Qt5 ModelView architecture.

We define a QStandardItemModel
model which we can use to update the data in the table, and set the headers for the colums. Finally, the .itemChanged
signal is connect to our custom slot method check_check_state
.
self.listView=QTableView()self.model=QStandardItemModel()self.model.setHorizontalHeaderLabels(["Currency","Rate"])self.model.itemChanged.connect(self.check_check_state)
If the item is checked, and the currency is not currently displayed (the currency identifier is not in our ._data_visible
map) we add the currency to it and trigger a redraw. Likewise, if the item is unchecked but the currency is displayed, we remove it and trigger a redraw.
defcheck_check_state(self,i):ifnoti.isCheckable():# Skip data columns.returncurrency=i.text()checked=i.checkState()==Qt.Checkedifcurrencyinself._data_visible:ifnotchecked:self._data_visible.remove(currency)self.redraw()else:ifchecked:self._data_visible.append(currency)self.redraw()
We always download data for all currencies, even if they are not currently displayed, so we can update the plot immediately. You might want to have a go at changing this behaviour.
To automatically update the values on the rates table we have already connected our mouse_move_handler
slot to mouse moves on the main axis. This slot receives a pos
value which is a QPoint
position relative to the axis. We first use the .x()
value to set the position of the vertical line, and then hand off the int of the value to our update_data_viewer
method.
defmouse_move_handler(self,pos):pos=self.ax.getViewBox().mapSceneToView(pos)self.line.setPos(pos.x())self.update_data_viewer(int(pos.x()))
This next method checks if the position i
is within the range of our data (the number of days of data we have). Then for each currency it gets the corresponding value (the close value) and then sets this onto the second QStandardItems
— the column with the currency exchange rates — as a 4dp number.
defupdate_data_viewer(self,i):ifinotinrange(NUMBER_OF_TIMEPOINTS):returnforcurrency,datainself.data.items():self.update_data_row(currency,data[i])defupdate_data_row(self,currency,data):citem,vitem=self.get_or_create_data_row(currency)vitem.setText("%.4f"%data['close'])
The get_or_create_data_row
looks to see if a data row exists in the model for the corresponding currency. If it does it returns the existing row, if not it creates a new row by calling add_data_row
. This means we don't need to define the rows explicitly, they are created automatically based on the data returned by the API.
defget_or_create_data_row(self,currency):ifcurrencynotinself._data_items:self._data_items[currency]=self.add_data_row(currency)returnself._data_items[currency]defadd_data_row(self,currency):citem=QStandardItem()citem.setText(currency)citem.setForeground(QBrush(QColor(self.get_currency_color(currency))))citem.setColumnCount(2)citem.setCheckable(True)ifcurrencyinDEFAULT_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)returncitem,vitem
Packaging
Now we have the working app, the last step is to bundle it up for distribution. Installers for Windows and Mac are available at the top of this article. To do this we're using PyInstaller the current standard for bundling Python applications.
Because this app has no external data files building an installer for it is pretty straightforward. Install PyInstaller with pip3 install pyinstaller
then run —
pyinstaller crypto.py
This produces a spec file which contains the information needed by PyInstaller to build the distribution installers. This file is cross-platform and should be included in your source control so any improvements are persisted.
The automatically generated file is enough to package this application as-is, but we need a few tweaks to make it complete.
MacOS Retina Support
By default MacOS applications don't support retina (high resolution) screens. To enable this support you need to set the NSHighResolutionCapable
flag in the application .plist
bundled inside the .app
. This is simple enough to do in PyInstaller.
Edit the .spec
file to add the info_plist
block shown below, with NSHighResolutionCapable
app=BUNDLE(coll,...info_plist={'NSHighResolutionCapable':'True'},
Now, whenever you bundle your application, this flag will be added to the MacOS bundle .plist
automatically.
Icons
To make the application show a custom icon while running we need to generate Windows .ico
and MacOS .icns
files and add these to the .spec
definition.
A MacOSX icon bundle icon.icns
contains multiple alternative icon sizes, which are laborious to generate by hand. The following script will take a single .png
file input and automatically generate an .icns
bundle containing the different sizes.
#!/bin/bash mkdir $1.iconset sips -z 1616$1 --out $1.iconset/icon_16x16.png sips -z 3232$1 --out $1.iconset/icon_16x16@2x.png sips -z 3232$1 --out $1.iconset/icon_32x32.png sips -z 6464$1 --out $1.iconset/icon_32x32@2x.png sips -z 128128$1 --out $1.iconset/icon_128x128.png sips -z 256256$1 --out $1.iconset/icon_128x128@2x.png sips -z 256256$1 --out $1.iconset/icon_256x256.png sips -z 512512$1 --out $1.iconset/icon_256x256@2x.png sips -z 512512$1 --out $1.iconset/icon_512x512.png cp $1$1.iconset/icon_512x512@2x.png iconutil -c icns $1.iconset rm -R $1.iconset
This file saved as makeicns.sh
and chmod +x makeicns.sh
can then be used to generate an .icns
bundle from a single large PNG, as follows.
./makeicns.sh bitcoin-icon.png
You may want to check the resized icons and edit the lower resolution ones to simplify them to improve clarity. Just remove the rm -R $1.iconset
step from the script.
For Windows we can generate an .ico
file by loading a PNG into Gimp and resize down to 64x64, 32x32 and 16x16 on separate layers. Unlike for MacOS you can provide a single square image if you are happy to let it be resized automatically, just ensure it is saved as .ico
.
The completed spec file
To complete the spec file we can manually set the name of the application (to Goodforbitcoin) and update the filenames for the bundled applications to match. In addition the PyInstaller script will have added a pathex
variable with a static path.
pathex=['/Users/martin/repos/minute-apps/crypto'],
The pathex
will be different if you generate this file yourself.
This can be removed if the .spec
file is in the same folder are your applications base Python file to make the file portable. The last step is to add a number of hidden imports (modules which are not correctly detected by PyInstaller). These are only necessary for the Windows builds.
The completed spec file used to bundle the downloads is shown below.
# -*- mode: python ; coding: utf-8 -*-block_cipher=Nonea=Analysis(['crypto.py'],binaries=[],datas=[],hiddenimports=['numpy.random.common','numpy.random.bounded_integers','numpy.random.entropy',],hookspath=[],runtime_hooks=[],excludes=[],win_no_prefer_redirects=False,win_private_assemblies=False,cipher=block_cipher,noarchive=False)pyz=PYZ(a.pure,a.zipped_data,cipher=block_cipher)exe=EXE(pyz,a.scripts,[],exclude_binaries=True,name='Goodforbitcoin',debug=False,bootloader_ignore_signals=False,strip=False,upx=True,console=False,icon='resources/icon.ico')coll=COLLECT(exe,a.binaries,a.zipfiles,a.datas,strip=False,upx=True,upx_exclude=[],name='Goodforbitcoin')app=BUNDLE(coll,name='Goodforbitcoin.app',icon='resources/icon.icns',bundle_identifier='com.learnpyqt.Goodforbitcoin',info_plist={'NSHighResolutionCapable':'True'},)
The packaged Goodforbitcoin apps, along with the source code, can be downloaded using the links below.
Speed Matters: Python and Lua
Python is great, but pure Python code sometimes has one problem: It’s slow.
Fortunately there are several great solutions to improve the performance, like Numpy, Cython, Numba, Pypy.
All of the above solutions have different drawbacks:
Numpy and Numba are big modules. In addition Numpy is not always fast enough. Pypy is not 100% compatible and a heavy solution if it is used in addition to CPython. Cython is complex and you need a C-compiler. Recently I’ve found one more solution which maybe is not that well known: Lua integration in Python programs.
Lua is another scripting language with dynamic data types.
So I asked myself:
Does it make sense to have another scripting language inside Python scripts?
Let’s have a look at a simple example: Mandelbrot
First of all the pure Python example:
fromnumpyimportcomplex,arangedefPyMandelbrotIter(c):z=0foritersinrange(200):ifabs(z)>=2:returnitersz=z**2+creturnitersdefPyMandelbrot(size):image=Image.new('RGB',(size,size))pix=image.load()t1=time.time()xPts=arange(-1.5,0.5,2.0/size)yPts=arange(-1,1,2.0/size)forxx,xinenumerate(xPts):foryy,yinenumerate(yPts):pix[xx,yy]=PyMandelbrotIter(complex(x,y))dt=time.time()-t1print(f"dt={dt:.2f}")image.show()
Runtimes of this example on a Core i7 laptop with Python 3.7 and Windows 10:
Size | dt [s] |
---|---|
320 | 3.32 |
640 | 13.54 |
1280 | 55.59 |
Now the Lua example integrated in a Python script:
fromlupaimportLuaRuntimelua_code='''\
function(N, i, total)
local char, unpack = string.char, table.unpack
local result = ""
local M, ba, bb, buf = 2/N, 2^(N%8+1)-1, 2^(8-N%8), {}
local start_line, end_line = N/total * (i-1), N/total * i - 1
for y=start_line,end_line do
local Ci, b, p = y*M-1, 1, 0
for x=0,N-1 do
local Cr = x*M-1.5
local Zr, Zi, Zrq, Ziq = Cr, Ci, Cr*Cr, Ci*Ci
b = b + b
for i=1,49 do
Zi = Zr*Zi*2 + Ci
Zr = Zrq-Ziq + Cr
Ziq = Zi*Zi
Zrq = Zr*Zr
if Zrq+Ziq > 4.0 then b = b + 1; break; end
end
if b >= 256 then p = p + 1; buf[p] = 511 - b; b = 1; end
end
if b ~= 1 then p = p + 1; buf[p] = (ba-b)*bb; end
result = result .. char(unpack(buf, 1, p))
end
return result
end
'''defLuaMandelbrot(thrCnt,size):defLuaMandelbrotFunc(i,lua_func):results[i]=lua_func(size,i+1,thrCnt)t1=time.time()lua_funcs=[LuaRuntime(encoding=None).eval(lua_code)for_inrange(thrCnt)]results=[None]*thrCntthreads=[threading.Thread(target=LuaMandelbrotFunc,args=(i,lua_func))fori,lua_funcinenumerate(lua_funcs)]forthreadinthreads:thread.start()forthreadinthreads:thread.join()result_buffer=b''.join(results)dt=time.time()-t1print(f"dt={dt:.2f}")image=Image.frombytes('1',(size,size),result_buffer)image.show()
Runtimes of this example on a performance laptop with Python 3.7 and Windows 10:
Size | dt [s] 1 thread | dt [s] 2 threads | dt [s] 4 threads | dt [s] 8 threads | dt [s] 16 threads |
---|---|---|---|---|---|
320 | 0.22 | 0.11 | 0.07 | 0.06 | 0.04 |
640 | 0.68 | 0.38 | 0.26 | 0.21 | 0.17 |
1280 | 2.71 | 1.50 | 1.05 | 0.81 | 0.66 |
The above results are very impressive. As you can see Lua is really much faster. And as you can also see: You can parallelize with threads!
The module lupa
, which comes with a Lua interpreter and a JIT compiler, is a very interesting alternative for speeding up long running tasks.
The Lua solution has the following advantages:
- The
lupa
module is very small. - Lua is much faster than Python.
- You can run Lua scripts in parallel with threads.
- Lua is very easy to read and code.
- You can easily integrate Lua scripts in Python code.
- You can easily access Python objects within Lua and vice versa.
- There are many extension modules available for Lua (~2600, see luarocks.org).
Give lupa
a try. It’s easy to use and really great!
Weekly Python StackOverflow Report: (clxxxviii) stackoverflow python report
These are the ten most rated questions at Stack Overflow last week.
Between brackets: [question score / answers count]
Build date: 2019-07-28 11:32:11 GMT
- Is Python `list.extend(iterator)` guaranteed to be lazy? - [11/2]
- Count the identical pairs in two lists - [10/6]
- Get the next element of list in Python - [8/4]
- Infer generic lambda parameters from another generic lambda's parameters - [8/1]
- Group recursively adjacent tuples from a list in Python - [7/5]
- Significance of Capitalization in Python collections module? - [7/1]
- How to remove consecutive identical words from a string in python - [6/7]
- Random list only with -1 and 1 - [6/6]
- How to align two lists of numbers - [6/5]
- Structuring a 2D array from a pandas dataframe - [6/2]
Catalin George Festila: Python 3.7.3 : Using the flask - part 003.
PSF GSoC students blogs: Weekly check-in #7 (week 9): 22/07 to 28/07
Hi all. Hope we all passed evaluation 2 and our work continues!
What did you do this week?
The text PR saw a couple of updates, such as support for more ways to specify certain arguments (take more types), a change to the underlying method used for 1D array resizing (replaced scipy.signal with scipy.interpolate), and unit tests for text helpers.
I moved the code that could be reused by different neural network packages (Keras, PyTorch, Tensorflow, etc) into a new ‘nn’ package.
A major feature was adding PyTorch support in a new branch off the text PR. I got working support for images (torchvision and reusable numpy code helped a lot), but text support is yet to be added.
What is coming up next?
Work will continue as usual: Text PR needs fixes, docs and tests. In particular, I will need to train better models for integration tests.
I should open a PyTorch WIP PR once text support is added. That includes obtaining a text model with which I can do manual tests. A stretch goal would be to also get started with docs and other things that come with PR’s.
Since one of my mentors is back from holidays, I will take any feedback, and we can also plan out the last 3 weeks of GSoC. Besides the current tasks, there is some room in the schedule for other things.
Did you get stuck anywhere?
As mentioned, I need to train text models specifically designed for integration tests – small, covering many cases (kinds of layers used, character/word level tokenization, output classes, etc), and working well with explanations. I tried to add an integration test but the model I used gave dubious explanations for simple sentences, so changing the model is a solution I can try.
I was keen on opening a PyTorch PR this week, but a blocker was adding text support. Specifically I didn’t have an off-the-shelf pretrained text model to test with. The simplest solution I tried was saving a text classifier from a public kaggle notebook (I got an error when trying to save so I will need to retry).
I shall see you next week!
Tomas Baltrunas
PSF GSoC students blogs: Coding Period: Week 9
Greetings everyone! Update for this week is as follows. In this phase, I sent the remaining patches required to complete hg continue support for core Mercurial repository.
What did I do this week?
As stated in the previous week I sent patches for hg continue for for graft, transplant and histedit.[1][2][3][4]
I also created logic for hg transplant --abort[5] as asked by @Pulkit and added its support to hg abort plan [6]. These patches are still to be merged and under review.
What is coming up next?
This week I am planning to complete support for hg continue for evolve. Also, I will start my work on logic for hg update --abort which is one of the most demanded features for Mercurial.
My patches for hg abort for evolve merged as they still left to get reviews as most of the people in the community are busy with the new version release.
Did you get stuck anywhere?
I did get stuck when I started with the logic for aborting a transplant but @Pulkit again helped me out of the situation and suggested that I should look into the recover method of the transplanter class.
PSF GSoC students blogs: Weekly CheckIN 8th
What did you do this week?
Hoorya!! I passed the second evaluation. In the evaluation week I mainly completed the only test which is left before the second evaluation i.e Adding test for the ploneGenerator method which generate all the Gatsby Node initially. It is the most important generator function. I never tested the generator function in my life. I searched what is the best way to test the generator function and I found some response and successfully Send a pr for that. After that there is simple bug i.e Foreach array method randomly calling the api but we need syncrounus behaviour. After devoting one day and lot of thinking I found that only replacing the foreach method with for loop does the trick. Then I successfully created the pr for that also. You can see the pr link below.
https://github.com/collective/gatsby-source-plone/pull/218
https://github.com/collective/gatsby-source-plone/pull/225
What is coming up next?
Upto this time I have successfully implemented the MVP for my project. Written test for all the function which is present in the source code. Now, I only have to refactor the code and write documentation for the added feature. So, You can think that this period mainly deal with refactoring and documenting.
Did you get stuck anywhere?
Really for first time in this project I suffer with simple issue which is converting the foreach method to for loop. It was not easy to find what's going wrong with foreach. Why it doesn't behaviour synchronously. But after lot of thinking and experiment I am able to attend the behaviour i.e synchronous.
PSF GSoC students blogs: 4th Blog Post (During the second Period)
So, what I have done in second period. For me the answer is very simple, MVP is completed in first phase which give me ample time to write test for the added feature and other function which is not tested earlier. In second Period I read lot of documentation of Jest. I have gone through each Matchers which is Provided by jest. Spent almost a week on reading it. After reading the documentation I feel very comfortable in writing test in jest. From my experience I would say don't dive in coding without knowing anything about the subject. First read the documentation of the following framework and become comfortable with the language. After that you will able to solve the problem in very less time. I have written around 10 test in this period which covers every aspect of the source code. I got stuck at the point where I have to write test for the generator function. I have to mock lot of data because It is the main function of source code which generates all the gatsby nodes initially. I also make some mistake. I am fetching data from the Plone CMS and then doing logging. I just forget that console log not print the nested array. So, I have made a huge mistake using console log. After lot of try I am not able to write test properly. I stuck in loop fixing one thing break another, after lot of afford I am able to solve this problem at 3am when I am going to bed after hectic day. I really enjoyed this phase a lot, I learned lot of new things and implemented it. I am very happy with myself what I am doing right now. In third phase I mainly write documentation and refactoring the code into small pieces of function so that It can enhance the readability of code.
If You find any thing interesting and anything which you want to ask, I will be happy to answer it :)
Thank You
PSF GSoC students blogs: Weekly check-in #5
Hi and welcome again!
Let's get going.
What did you do this week?
Last week I was fixing bugs and errors in the landing page code. I finished with that(almost). I had a really hard time, because I created a lot more errors while fixing a few errors. But Finally, I don't remember how but I fixed those. Well, I still need to do a few more adjustments but its fine. Also, I wrote the code for the cheatsheet and its almost ready.
What's coming next?
After I finish with the Landing page and cheatsheet, I will create a Mockup for the documentation page of EOS and later I may have to implement it too. Those are some later plans.
Did you stuck anywhere?
Yes, yes I was stuck for a long time here, when I was fixing the landing page. Man, I don't know how but some vs-code extension played with my code and created some .map files on its own. And keeps on generating those files automatically every time I use gulp. That was a pain but eventually, I was able to fix that!
Podcast.__init__: Docker Best Practices For Python In Production
Summary
Docker is a useful technology for packaging and deploying software to production environments, but it also introduces a different set of complexities that need to be understood. In this episode Itamar Turner-Trauring shares best practices for running Python workloads in production using Docker. He also explains some of the security implications to be aware of and digs into ways that you can optimize your build process to cut down on wasted developer time. If you are using Docker, thinking about using it, or just heard of it recently then it is worth your time to listen and learn about some of the cases you might not have considered.
Announcements
- Hello and welcome to Podcast.__init__, the podcast about Python and the people who make it great.
- When you’re ready to launch your next app or want to try a project you hear about on the show, you’ll need somewhere to deploy it, so take a look at our friends over at Linode. With 200 Gbit/s private networking, scalable shared block storage, node balancers, and a 40 Gbit/s public network, all controlled by a brand new API you’ve got everything you need to scale up. And for your tasks that need fast computation, such as training machine learning models, they just launched dedicated CPU instances. Go to pythonpodcast.com/linode to get a $20 credit and launch a new server in under a minute. And don’t forget to thank them for their continued support of this show!
- To connect with the startups that are shaping the future and take advantage of the opportunities that they provide, check out Angel List where you can invest in innovative business, find a job, or post a position of your own. Sign up today at pythonpodcast.com/angel and help support this show.
- You listen to this show to learn and stay up to date with the ways that Python is being used, including the latest in machine learning and data analysis. For even more opportunities to meet, listen, and learn from your peers you don’t want to miss out on this year’s conference season. We have partnered with organizations such as O’Reilly Media, Dataversity, and the Open Data Science Conference. Coming up this fall is the combined events of Graphorum and the Data Architecture Summit. The agendas have been announced and super early bird registration for up to $300 off is available until July 26th, with early bird pricing for up to $200 off through August 30th. Use the code BNLLC to get an additional 10% off any pass when you register. Go to pythonpodcast.com/conferences to learn more and take advantage of our partner discounts when you register.
- Visit the site to subscribe to the show, sign up for the newsletter, and read the show notes. And if you have any questions, comments, or suggestions I would love to hear them. You can reach me on Twitter at @Podcast__init__ or email hosts@podcastinit.com)
- To help other people find the show please leave a review on iTunes and tell your friends and co-workers
- Join the community in the new Zulip chat workspace at pythonpodcast.com/chat
- Your host as usual is Tobias Macey and today I’m interviewing Itamar Turner-Trauring about what you need to know about running Python workloads in Docker
Interview
- Introductions
- How did you get introduced to Python?
- For anyone who is unfamiliar with it, can you describe what Docker is and the benefits that it can provide?
- What was your motivation for dedicating so much time and energy to the specific area of using Docker for Python production usage?
- What are some of the common issues that developers and operations engineers run into when dealing with Docker and its build system?
- What are some of the issues that are specific to Python that you have run into when using Docker?
- How does the ecosystem for Python in containers compare to other languages that you are familiar with?
- What are some of the security issues that engineers are likely to run into when using some of the advice and pre-existing containers that are publicly available?
- One of the issues that you call out is the speed of container builds. What are some of the contributing factors that lead to such slow packaging times?
- Can you talk through some of the aspects of multi-layer packages and useful ways to take proper advantage of them?
- There have been some recent projects that attempt to work around the shortcomings of the Dockerfile itself. What are your thoughts on that overall effort and any specific tools that you have experimented with?
- When is Docker the wrong choice for a production environment?
- What are some useful alternatives to Docker, for Python specifically and for software distribution in general that you have had good luck with?
Keep In Touch
Picks
- Tobias
- Itamar
Links
- Itamar’s Best Practices Guide
- Docker
- Zope
- GitLab CI
- Heresy In The Church Of Docker
- Poetry
- Pipenv
- Dockerfile
- 40 Years of DSL Disasters (Slides)
- Ubuntu
- Debian
- Docker Layers
- Bitnami
- Alpine Linuxhttps://alpinelinux.org?utm_source=rss&utm_medium=rss
- PodMan
- Nix
- Heroku Buildpacks
- Itamar’s Docker Template
- Hashicorp Packer
- Rkt
- Solaris Zones
- BSD Jails
- PyInstaller
- Snap
- FlatPak
- Conda
The intro and outro music is from Requiem for a Fish The Freak Fandango Orchestra / CC BY-SA
IslandT: Get top exchanges by volume for a currency pair
In this chapter, we will create a button which will call a function to retrieve the top 50 exchanges and the trading volume for a cryptocurrency/currency pair. The API needs the selected cryptocurrency and the selected currency symbol to make the call.

First we will create the exchange button.
# get top 50 exchanges by volume for currency/cryptocurrency pairs action_coin_top_exchange = tk.Button(selectorFrame, text="Top Exchange", command=top_exchange, state=DISABLED) # button used to get the top currency/cryptocurrency pair data action_coin_top_exchange.pack(side=LEFT)
Then we create the function.
def top_exchange(): # Get a number of top 50 exchanges by their volumes sell_buy = '' try: currency_symbol = based.get() # get the currency symbol crypto_symbol = crypto.get() # get the cryptocurrency symbol url = "https://min-api.cryptocompare.com/data/top/exchanges" data = {'tsym': currency_symbol, 'fsym': crypto_symbol, 'limit': 50} r = requests.get(url, params=data) exchange_rate_s = json.loads(json.dumps(r.json())) except: print("An exception occurred") for value in exchange_rate_s['Data']: # populate exchange rate string for key, value1 in value.items(): sell_buy += str(key) + " : " + str(value1) + "\n" sell_buy += "\n" text_widget.delete('1.0', END) # clear all those previous text first s.set(sell_buy) text_widget.insert(INSERT, s.get()) # populate the text widget with new exchange rate data
If you miss out the first few posts regarding this project, you can read them all on this website under the python category.
Kushal Das: Using sops with Ansible for vars encryption
Sops is a secret management tool from Mozilla. According to the official Github page, it is defined as:
sops is an editor of encrypted files that supports YAML, JSON, ENV, INI and BINARY formats and encrypts with AWS KMS, GCP KMS, Azure Key Vault and PGP.
In this blog post, I am going to show you how you can use it along with GPG to encrypt secrets in YAML files for your Ansible roles/playbooks.
Installation
Download the official binary from the release page, or you can build and install from the source code.
Basic usage (manual)
First let us create a .sops.yaml
in the git repository root directory. In
this example, I am saying that I can encrypt any .yml using the two given
GPG fingerprints.
creation_rules:
# If vars file matchs "*.yml", then encrypt it more permissively.
- path_regex: \.yml$
pgp: "A85FF376759C994A8A1168D8D8219C8C43F6C5E1,2871635BE3B4E5C04F02B848C353BFE051D06C33"
Now to encrypt a file in place, I can use the following command:
sops -e -i host_vars/stg.dgplug.org.yml
If we open the file afterward, we will see something like below.
mysql_root_password: ENC[AES256_GCM,data:732TA7ps+qE=,iv:3azuZg4tqsLfe5IHDLJDKSUHmVk2c0g1Nl+oIcKOXRw=,tag:yD8iwmxmENww+waTs5Kzxw==,type:str]
mysql_user_password: ENC[AES256_GCM,data:YXBpO54=,iv:fQYATEWw4pv4lW5Ht8xiaBCliat8xdje5qdmb0Sff4Y=,tag:cncwg2Ops35l0lWegCSEJQ==,type:str]
mysql_user: ENC[AES256_GCM,data:cq/VgDlpRBxuHKM+cw==,iv:K+v6fkCIucMrMJ7pDRxFS/aHh0OCxqUcLJhZIgCsfA0=,tag:BD7l662OVOWRaHi2Rtw37g==,type:str]
mysql_db_name: ENC[AES256_GCM,data:hCgrKmU=,iv:/jnypeWdqUbIRy75q7OIODgZnaDpR3oTa0G/L8MRiZA=,tag:0k6cGNoDajUuKpKzrwQBaw==,type:str]
sops:
kms: []
gcp_kms: []
azure_kv: []
lastmodified: '2019-07-29T04:05:09Z'
mac: ENC[AES256_GCM,data:qp9yV3qj0tYf/EaO0Q3JdlpPvm5WY4ev1zGCVNoo+Anm/esj0WHlR7M7SNg54xRTUwMhRRnirx7IsEC8EZW1lE+8DObnskemcXm93CJOBfVzQOX/RvCSR4rMp2FgBEPZADCDiba1X2K/9myU96lADME0nkdmX9YjhOLMFJ6ll4o=,iv:2WNKhl81FI/qw6mRKpK5wRYjqK16q1ASeCJYpEeuhj4=,tag:v4PlGT4ZmPUxj7aYIendVg==,type:str]
pgp:
- created_at: '2019-07-29T04:05:06Z'
enc: |-
-----BEGIN PGP MESSAGE-----
wcFMA/uCql0ybaddARAADkHqV/mEgVoTxVHkRKW3mjajReKaQ5Mz/VwMal3GsjKr
8aGnghCjtgJE01wCBjrTfNKKmlf5JjMSFufy9pG0xE+OFOXt+pnJFDSro26QnPFG
VlvkvnjTxw4gV/mIvxUXTT6rmijvQSHMXdyEGmyHu3kNprKuuN37xZ4SYSWg7pdO
vee4DSOaw+XfdgYUF+YSEjKMZ+qevRtzJL4hg9SZvEsHMQObueMBywAc5pyR6LvS
ZuAS5SS+cPXnyMJLemqfU7L+XMw2NMrmNYFXOWnMMny1Hez5fcmebJp+wjDqWJTX
j9vJvXLuNAglFvL1yz2rNJYTb0mC9chLT7YxEa9Z9JHWezUB8ZYuOC6vRf18/Hz8
e6Gd2sncl4rleCxvKZF9qECsmFzs4p7H5M+O31jnjWdnPBD8a84Du3wdeWiI5cRF
d7/aUXEdGJQy7OVbzGE1alDOSIyDD2S73ou2du7s/79Wb11RwQV898OyGgmjWm0Y
7hTsBiBXnayQdjtg6qKlvoWIn79PU0YmDYLujMiXDQPJLV3ta82dcK2B1yTCLuSO
nGfFzNOSNemmniyEuMI16SrfYDsf9l/K3gec+TRNvEdc1GqO4gFblQWptPE7KIIC
oBVMDLUJSpOf5yF7Izedy5wBb8ZmzJAvpchvTMUls2+En4ifYh90cauXxiP6edPS
4AHkZMebg44aCefn4zMdKdUhbOFxbuA/4I3hQWLgmuJMFlcI4Orl5tsRjXwCfQ9S
jOdGAUJ8usV9gT4IXj73WfN1JJHj7DTgoORXFtDMs2Yy/rPPR4H9msSL4reJGZLh
dEAA
=LoMY
-----END PGP MESSAGE-----
fp: A85FF376759C994A8A1168D8D8219C8C43F6C5E1
- created_at: '2019-07-29T04:05:06Z'
enc: |-
-----BEGIN PGP MESSAGE-----
wcFMA0HZfc7pf7hDARAAX6xF6qP9KQJ5wLn0kv5WVf8HgOkk+2ziIuBH411hVEir
oN4bwwnL+DEYZm2aFvZl5zQElua7nGkQK041DecPbOCCBqThv3QKVMy2jScG5tTj
jxGgh8W/KxdwIY7teRaCRNDkT18IhtBc4SO2smJEtPeGIptvDHLjETBFbDnZeo6/
PG4QIA1Rfvm14n1NR56oibWduwvb1wrm4tGYPDx8mVgfugxeIhoqlZ87VrivrN+2
40S/rwWQ/aEVM1A8q19DGkXYVBcxQA1dGBrKLDPtiCq/LCSDNY4iuzMjzQuHPjgM
bS0lWv8fvSp6iIZlB3eSRPW9Ia8tRJpEfTLS8jiHlcCZ4Vy3fW6EijBf00iSy5hP
Y54TCERscXt1/UOW2ACYTNhMfuiO+WuG6Vjdns1NsSUVazqxmf+kBMwl06/HyUwL
KAYTOB2hipYUm6mlpSBfDgBKjQq8dgvxlWP0Ay0of0p4ZzFv7MepYkJA+gwb0hUt
rui9GVE/uys8W8buYqfM2ABzIG4GrH3rELh8eW8oPwlu7rgS7YGhyog2xabJyQnj
BZ65wwu5TzMq+n5v1+878teOzqqD/F+6X5dw0jF95bDHKdA64JR/Uxlj75sp59GH
e/+3UDm0UwSILDrYJkcVaTnrt3wPjQAw4ynKZuN+k6KvmDCkGquHNaM+2hqvWq/S
4AHkgpZHzmU14QmfJy7RN2HPduHhSeCh4LrhMzngJuJyH72G4IPlP4WwPJUhrKMt
UI+Azl61rIZEm6n82oWbY1gIrrIygWLg1OSD/0Ly6KbO4/W1NipWU53w4nAo7abh
3gkA
=YXu1
-----END PGP MESSAGE-----
fp: 2871635BE3B4E5C04F02B848C353BFE051D06C33
unencrypted_suffix: _unencrypted
version: 3.1.1
If you look closely, we can see that sops only encrypts the values, not the keys in the YAML file.
Decrypting as required on runtime
We can use a small Python script to enable runtime decryption of the file as required in Ansible. Create a vars_plugin directory in the git repository root, and then put the following code in there as sops.py.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = '''
vars: sops
version_added: "N/A"
short_description: In charge of loading SOPS-encrypted vars
description:
- Loads SOPS-encrytped YAML vars into corresponding groups/hosts in group_vars/ and host_vars/ directories.
- Only SOPS-encrypted vars files, with a top-level "sops" key, will be loaded.
- Extends host/group vars logic from Ansible core.
notes:
- SOPS binary must be on path (missing will raise exception).
- Only supports YAML vars files (JSON files will raise exception).
- Only host/group vars are supported, other files will not be parsed.
options: []
'''
import os
import subprocess
import yaml
from ansible.errors import AnsibleError, AnsibleParserError
from ansible.module_utils._text import to_bytes, to_native, to_text
from ansible.inventory.host import Host
from ansible.inventory.group import Group
from ansible.utils.vars import combine_vars
FOUND = {}
# Import host_group_vars logic for file-walking functions.
# We'll still need to copy/paste and modify the `get_vars` function
# and edit below to insert a call to sops cli.
from ansible.plugins.vars.host_group_vars import VarsModule as HostGroupVarsModule
# All SOPS-encrypted vars files will have a top-level key called "sops".
# In order to determine whether a file is SOPS-encrypted, let's inspect
# such a key if it is found, and expect the following subkeys.
SOPS_EXPECTED_SUBKEYS = [
"lastmodified",
"mac",
"version",
]
class AnsibleSopsError(AnsibleError):
pass
class VarsModule(HostGroupVarsModule):
def get_vars(self, loader, path, entities, cache=True):
"""
Parses the inventory file and assembles host/group vars.
Lifted verbatim from ansible.plugins.vars.host_group_vars, with a single
in-line edit to support calling out to the SOPS CLI for decryption.
Only SOPS-encrypted files will be handled.
"""
if not isinstance(entities, list):
entities = [entities]
super(VarsModule, self).get_vars(loader, path, entities)
data = {}
for entity in entities:
if isinstance(entity, Host):
subdir = 'host_vars'
elif isinstance(entity, Group):
subdir = 'group_vars'
else:
raise AnsibleParserError("Supplied entity must be Host or Group, got %s instead" % (type(entity)))
# avoid 'chroot' type inventory hostnames /path/to/chroot
if not entity.name.startswith(os.path.sep):
try:
found_files = []
# load vars
b_opath = os.path.realpath(to_bytes(os.path.join(self._basedir, subdir)))
opath = to_text(b_opath)
key = '%s.%s' % (entity.name, opath)
if cache and key in FOUND:
found_files = FOUND[key]
else:
# no need to do much if path does not exist for basedir
if os.path.exists(b_opath):
if os.path.isdir(b_opath):
self._display.debug("\tprocessing dir %s" % opath)
found_files = loader.find_vars_files(opath, entity.name)
FOUND[key] = found_files
else:
self._display.warning("Found %s that is not a directory, skipping: %s" % (subdir, opath))
for found in found_files:
# BEGIN SOPS-specific logic
if self._is_encrypted_sops_file(found):
new_data = self._decrypt_sops_file(found)
if new_data: # ignore empty files
data = combine_vars(data, new_data)
# END SOPS-specific logic
except Exception as e:
raise AnsibleParserError(to_native(e))
return data
def _is_encrypted_sops_file(self, path):
"""
Check whether given filename is likely a SOPS-encrypted vars file.
Determined by presence of top-level 'sops' key in vars file.
Assumes file is YAML. Does not support JSON files.
"""
is_sops_file_result = False
with open(path, 'r') as f:
y = yaml.safe_load(f)
if type(y) == dict:
# All SOPS-encrypted vars files will have top-level "sops" key.
if 'sops' in y.keys() and type(y['sops'] == dict):
if all(k in y['sops'].keys() for k in SOPS_EXPECTED_SUBKEYS):
is_sops_file_result = True
return is_sops_file_result
def _decrypt_sops_file(self, path):
"""
Shells out to `sops` binary and reads decrypted vars from stdout.
Passes back dict to vars loader.
Assumes that a file is a valid SOPS-encrypted file. Use function
`is_encrypted_sops_file` to check.
Assumes file is YAML. Does not support JSON files.
"""
cmd = ["sops", "--input-type", "yaml", "--decrypt", path]
real_yaml = None
try:
decrypted_yaml = subprocess.check_output(cmd)
except OSError:
msg = "Failed to call SOPS to decrypt file at {}".format(path)
msg += ", ensure sops is installed in PATH."
raise AnsibleSopsError(msg)
except subprocess.CalledProcessError:
msg = "Failed to decrypt SOPS file at {}".format(path)
raise AnsibleSopsError(msg)
try:
real_yaml = yaml.safe_load(decrypted_yaml)
except yaml.parser.ParserError:
msg = "Failed to parse YAML from decrypted SOPS file at {},".format(path)
msg += " confirm file is YAML format."
raise AnsibleSopsError(msg)
return real_yaml
From the next you will try to use any of the encrypted vars files in an Ansible run, it will ask for the GPG passphrase to decrypt the file as required.
Thank you Conor Schaefer for the original version of the Python script.
Mike Driscoll: PyDev of the Week: Ines Montani
This week we welcome Ines Montani (@_inesmontani) as our PyDev of the Week! Ines is the Founder of Explosion AI and a core developer of the spaCy package, which is a Python package for Natural Language Processing. If you would like to know more about Ines, you can check out her website or her Github profile. Let’s take a few moments to get to know her better!
Can you tell us a little about yourself (hobbies, education, etc):
Hi, I’m Ines! I pretty much grew up on the internet and started making websites when I was 11. I remember sitting in school and counting the hours until I could go back home and keep working on my websites. I still get that feeling sometimes when I’m working on something particularly exciting.
I wasn’t quite sure what to do with my life, so I ended up doing a combined degree of media science and linguistics and went on to work in the media industry for a few years, leading marketing and sales. But I always kept programming and building things on the side.
In 2016, I started Explosion, together with my co-founder Matt. We specialise in developer tools for Machine Learning, specifically Natural Language Processing – so basically, working with and extracting information from large volumes of text. Our open-source library spaCy is a popular package for building industrial-strength, production-ready NLP pipelines. We also develop Prodigy, an annotation tool for creating training data for machine learning models.
I’m based in Berlin, Germany, and if I’m not programming, I enjoy bouldering , eating good food
and spending time with my pet rats
.
Why did you start using Python?
It really just kinda… happened. I never sat down and said, hey, I want to learn Python. I’m actually pretty bad at just sitting down and learning things. I always need a project or a higher-level goal. When I started getting into Natural Language Processing, many of the tools I wanted to use and work on were written in Python. So I ended up learning Python along the way. It also appealed to me as a language, because it’s just very accessible and straightforward, and I like the syntax.
What other programming languages do you know and which is your favorite?
These days, I mostly work in Python and Cython. I’m also fluent in JavaScript, have recently started working more with TypeScript, and did a bit of PHP and Perl back in the day.
I don’t want to get hung up on the definition of a “programming language”, but in terms of *writing code*, I also really love building things for the web. CSS is quite elegant once you get to know it, and it’s actually one of my favourite things to write.
What projects are you working on now?
I recently finished a bunch of stuff that has been in the works for a long time! The other month, we finally released v2.1 of our open-source library spaCy. I also published a free interactive online course on Advanced NLP with spaCy, together with an open-source framework for building interactive online courses.
At the moment, we’re working on Prodigy Scale, which will be an extension product for our annotation tool Prodigy, specifically for larger teams who want to scale up their annotation projects. I’m especially excited about the data privacy-focused architecture via a self-hosted cluster, and the new features for reviewing data, measuring annotator agreement and building annotation and model training flows interactively.
Finally, I’m also working on organizing our first real-life (!) event here in Berlin for folks working with spaCy and NLP more generally. We have a really cool mix of speakers lined up so it’s going to be a lot of fun. It’s called “spaCy IRL” and takes place on July 6 – you can find more details about it here.
Which Python libraries are your favorite (core or 3rd party)?
- IPython shell: I have to admit, I use plain Python interpreters *a lot* – mostly to try things out, quickly test and run some code, parse some text with spaCy, and so on. It was kind of a “mind blown!” moment when I discovered that IPython included an enhanced interactive shell with syntax highlighting, autocomplete and various other cool features.
- black: We’ve been slowly adding the Black auto-formatter to our Python code bases and it’s been incredibly satisfying. Combined with flake8 and auto-formatting and linting in Visual Studio Code, it’s really changed the way I write Python code.
- plac: We write a lot of command-line interfaces for our Python libraries — it’s an especially prominent feature in Prodigy. Plac provides a very small and concise way to define command-line interfaces with decorators. It works for quick scripts, but is also a good solution for long-lasting library code, as it helps you make sure your CLIs behave consistently and have decent documentation.
- FastAPI: I’ve never liked the server-side templating style of web development you get with frameworks like Django or Rails, as I’ve always wanted to write more interactive single-page applications. FastAPI is a new step in REST-only frameworks, that really takes advantage of new Python features like type hints and async/await. We’ve been switching all our services over to it, and the experience so far has been great.
Even though it’s not strictly a Python library, I’d also like to give a shoutout to Binder (and the related Jupyter ecosystem). Their components and building blocks have completely changed the way I think about executing Python code on the web, and have enabled me to build so many cool things, including spaCy’s interactive docs and my online course framework.
Can you tell us how your business, Explosion AI, came about?
My co-founder Matt left academia in 2014 to write spaCy. As the technologies he was working on became more and more viable, companies became increasingly interested in using his research code. What was really missing at the time was a library that took what works in research and made it available as a production-ready implementation. We met while Matt was in Berlin writing the first version of spaCy. We started working together soon after. The first thing I built was an interactive visualizer for the syntax of a text predicted by a statistical model.
In 2016, we founded Explosion, to focus on building developer tools for NLP and Machine Learning. For the first few months, we bootstrapped our company with consulting, before focusing full-time on our products. We’ve been pleased to avoid a lot of the distractions that come up for new software companies today, driven by the venture capital ecosystem. I actually gave a keynote about our take on running an open-source software company at EuroPython 2018, which still sums up my feelings very well.
Explosion is now very stable, based entirely on revenue from our first product, Prodigy. We’re also close to launching our second product, Prodigy Scale, and have been working with some great developers, including some new hires who’ll be joining our team soon. All this means spaCy will be well-funded going forward, with lots of cool new features to look forward to.
Thanks for doing the interview, Ines!
The post PyDev of the Week: Ines Montani appeared first on The Mouse Vs. The Python.