Browse Source

Fixed compatability issues with Python 3

main
Bill Ladwig 9 years ago
parent
commit
89924267da
  1. 8
      wrf_open/src/python/wrf/decorators.py
  2. 3
      wrf_open/src/python/wrf/extension.py
  3. 5
      wrf_open/src/python/wrf/interputils.py
  4. 28
      wrf_open/src/python/wrf/metadecorators.py
  5. 4
      wrf_open/src/python/wrf/routines.py
  6. 106
      wrf_open/src/python/wrf/util.py
  7. 6
      wrf_open/src/python/wrf/uvdecorator.py
  8. 16
      wrf_open/test/ipynb/WRF_python_demo.ipynb

8
wrf_open/src/python/wrf/decorators.py

@ -8,7 +8,7 @@ import numpy as np
import numpy.ma as ma import numpy.ma as ma
from .units import do_conversion, check_units from .units import do_conversion, check_units
from .util import (iter_left_indexes, viewitems, from_args, npvalues) from .util import (iter_left_indexes, viewitems, from_args, npvalues, range2)
from .config import xarray_enabled from .config import xarray_enabled
if xarray_enabled(): if xarray_enabled():
@ -86,7 +86,7 @@ def handle_left_iter(ref_var_expected_dims, ref_var_idx=-1,
return wrapped(*args, **kwargs) return wrapped(*args, **kwargs)
# Start by getting the left-most 'extra' dims # Start by getting the left-most 'extra' dims
extra_dims = [ref_var_shape[x] for x in xrange(extra_dim_num)] extra_dims = [ref_var_shape[x] for x in range2(extra_dim_num)]
out_inited = False out_inited = False
for left_idxs in iter_left_indexes(extra_dims): for left_idxs in iter_left_indexes(extra_dims):
@ -135,14 +135,14 @@ def handle_left_iter(ref_var_expected_dims, ref_var_idx=-1,
outdims = _calc_out_dims(res[0], extra_dims) outdims = _calc_out_dims(res[0], extra_dims)
if not isinstance(res[0], ma.MaskedArray): if not isinstance(res[0], ma.MaskedArray):
output = [np.empty(outdims, ref_var.dtype) output = [np.empty(outdims, ref_var.dtype)
for i in xrange(len(res))] for i in range2(len(res))]
masked = False masked = False
else: else:
output = [ma.MaskedArray( output = [ma.MaskedArray(
np.zeros(outdims, ref_var.dtype), np.zeros(outdims, ref_var.dtype),
mask=np.zeros(outdims, np.bool_), mask=np.zeros(outdims, np.bool_),
fill_value=res[0].fill_value) fill_value=res[0].fill_value)
for i in xrange(len(res))] for i in range2(len(res))]
masked = True masked = True
out_inited = True out_inited = True

3
wrf_open/src/python/wrf/extension.py

@ -16,6 +16,7 @@ from ._wrfext import (f_interpz3d, f_interp2dxy,f_interp1d,
from ._wrfcape import f_computecape from ._wrfcape import f_computecape
from .decorators import (handle_left_iter, handle_casting, from .decorators import (handle_left_iter, handle_casting,
handle_extract_transpose) handle_extract_transpose)
from .util import range2
from .uvdecorator import uvmet_left_iter from .uvdecorator import uvmet_left_iter
__all__ = ["FortranException", "computeslp", "computetk", "computetd", __all__ = ["FortranException", "computeslp", "computetk", "computetd",
@ -67,7 +68,7 @@ def computevertcross(field3d, xy, var2dz, z_var2d, missingval):
var2d = np.empty((z_var2d.size, xy.shape[0]), dtype=var2dz.dtype) var2d = np.empty((z_var2d.size, xy.shape[0]), dtype=var2dz.dtype)
var2dtmp = interp2dxy(field3d, xy) var2dtmp = interp2dxy(field3d, xy)
for i in xrange(xy.shape[0]): for i in range2(xy.shape[0]):
var2d[:,i] = interp1d(var2dtmp[:,i], var2dz[:,i], z_var2d, missingval) var2d[:,i] = interp1d(var2dtmp[:,i], var2dz[:,i], z_var2d, missingval)
return var2d return var2d

5
wrf_open/src/python/wrf/interputils.py

@ -6,6 +6,7 @@ from math import floor, ceil
import numpy as np import numpy as np
from .extension import interp2dxy from .extension import interp2dxy
from .util import range2
__all__ = ["to_positive_idxs", "calc_xy", "get_xy_z_params", "get_xy"] __all__ = ["to_positive_idxs", "calc_xy", "get_xy_z_params", "get_xy"]
@ -118,7 +119,7 @@ def calc_xy(xdim, ydim, pivot_point=None, angle=None,
dx = dx/npts dx = dx/npts
dy = dy/npts dy = dy/npts
for i in xrange(npts): for i in range2(npts):
xy[i,0] = x0 + i*dx xy[i,0] = x0 + i*dx
xy[i,1] = y0 + i*dy xy[i,1] = y0 + i*dy
@ -153,7 +154,7 @@ def get_xy_z_params(z, pivot_point=None, angle=None,
z_var2d = np.zeros((nlevels), dtype=z.dtype) z_var2d = np.zeros((nlevels), dtype=z.dtype)
z_var2d[0] = z_min z_var2d[0] = z_min
for i in xrange(1,nlevels): for i in range2(1,nlevels):
z_var2d[i] = z_var2d[0] + i*dz z_var2d[i] = z_var2d[0] + i*dz
return xy, var2dz, z_var2d return xy, var2dz, z_var2d

28
wrf_open/src/python/wrf/metadecorators.py

@ -9,7 +9,7 @@ import numpy.ma as ma
from .util import (viewkeys, viewitems, extract_vars, from .util import (viewkeys, viewitems, extract_vars,
combine_with, either, from_args, arg_location, combine_with, either, from_args, arg_location,
_is_coord_var, CoordPair, npvalues) _is_coord_var, CoordPair, npvalues, range2, ucode)
from .interputils import get_xy_z_params, get_xy from .interputils import get_xy_z_params, get_xy
from .latlonutils import ij_to_ll, ll_to_ij from .latlonutils import ij_to_ll, ll_to_ij
from .config import xarray_enabled from .config import xarray_enabled
@ -87,7 +87,7 @@ def copy_and_set_metadata(copy_varname=None, delete_attrs=None, name=None,
outattrs = OrderedDict() outattrs = OrderedDict()
if copy_varname is not None: if copy_varname is not None:
outname = unicode(var_to_copy.name) outname = ucode(var_to_copy.name)
if dimnames is not None: if dimnames is not None:
if isinstance(dimnames, combine_with): if isinstance(dimnames, combine_with):
@ -458,15 +458,15 @@ def _set_cross_meta(wrapped, instance, args, kwargs):
outattrs = OrderedDict() outattrs = OrderedDict()
outdimnames = list(field3d.dims) outdimnames = list(field3d.dims)
outcoords.update(field3d.coords) outcoords.update(field3d.coords)
for i in xrange(-3,0,1): for i in range2(-3,0,1):
outdimnames.remove(field3d.dims[i]) outdimnames.remove(field3d.dims[i])
del outcoords[field3d.dims[i]] del outcoords[field3d.dims[i]]
# Delete any lat,lon coords # Delete any lat,lon coords
for key in viewkeys(outcoords): delkeys = [key for key in viewkeys(outcoords) if _is_coord_var(key)]
if _is_coord_var(key): for key in delkeys:
del outcoords[key] del outcoords[key]
outdimnames.append("vertical") outdimnames.append("vertical")
outdimnames.append("xy") outdimnames.append("xy")
@ -481,7 +481,7 @@ def _set_cross_meta(wrapped, instance, args, kwargs):
pass pass
outcoords["xy_loc"] = ("xy", [CoordPair(xy[i,0], xy[i,1]) outcoords["xy_loc"] = ("xy", [CoordPair(xy[i,0], xy[i,1])
for i in xrange(xy.shape[-2])]) for i in range2(xy.shape[-2])])
outcoords["vertical"] = z_var2d[:] outcoords["vertical"] = z_var2d[:]
@ -549,14 +549,14 @@ def _set_line_meta(wrapped, instance, args, kwargs):
outattrs = OrderedDict() outattrs = OrderedDict()
outdimnames = list(field2d.dims) outdimnames = list(field2d.dims)
outcoords.update(field2d.coords) outcoords.update(field2d.coords)
for i in xrange(-2,0,1): for i in range2(-2,0,1):
outdimnames.remove(field2d.dims[i]) outdimnames.remove(field2d.dims[i])
del outcoords[field2d.dims[i]] del outcoords[field2d.dims[i]]
# Delete any lat,lon coords # Delete any lat,lon coords
for key in viewkeys(outcoords): delkeys = [key for key in viewkeys(outcoords) if _is_coord_var(key)]
if _is_coord_var(key): for key in delkeys:
del outcoords[key] del outcoords[key]
outdimnames.append("xy") outdimnames.append("xy")
outattrs.update(field2d.attrs) outattrs.update(field2d.attrs)
@ -570,7 +570,7 @@ def _set_line_meta(wrapped, instance, args, kwargs):
pass pass
outcoords["xy_loc"] = ("xy", [CoordPair(xy[i,0], xy[i,1]) outcoords["xy_loc"] = ("xy", [CoordPair(xy[i,0], xy[i,1])
for i in xrange(xy.shape[-2])]) for i in range2(xy.shape[-2])])
else: else:
outname = "field2d_line" outname = "field2d_line"
@ -652,7 +652,7 @@ def _set_2dxy_meta(wrapped, instance, args, kwargs):
outattrs = OrderedDict() outattrs = OrderedDict()
outdimnames = list(field3d.dims) outdimnames = list(field3d.dims)
outcoords.update(field3d.coords) outcoords.update(field3d.coords)
for i in xrange(-2,0,1): for i in range2(-2,0,1):
outdimnames.remove(field3d.dims[i]) outdimnames.remove(field3d.dims[i])
del outcoords[field3d.dims[i]] del outcoords[field3d.dims[i]]
@ -662,7 +662,7 @@ def _set_2dxy_meta(wrapped, instance, args, kwargs):
outname = "{0}_xy".format(field3d.name) outname = "{0}_xy".format(field3d.name)
outcoords["xy_loc"] = ("xy", [CoordPair(xy[i,0], xy[i,1]) outcoords["xy_loc"] = ("xy", [CoordPair(xy[i,0], xy[i,1])
for i in xrange(xy.shape[-2])]) for i in range2(xy.shape[-2])])
for key in ("MemoryOrder",): for key in ("MemoryOrder",):
try: try:

4
wrf_open/src/python/wrf/routines.py

@ -1,6 +1,6 @@
from .util import _get_iterable, is_standard_wrf_var, extract_vars from .util import _get_iterable, is_standard_wrf_var, extract_vars, viewkeys
from .cape import get_2dcape, get_3dcape from .cape import get_2dcape, get_3dcape
from .ctt import get_ctt from .ctt import get_ctt
from .dbz import get_dbz, get_max_dbz from .dbz import get_dbz, get_max_dbz
@ -143,7 +143,7 @@ def _undo_alias(alias):
return actual return actual
def _check_kargs(var, kargs): def _check_kargs(var, kargs):
for arg in kargs.iterkeys(): for arg in viewkeys(kargs):
if arg not in _VALID_KARGS[var]: if arg not in _VALID_KARGS[var]:
raise ArgumentError("'%s' is an invalid keyword " raise ArgumentError("'%s' is an invalid keyword "
"argument for '%s'" % (arg, var)) "argument for '%s'" % (arg, var))

106
wrf_open/src/python/wrf/util.py

@ -8,13 +8,17 @@ from itertools import product
from types import GeneratorType from types import GeneratorType
import datetime as dt import datetime as dt
from math import floor, copysign from math import floor, copysign
from inspect import getmodule from inspect import getmodule
try: try:
from inspect import signature from inspect import signature
except ImportError: except ImportError:
from inspect import getargspec, getargvalues from inspect import getargspec
try:
from inspect import getargvalues
except ImportError:
from inspect import getgeneratorlocals
import numpy as np import numpy as np
import numpy.ma as ma import numpy.ma as ma
@ -81,7 +85,10 @@ def _is_mapping(wrfnc):
def _generator_copy(gen): def _generator_copy(gen):
funcname = gen.__name__ funcname = gen.__name__
argvals = getargvalues(gen.gi_frame) try:
argvals = getargvalues(gen.gi_frame)
except NameError:
argvals = getgeneratorlocals(gen)
module = getmodule(gen.gi_frame) module = getmodule(gen.gi_frame)
if module is not None: if module is not None:
@ -177,17 +184,33 @@ def isstr(s):
except NameError: except NameError:
return isinstance(s, str) return isinstance(s, str)
# Python 2 rounding behavior # Python 2 rounding behavior
def _round2(x, d=0): def _round2(x, d=0):
p = 10 ** d p = 10 ** d
return float(floor((x * p) + copysign(0.5, x)))/p return float(floor((x * p) + copysign(0.5, x)))/p
def py2round(x, d=0): def py2round(x, d=0):
if version_info >= (3,): if version_info >= (3,):
return _round2(x, d) return _round2(x, d)
return round(x, d) return round(x, d)
def range2(*args):
if version_info >= (3,):
return range(*args)
return xrange(*args)
def ucode(*args, **kwargs):
if version_info >= (3, ):
return str(*args, **kwargs)
return unicode(*args, **kwargs)
# Helper to extract masked arrays from DataArrays that convert to NaN # Helper to extract masked arrays from DataArrays that convert to NaN
def npvalues(da): def npvalues(da):
@ -262,7 +285,7 @@ def _corners_moved(wrfnc, first_ll_corner, first_ur_corner, latvar, lonvar):
lons = wrfnc.variables[lonvar] lons = wrfnc.variables[lonvar]
# Need to check all times # Need to check all times
for i in xrange(lats.shape[-3]): for i in range2(lats.shape[-3]):
start_idxs = [0]*len(lats.shape) # PyNIO does not support ndim start_idxs = [0]*len(lats.shape) # PyNIO does not support ndim
start_idxs[-3] = i start_idxs[-3] = i
start_idxs = tuple(start_idxs) start_idxs = tuple(start_idxs)
@ -459,7 +482,7 @@ def _combine_dict(wrfdict, varname, timeidx, method, meta):
idx += 1 idx += 1
if xarray_enabled() and meta: if xarray_enabled() and meta:
outname = unicode(first_array.name) outname = str(first_array.name)
# Note: assumes that all entries in dict have same coords # Note: assumes that all entries in dict have same coords
outcoords = OrderedDict(first_array.coords) outcoords = OrderedDict(first_array.coords)
outdims = ["key"] + list(first_array.dims) outdims = ["key"] + list(first_array.dims)
@ -580,7 +603,7 @@ def _build_data_array(wrfnc, varname, timeidx, is_moving_domain):
varname) varname)
proj = [getproj(lats=lats[i,:], proj = [getproj(lats=lats[i,:],
lons=lons[i,:], lons=lons[i,:],
**proj_params) for i in xrange(lats.shape[0])] **proj_params) for i in range2(lats.shape[0])]
if time_coord is not None: if time_coord is not None:
coords[time_coord] = (lon_coord_var.dimensions[0], coords[time_coord] = (lon_coord_var.dimensions[0],
@ -778,14 +801,14 @@ def _cat_files(wrfseq, varname, timeidx, is_moving, squeeze, meta):
varname) varname)
projs = [getproj(lats=lats[i,:], projs = [getproj(lats=lats[i,:],
lons=lons[i,:], lons=lons[i,:],
**proj_params) for i in xrange(lats.shape[0])] **proj_params) for i in range2(lats.shape[0])]
outprojs[startidx:endidx] = np.asarray(projs, np.object)[:] outprojs[startidx:endidx] = np.asarray(projs, np.object)[:]
startidx = endidx startidx = endidx
if xarray_enabled() and meta: if xarray_enabled() and meta:
outname = unicode(first_var.name) outname = ucode(first_var.name)
outattrs = OrderedDict(first_var.attrs) outattrs = OrderedDict(first_var.attrs)
outcoords = OrderedDict(first_var.coords) outcoords = OrderedDict(first_var.coords)
outdimnames = list(first_var.dims) outdimnames = list(first_var.dims)
@ -939,7 +962,7 @@ def _join_files(wrfseq, varname, timeidx, is_moving, meta):
varname) varname)
projs = [getproj(lats=lats[i,:], projs = [getproj(lats=lats[i,:],
lons=lons[i,:], lons=lons[i,:],
**proj_params) for i in xrange(lats.shape[0])] **proj_params) for i in range2(lats.shape[0])]
outprojs[file_idx, 0:numtimes] = ( outprojs[file_idx, 0:numtimes] = (
np.asarray(projs, np.object)[:]) np.asarray(projs, np.object)[:])
@ -954,12 +977,12 @@ def _join_files(wrfseq, varname, timeidx, is_moving, meta):
outdata = np.ma.masked_values(outdata, Constants.DEFAULT_FILL) outdata = np.ma.masked_values(outdata, Constants.DEFAULT_FILL)
if xarray_enabled() and meta: if xarray_enabled() and meta:
outname = unicode(first_var.name) outname = ucode(first_var.name)
outcoords = OrderedDict(first_var.coords) outcoords = OrderedDict(first_var.coords)
outattrs = OrderedDict(first_var.attrs) outattrs = OrderedDict(first_var.attrs)
# New dimensions # New dimensions
outdimnames = ["file"] + list(first_var.dims) outdimnames = ["file"] + list(first_var.dims)
outcoords["file"] = [i for i in xrange(numfiles)] outcoords["file"] = [i for i in range2(numfiles)]
# Time needs to be multi dimensional, so use the default dimension # Time needs to be multi dimensional, so use the default dimension
del outcoords["Time"] del outcoords["Time"]
@ -1085,14 +1108,20 @@ def extract_vars(wrfnc, timeidx, varnames, method="cat", squeeze=True,
method, squeeze, cache, meta) method, squeeze, cache, meta)
for var in varlist} for var in varlist}
# Python 3 compatability
def _npbytes_to_str(var):
return (bytes(c).decode("utf-8") for c in var[:])
def _make_time(timearr): def _make_time(timearr):
return dt.datetime.strptime("".join(timearr[:]), "%Y-%m-%d_%H:%M:%S") return dt.datetime.strptime("".join(_npbytes_to_str(timearr)),
"%Y-%m-%d_%H:%M:%S")
def _file_times(wrfnc, timeidx): def _file_times(wrfnc, timeidx):
multitime = _is_multi_time_req(timeidx) multitime = _is_multi_time_req(timeidx)
if multitime: if multitime:
times = wrfnc.variables["Times"][:,:] times = wrfnc.variables["Times"][:,:]
for i in xrange(times.shape[0]): for i in range2(times.shape[0]):
yield _make_time(times[i,:]) yield _make_time(times[i,:])
else: else:
times = wrfnc.variables["Times"][timeidx,:] times = wrfnc.variables["Times"][timeidx,:]
@ -1148,7 +1177,7 @@ def get_left_indexes(ref_var, expected_dims):
if (extra_dim_num == 0): if (extra_dim_num == 0):
return [] return []
return tuple([ref_var.shape[x] for x in xrange(extra_dim_num)]) return tuple([ref_var.shape[x] for x in range2(extra_dim_num)])
def iter_left_indexes(dims): def iter_left_indexes(dims):
"""A generator which yields the iteration tuples for a sequence of """A generator which yields the iteration tuples for a sequence of
@ -1163,7 +1192,7 @@ def iter_left_indexes(dims):
- dims - a sequence of dimensions sizes (e.g. ndarry.shape) - dims - a sequence of dimensions sizes (e.g. ndarry.shape)
""" """
arg = [xrange(dim) for dim in dims] arg = [range2(dim) for dim in dims]
for idxs in product(*arg): for idxs in product(*arg):
yield idxs yield idxs
@ -1236,26 +1265,26 @@ class CoordPair(object):
def from_args(func, argnames, *args, **kwargs): def from_args(func, argnames, *args, **kwargs):
"""Parses the function args and kargs looking for the desired argument """Parses the function args and kargs looking for the desired argument
value. Otherwise, the value is taken from the default keyword argument value. Otherwise, the value is taken from the default keyword argument
using the arg spec. using the arg spec.
"""
if isstr(argnames):
arglist = [argnames]
else:
arglist = argnames
result = {}
for argname in arglist:
arg_loc = arg_location(func, argname, args, kwargs)
""" if arg_loc is not None:
if isstr(argnames): result[argname] = arg_loc[0][arg_loc[1]]
arglist = [argnames]
else: else:
arglist = argnames result[argname] = None
result = {} return result
for argname in arglist:
arg_loc = arg_location(func, argname, args, kwargs)
if arg_loc is not None:
result[argname] = arg_loc[0][arg_loc[1]]
else:
result[argname] = None
return result
def _args_to_list2(func, args, kwargs): def _args_to_list2(func, args, kwargs):
argspec = getargspec(func) argspec = getargspec(func)
@ -1281,7 +1310,7 @@ def _args_to_list3(func, args, kwargs):
bound = sig.bind(*args, **kwargs) bound = sig.bind(*args, **kwargs)
bound.apply_defaults() bound.apply_defaults()
return [x for x in bound.arguments.values] return [x for x in bound.arguments.values()]
def args_to_list(func, args, kwargs): def args_to_list(func, args, kwargs):
@ -1309,10 +1338,15 @@ def _arg_location2(func, argname, args, kwargs):
def _arg_location3(func, argname, args, kwargs): def _arg_location3(func, argname, args, kwargs):
sig = signature(func) sig = signature(func)
params = list(sig.params.keys()) params = list(sig.parameters.keys())
list_args = _args_to_list3(func, args, kwargs) list_args = _args_to_list3(func, args, kwargs)
result_idx = params.index(argname)
try:
result_idx = params.index(argname)
except ValueError:
return None
return list_args, result_idx return list_args, result_idx

6
wrf_open/src/python/wrf/uvdecorator.py

@ -6,7 +6,7 @@ import numpy as np
import wrapt import wrapt
from .destag import destagger from .destag import destagger
from .util import iter_left_indexes from .util import iter_left_indexes, range2
__all__ = ["uvmet_left_iter"] __all__ = ["uvmet_left_iter"]
@ -53,13 +53,13 @@ def uvmet_left_iter():
return wrapped(u, v, lat, lon, cen_long, cone) return wrapped(u, v, lat, lon, cen_long, cone)
# Start by getting the left-most 'extra' dims # Start by getting the left-most 'extra' dims
outdims = [u.shape[x] for x in xrange(extra_dim_num)] outdims = [u.shape[x] for x in range2(extra_dim_num)]
extra_dims = list(outdims) # Copy the left-most dims for iteration extra_dims = list(outdims) # Copy the left-most dims for iteration
# Append the right-most dimensions # Append the right-most dimensions
outdims += [2] # For u/v components outdims += [2] # For u/v components
outdims += [u.shape[x] for x in xrange(-num_right_dims,0,1)] outdims += [u.shape[x] for x in range2(-num_right_dims,0,1)]
output = np.empty(outdims, u.dtype) output = np.empty(outdims, u.dtype)

16
wrf_open/test/ipynb/WRF_python_demo.ipynb

@ -343,8 +343,8 @@
}, },
"outputs": [], "outputs": [],
"source": [ "source": [
"\n", "keys = [x for x in vard.keys()]\n",
"for key in vard.keys():\n", "for key in keys:\n",
" del vard[key]" " del vard[key]"
] ]
}, },
@ -727,7 +727,7 @@
"fig = plt.figure(figsize=(20,8))\n", "fig = plt.figure(figsize=(20,8))\n",
"ax = plt.axes([0.1,0.1,0.8,0.8])\n", "ax = plt.axes([0.1,0.1,0.8,0.8])\n",
"\n", "\n",
"p_vert.plot.contour(ax=ax, levels=[0 + 50*n for n in xrange(20)], cmap=get_cmap(\"viridis\"))\n", "p_vert.plot.contour(ax=ax, levels=[0 + 50*n for n in range(20)], cmap=get_cmap(\"viridis\"))\n",
"\n", "\n",
"\n" "\n"
] ]
@ -885,21 +885,21 @@
], ],
"metadata": { "metadata": {
"kernelspec": { "kernelspec": {
"display_name": "Python 2", "display_name": "Python 3",
"language": "python", "language": "python",
"name": "python2" "name": "python3"
}, },
"language_info": { "language_info": {
"codemirror_mode": { "codemirror_mode": {
"name": "ipython", "name": "ipython",
"version": 2 "version": 3
}, },
"file_extension": ".py", "file_extension": ".py",
"mimetype": "text/x-python", "mimetype": "text/x-python",
"name": "python", "name": "python",
"nbconvert_exporter": "python", "nbconvert_exporter": "python",
"pygments_lexer": "ipython2", "pygments_lexer": "ipython3",
"version": "2.7.11" "version": "3.5.0rc4"
} }
}, },
"nbformat": 4, "nbformat": 4,

Loading…
Cancel
Save