Skip to content

Instantly share code, notes, and snippets.

@josePhoenix
Last active September 13, 2016 22:46
Show Gist options
  • Save josePhoenix/26b80ded83d09c5b118223963a88d35e to your computer and use it in GitHub Desktop.
Save josePhoenix/26b80ded83d09c5b118223963a88d35e to your computer and use it in GitHub Desktop.
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"'3.5.2 (default, Jul 14 2016, 10:29:47) \\n[GCC 4.2.1 Compatible Apple LLVM 7.3.0 (clang-703.0.29)]'"
]
},
"execution_count": 1,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"import sys\n",
"sys.version"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"import numpy as np\n",
"from numpy import rec"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"'1.2.1'"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"import astropy\n",
"astropy.__version__"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"from astropy.io import fits"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"# example recarray\n",
"stars_rec = rec.array([(1,'Sirius', -1.45, 'A1V'),\n",
" (2,'Canopus', -0.73, 'F0Ib'),\n",
" (3,'Rigil Kent', -0.1, 'G2V')],\n",
" formats='int16,a20,float32,a10',\n",
" names='order,name,mag,Sp')"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"numpy.recarray"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"type(stars_rec)"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"stars_bintable = fits.BinTableHDU(stars_rec)"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"FITS_rec([(1, 'Sirius', -1.45, 'A1V'), (2, 'Canopus', -0.73000002, 'F0Ib'),\n",
" (3, 'Rigil Kent', -0.1, 'G2V')], \n",
" dtype=(numpy.record, [('order', '<i2'), ('name', 'S20'), ('mag', '<f4'), ('Sp', 'S10')]))"
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"stars_bintable.data"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"astropy.io.fits.fitsrec.FITS_rec"
]
},
"execution_count": 9,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"type(stars_bintable.data)"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Filename: (No file associated with this HDUList)\n",
"No. Name Type Cards Dimensions Format\n",
"0 PRIMARY PrimaryHDU 4 () \n",
"1 ImageHDU 7 (16, 16) float64 \n",
"2 BinTableHDU 16 3R x 4C ['I', '20A', 'E', '10A'] \n"
]
}
],
"source": [
"# Build up multi-extension FITS\n",
"extensions = [\n",
" fits.PrimaryHDU(),\n",
" fits.ImageHDU(np.random.randn(16, 16)),\n",
" stars_bintable,\n",
"]\n",
"hdul = fits.HDUList(extensions)\n",
"hdul.info()"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"hdul.writeto('./multi_extension_img_and_table.fits', clobber=True)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Try appending a new record to the `FITS_rec` array"
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"stars_fits_rec = stars_bintable.data"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"(1, 'Sirius', -1.45, 'A1V')"
]
},
"execution_count": 13,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"stars_fits_rec[0]"
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"new_row = np.copy(stars_fits_rec[0])"
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"array(['1', 'Sirius', '-1.45', 'A1V'], \n",
" dtype='<U6')"
]
},
"execution_count": 15,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"new_row"
]
},
{
"cell_type": "code",
"execution_count": 16,
"metadata": {
"collapsed": false
},
"outputs": [
{
"ename": "TypeError",
"evalue": "invalid type promotion",
"output_type": "error",
"traceback": [
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)",
"\u001b[0;32m<ipython-input-16-d79cc0b62aea>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m()\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mnp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstars_fits_rec\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mnew_row\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
"\u001b[0;32m/Users/jlong/homebrew/Cellar/python3/3.5.2/Frameworks/Python.framework/Versions/3.5/lib/python3.5/site-packages/numpy/lib/function_base.py\u001b[0m in \u001b[0;36mappend\u001b[0;34m(arr, values, axis)\u001b[0m\n\u001b[1;32m 4584\u001b[0m \u001b[0mvalues\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mravel\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mvalues\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 4585\u001b[0m \u001b[0maxis\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0marr\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mndim\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 4586\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mconcatenate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0marr\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mvalues\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0maxis\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0maxis\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 4587\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n",
"\u001b[0;31mTypeError\u001b[0m: invalid type promotion"
]
}
],
"source": [
"np.append(stars_fits_rec, new_row)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Okay, maybe the problem is that it's not really a FITS_rec?"
]
},
{
"cell_type": "code",
"execution_count": 17,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"numpy.ndarray"
]
},
"execution_count": 17,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"type(new_row)"
]
},
{
"cell_type": "code",
"execution_count": 18,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"astropy.io.fits.fitsrec.FITS_record"
]
},
"execution_count": 18,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"type(stars_fits_rec[0])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Try appending an existing row..."
]
},
{
"cell_type": "code",
"execution_count": 19,
"metadata": {
"collapsed": false
},
"outputs": [
{
"ename": "TypeError",
"evalue": "invalid type promotion",
"output_type": "error",
"traceback": [
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)",
"\u001b[0;32m<ipython-input-19-5852a3c4b04f>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m()\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mnp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstars_fits_rec\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstars_fits_rec\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
"\u001b[0;32m/Users/jlong/homebrew/Cellar/python3/3.5.2/Frameworks/Python.framework/Versions/3.5/lib/python3.5/site-packages/numpy/lib/function_base.py\u001b[0m in \u001b[0;36mappend\u001b[0;34m(arr, values, axis)\u001b[0m\n\u001b[1;32m 4584\u001b[0m \u001b[0mvalues\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mravel\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mvalues\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 4585\u001b[0m \u001b[0maxis\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0marr\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mndim\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 4586\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mconcatenate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0marr\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mvalues\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0maxis\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0maxis\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 4587\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n",
"\u001b[0;31mTypeError\u001b[0m: invalid type promotion"
]
}
],
"source": [
"np.append(stars_fits_rec, stars_fits_rec[0])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# For comparison, the same thing with NumPy `recarray`"
]
},
{
"cell_type": "code",
"execution_count": 20,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"stars_rec_copy = stars_rec.copy()"
]
},
{
"cell_type": "code",
"execution_count": 21,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"new_row = np.copy(stars_rec_copy[0])"
]
},
{
"cell_type": "code",
"execution_count": 22,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"array([(1, b'Sirius', -1.4500000476837158, b'A1V'),\n",
" (2, b'Canopus', -0.7300000190734863, b'F0Ib'),\n",
" (3, b'Rigil Kent', -0.10000000149011612, b'G2V'),\n",
" (1, b'Sirius', -1.4500000476837158, b'A1V')], \n",
" dtype=(numpy.record, [('order', '<i2'), ('name', 'S20'), ('mag', '<f4'), ('Sp', 'S10')]))"
]
},
"execution_count": 22,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"np.append(stars_rec_copy, new_row)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Instead, there's a workaround with `astropy.table.Table`"
]
},
{
"cell_type": "code",
"execution_count": 23,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"from astropy.table import Table"
]
},
{
"cell_type": "code",
"execution_count": 24,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/html": [
"&lt;Table length=3&gt;\n",
"<table id=\"table4410908288\" class=\"table-striped table-bordered table-condensed\">\n",
"<thead><tr><th>order</th><th>name</th><th>mag</th><th>Sp</th></tr></thead>\n",
"<thead><tr><th>int16</th><th>str20</th><th>float32</th><th>str10</th></tr></thead>\n",
"<tr><td>1</td><td>Sirius</td><td>-1.45</td><td>A1V</td></tr>\n",
"<tr><td>2</td><td>Canopus</td><td>-0.73</td><td>F0Ib</td></tr>\n",
"<tr><td>3</td><td>Rigil Kent</td><td>-0.1</td><td>G2V</td></tr>\n",
"</table>"
],
"text/plain": [
"<Table length=3>\n",
"order name mag Sp \n",
"int16 str20 float32 str10\n",
"----- ---------- ------- -----\n",
" 1 Sirius -1.45 A1V\n",
" 2 Canopus -0.73 F0Ib\n",
" 3 Rigil Kent -0.1 G2V"
]
},
"execution_count": 24,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"stars_tbl = Table.read('./multi_extension_img_and_table.fits', hdu=2)\n",
"stars_tbl"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Note this uses a different keyword argument from `getdata`:"
]
},
{
"cell_type": "code",
"execution_count": 25,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"FITS_rec([(1, 'Sirius', -1.45, 'A1V'), (2, 'Canopus', -0.73000002, 'F0Ib'),\n",
" (3, 'Rigil Kent', -0.1, 'G2V')], \n",
" dtype=(numpy.record, [('order', '>i2'), ('name', 'S20'), ('mag', '>f4'), ('Sp', 'S10')]))"
]
},
"execution_count": 25,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"fits.getdata('./multi_extension_img_and_table.fits', ext=2)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Appending rows to an Astropy table is much easier!"
]
},
{
"cell_type": "code",
"execution_count": 26,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"stars_tbl.add_row([4, 'Deneb', 1.25, 'A2Ia'])"
]
},
{
"cell_type": "code",
"execution_count": 27,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/html": [
"&lt;Table length=4&gt;\n",
"<table id=\"table4410908288\" class=\"table-striped table-bordered table-condensed\">\n",
"<thead><tr><th>order</th><th>name</th><th>mag</th><th>Sp</th></tr></thead>\n",
"<thead><tr><th>int16</th><th>str20</th><th>float32</th><th>str10</th></tr></thead>\n",
"<tr><td>1</td><td>Sirius</td><td>-1.45</td><td>A1V</td></tr>\n",
"<tr><td>2</td><td>Canopus</td><td>-0.73</td><td>F0Ib</td></tr>\n",
"<tr><td>3</td><td>Rigil Kent</td><td>-0.1</td><td>G2V</td></tr>\n",
"<tr><td>4</td><td>Deneb</td><td>1.25</td><td>A2Ia</td></tr>\n",
"</table>"
],
"text/plain": [
"<Table length=4>\n",
"order name mag Sp \n",
"int16 str20 float32 str10\n",
"----- ---------- ------- -----\n",
" 1 Sirius -1.45 A1V\n",
" 2 Canopus -0.73 F0Ib\n",
" 3 Rigil Kent -0.1 G2V\n",
" 4 Deneb 1.25 A2Ia"
]
},
"execution_count": 27,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"stars_tbl"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"However, writing is not as easy, especially when updating a Mulit-Extension FITS file."
]
},
{
"cell_type": "code",
"execution_count": 28,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"new_bintable_hdu = fits.table_to_hdu(stars_tbl)"
]
},
{
"cell_type": "code",
"execution_count": 29,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Filename: ./multi_extension_img_and_table.fits\n",
"No. Name Type Cards Dimensions Format\n",
"0 PRIMARY PrimaryHDU 4 () \n",
"1 ImageHDU 7 (16, 16) float64 \n",
"2 BinTableHDU 16 3R x 4C [I, 20A, E, 10A] \n"
]
}
],
"source": [
"hdul = fits.open('./multi_extension_img_and_table.fits')\n",
"hdul.info()"
]
},
{
"cell_type": "code",
"execution_count": 30,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"hdul[2] = new_bintable_hdu"
]
},
{
"cell_type": "code",
"execution_count": 31,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Filename: ./multi_extension_img_and_table.fits\n",
"No. Name Type Cards Dimensions Format\n",
"0 PRIMARY PrimaryHDU 4 () \n",
"1 ImageHDU 7 (16, 16) float64 \n",
"2 BinTableHDU 16 4R x 4C ['I', '20A', 'E', '10A'] \n"
]
}
],
"source": [
"hdul.info()"
]
},
{
"cell_type": "code",
"execution_count": 32,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"hdul.writeto('./multi_extension_img_and_table-updated.fits', clobber=True)"
]
},
{
"cell_type": "code",
"execution_count": 33,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/html": [
"&lt;Table length=4&gt;\n",
"<table id=\"table4410905712\" class=\"table-striped table-bordered table-condensed\">\n",
"<thead><tr><th>order</th><th>name</th><th>mag</th><th>Sp</th></tr></thead>\n",
"<thead><tr><th>int16</th><th>str20</th><th>float32</th><th>str10</th></tr></thead>\n",
"<tr><td>1</td><td>Sirius</td><td>-1.45</td><td>A1V</td></tr>\n",
"<tr><td>2</td><td>Canopus</td><td>-0.73</td><td>F0Ib</td></tr>\n",
"<tr><td>3</td><td>Rigil Kent</td><td>-0.1</td><td>G2V</td></tr>\n",
"<tr><td>4</td><td>Deneb</td><td>1.25</td><td>A2Ia</td></tr>\n",
"</table>"
],
"text/plain": [
"<Table length=4>\n",
"order name mag Sp \n",
"int16 str20 float32 str10\n",
"----- ---------- ------- -----\n",
" 1 Sirius -1.45 A1V\n",
" 2 Canopus -0.73 F0Ib\n",
" 3 Rigil Kent -0.1 G2V\n",
" 4 Deneb 1.25 A2Ia"
]
},
"execution_count": 33,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"Table.read('./multi_extension_img_and_table-updated.fits', hdu=2)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Alternatively, casting to `numpy.recarray` and back works"
]
},
{
"cell_type": "code",
"execution_count": 34,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"temp_rec_arr = np.asarray(fits.getdata('./multi_extension_img_and_table.fits', ext=2))"
]
},
{
"cell_type": "code",
"execution_count": 35,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"array([(1, b'Sirius', -1.4500000476837158, b'A1V'),\n",
" (2, b'Canopus', -0.7300000190734863, b'F0Ib'),\n",
" (3, b'Rigil Kent', -0.10000000149011612, b'G2V'),\n",
" (4, b'Deneb', 1.25, b'A2Ia')], \n",
" dtype=(numpy.record, [('order', '>i2'), ('name', 'S20'), ('mag', '>f4'), ('Sp', 'S10')]))"
]
},
"execution_count": 35,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"new_rec_arr = np.append(temp_rec_arr, np.array([(4, 'Deneb', 1.25, 'A2Ia')], dtype=temp_rec_arr.dtype))\n",
"new_rec_arr"
]
},
{
"cell_type": "code",
"execution_count": 36,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Filename: ./multi_extension_img_and_table.fits\n",
"No. Name Type Cards Dimensions Format\n",
"0 PRIMARY PrimaryHDU 4 () \n",
"1 ImageHDU 7 (16, 16) float64 \n",
"2 BinTableHDU 16 4R x 4C ['I', '20A', 'E', '10A'] \n"
]
}
],
"source": [
"# reusing the open FITS file in hdul from above\n",
"hdul[2] = fits.BinTableHDU(new_rec_arr)\n",
"hdul.info() # ensure it's still four rows"
]
},
{
"cell_type": "code",
"execution_count": 37,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"hdul.writeto('./multi_extension_img_and_table-updated-2.fits', clobber=True)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.5.2"
}
},
"nbformat": 4,
"nbformat_minor": 0
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment