Commit c1311b9c authored by Jérome Perrin's avatar Jérome Perrin

component/jupyter*: update scientific python stack

This bring new versions of jupyter, numpy and other scientific python
software. This impacts software/jupyter, which will now run more recent
versions and prepares stack/erp5 for python3 support, while not
impacting the python2 version.
parent e0746734
......@@ -11,7 +11,7 @@ parts =
<= numpy-env
[ipython]
recipe = zc.recipe.egg:custom
recipe = zc.recipe.egg
egg = ipython
environment = ipython-env
setup-eggs =
......
......@@ -75,7 +75,7 @@ class ERP5Kernel(Kernel):
self.title = None
# Allowed HTTP request code list for making request to erp5 from Kernel
# This list should be to used check status_code before making requests to erp5
self.allowed_HTTP_request_code_list = range(500, 511)
self.allowed_HTTP_request_code_list = list(range(500, 511))
# Append request code 200 in the allowed HTTP status code list
self.allowed_HTTP_request_code_list.append(200)
......
......@@ -14,9 +14,8 @@ parts +=
# Always build GCC for Fortran (see openblas).
max_version = 0
[jupyter]
[jupyter:python2]
extra-eggs =
python_executable = ${buildout:bin-directory}/${:interpreter}
[download-file-base]
recipe = slapos.recipe.build:download
......@@ -46,7 +45,7 @@ context =
key develop_eggs_directory buildout:develop-eggs-directory
key eggs_directory buildout:eggs-directory
key openssl_output openssl-output:openssl
key python_executable jupyter:python_executable
key python_executable jupyter:python-executable
key jupyter_config_location jupyter-notebook-config:location
key jupyter_config_filename jupyter-notebook-config:filename
key jupyter_set_password_location jupyter-set-password:location
......@@ -59,7 +58,7 @@ context =
key custom_js_filename custom-js:filename
key monitor_template_rendered buildout:directory
[versions]
[versions:python2]
Pygments = 2.2.0
ipykernel = 4.5.2
ipython = 5.3.0
......
......@@ -15,11 +15,11 @@
[instance-jupyter-notebook]
filename = instance.cfg.in
md5sum = fd7ed44da8d8723983b8666df2971a36
md5sum = c335782940a8f3b1ff7d4280aeec336e
[jupyter-notebook-config]
filename = jupyter_notebook_config.py.jinja
md5sum = 9d579353b579b6e488ae6330c7f4ad68
md5sum = eb8ffeef340c98e5ec351d8a1d0bb14a
[jupyter-set-password]
filename = jupyter_set_password.cgi.jinja
......@@ -27,7 +27,7 @@ md5sum = ac10fbcf790bd8e58750cfdd069812d2
[erp5-kernel]
filename = ERP5kernel.py
md5sum = 7d5309fe79afbcb455c0d8181b42e56c
md5sum = da04b99b70b2e327c9e9b4cdd056098e
[kernel-json]
filename = kernel.json.jinja
......
......@@ -55,7 +55,7 @@ key_file = ${directory:etc}/jupyter_cert.key
[instance]
recipe = slapos.cookbook:wrapper
command-line =
{{ bin_directory }}/jupyter-lab
{{ bin_directory }}/jupyter-notebook
--no-browser
--ip=${instance-parameter:host}
--port=${instance-parameter:port}
......@@ -69,15 +69,19 @@ environment =
JUPYTER_PATH=${directory:jupyter_dir}
JUPYTER_CONFIG_DIR=${directory:jupyter_config_dir}
JUPYTER_RUNTIME_DIR=${directory:jupyter_runtime_dir}
JUPYTERLAB_DIR=${directory:jupyterlab-dir}
LANG=C.UTF-8
[jupyter-notebook-config]
recipe = slapos.recipe.template:jinja2
url = {{ jupyter_config_location }}/{{ jupyter_config_filename }}
output = ${directory:jupyter_config_dir}/jupyter_notebook_config.py
output = ${directory:jupyter_config_dir}/jupyter_server_config.py
context =
raw config_cfg ${buildout:directory}/knowledge0.cfg
[jupyter-notebook-config:python2]
output = ${directory:jupyter_config_dir}/jupyter_notebook_config.py
[directory]
recipe = slapos.cookbook:mkdirectory
home = ${buildout:directory}
......@@ -95,12 +99,13 @@ jupyter_runtime_dir = ${:jupyter_dir}/runtime
jupyter_custom_dir = ${:jupyter_config_dir}/custom
jupyter_nbextensions_dir = ${:jupyter_dir}/nbextensions
erp5_kernel_dir = ${:jupyter_kernel_dir}/ERP5
jupyterlab-dir = ${:jupyter_dir}/lab
[jupyter_notebook]
# This part is called like this because knowledge0.write uses the part name for
# the section name in the config file.
recipe = slapos.cookbook:zero-knowledge.write
password =
password =
filename = knowledge0.cfg
[read-knowledge0]
......@@ -136,7 +141,7 @@ output = ${directory:erp5_kernel_dir}/kernel.json
context =
raw python_executable {{ python_executable }}
raw kernel_dir ${erp5-kernel:target-directory}/{{ erp5_kernel_filename }}
key erp5_url slapconfiguration:configuration.erp5-url
key erp5_url slapconfiguration:configuration.erp5-url
raw display_name ERP5
raw language_name python
......
......@@ -2,28 +2,36 @@
This script initializes Jupyter's configuration such as passwords and other
things. It is run by IPython hence why it can use functions like get_config().
'''
import ConfigParser
import random
from notebook.auth import passwd
import os
import ssl
import sys
import six
from six.moves.configparser import ConfigParser
if six.PY3:
from jupyter_server.auth import passwd
import secrets
random_password = secrets.token_hex
else:
from notebook.auth import passwd
import random
def random_password(length=10):
result = ""
for i in range(0, length):
result = result + chr(random.randint(0, 25) + ord('a'))
return result
def random_password(length = 10):
result = ""
for i in range(0, length):
result = result + chr(random.randint(0, 25) + ord('a'))
return result
knowledge_0 = '{{ config_cfg }}'
if not os.path.exists(knowledge_0):
print "Your software does <b>not</b> embed 0-knowledge. \
This interface is useless in this case</body></html>"
print ("Your software does <b>not</b> embed 0-knowledge.\n"
"This interface is useless in this case</body></html>")
exit(0)
c = get_config()
parser = ConfigParser.ConfigParser()
parser = ConfigParser()
parser.read(knowledge_0)
if not parser.has_section("jupyter_notebook"):
......@@ -33,10 +41,50 @@ if not parser.has_option("jupyter_notebook", "password") or \
parser.get("jupyter_notebook", "password") == "":
parser.set("jupyter_notebook", "password", random_password())
c.NotebookApp.password = passwd(parser.get("jupyter_notebook", "password"))
c.NotebookApp.ssl_options = {
'ssl_version': ssl.PROTOCOL_TLSv1_2,
}
if six.PY3:
import pathlib
import jupyterlab
jupyterlab_dir = pathlib.Path(os.environ['JUPYTERLAB_DIR'])
# symlink all schemas in a folder, jupyter seems to assume that everything is installed
# in the same place.
schemas_dir = jupyterlab_dir / 'schemas'
if not schemas_dir.exists():
schemas_dir.mkdir()
for p in sys.path:
for schema in (pathlib.Path(p) / 'share' / 'jupyter' / 'lab' / 'schemas').glob('*/'):
dest = (schemas_dir / schema.name)
if dest.exists():
dest.unlink()
dest.symlink_to(schema)
c.LabServerApp.schemas_dir = str(schemas_dir)
# static really needs to be a sub-folder of $JUPYTERLAB_DIR
static = pathlib.Path(jupyterlab.__file__).parent.parent / 'share' / 'jupyter' / 'lab' / 'static'
static_dir = jupyterlab_dir / 'static'
if static_dir.exists():
static_dir.unlink()
static_dir.symlink_to(static)
c.LabServerApp.themes_dir = str(pathlib.Path(jupyterlab.__file__).parent / 'themes')
c.ServerApp.jpserver_extensions = {
'notebook': True,
'jupyter_lsp':True,
'jupyter_server_terminals': True,
'jupyterlab': True,
'notebook_shim': True,
}
c.ServerApp.password = passwd(parser.get("jupyter_notebook", "password"))
else:
c.NotebookApp.password = passwd(parser.get("jupyter_notebook", "password"))
c.NotebookApp.ssl_options = {
'ssl_version': ssl.PROTOCOL_TLSv1_2,
}
with open(knowledge_0, 'w') as file:
parser.write(file)
......@@ -3,7 +3,7 @@ extends =
../numpy/openblas.cfg
../matplotlib/buildout.cfg
../ipython/buildout.cfg
../python-cffi/buildout.cfg
../python-argon2-cffi/buildout.cfg
../python-pyzmq/buildout.cfg
../scipy/buildout.cfg
../scikit-learn/buildout.cfg
......@@ -15,10 +15,6 @@ parts =
jupyter
jupyter-notebook-scripts
[argon2-cffi]
recipe = zc.recipe.egg:custom
egg = ${:_buildout_section_name_}
setup-eggs = ${python-cffi:egg}
[jupyter-env]
<= numpy-env
......@@ -74,6 +70,7 @@ scripts =
jupyter-migrate
jupyter-troubleshoot
jupyter-run
python-executable = ${buildout:bin-directory}/${:interpreter}
[jupyter-notebook-initialized-scripts]
recipe = zc.recipe.egg:scripts
......@@ -82,9 +79,7 @@ environment = jupyter-env
scripts =
jupyter-nbconvert
jupyter-nbextension
jupyter-notebook
jupyter-serverextension
jupyter-lab
jupyter-labextension
jupyter-labhub
......
......@@ -40,6 +40,7 @@ need-matplotlibrc = ${matplotlibrc:location}
[versions]
matplotlib = 2.1.2
cycler = 0.11.0
matplotlib-inline = 0.1.6:whl
[versions:sys.version_info < (3,8)]
cycler = 0.10.0
[buildout]
extends =
../python-cffi/buildout.cfg
parts = argon2-cffi
[argon2-cffi]
recipe = zc.recipe.egg:custom
egg = ${:_buildout_section_name_}
setup-eggs = ${python-cffi:egg}
......@@ -23,9 +23,21 @@ setup-eggs =
${PyWavelets:egg}
${pillow-python:egg}
networkx
pythran
packaging
rpath =
${openblas:location}/lib
[scikit-image:python2]
setup-eggs =
${numpy:egg}
${scipy:egg}
${cython:egg}
${PyWavelets:egg}
${pillow-python:egg}
networkx
[scikit-image-repository]
recipe = slapos.recipe.build:gitclone
git-executable = ${git:location}/bin/git
......
......@@ -15,6 +15,7 @@ recipe = zc.recipe.egg:custom
egg = scikit-learn
environment = scikit-learn-env
setup-eggs =
${cython:egg}
${numpy:egg}
${scipy:egg}
rpath =
......
......@@ -19,11 +19,11 @@ md5sum = 5f39952f94095b1f12f41db76867e71e
[instance-jupyter]
filename = instance-jupyter.cfg.in
md5sum = f9a0e5a134456d74ca8b4d87862f903d
md5sum = 1812fa797b9eb687a634ebe96134b504
[jupyter-notebook-config]
filename = jupyter_notebook_config.py.jinja
md5sum = 089e4c511a3c7b110471bf41ca2695a4
md5sum = 6c03113fb53d6ba98476f3353c083984
[erp5-kernel]
filename = ERP5kernel.py
......
......@@ -73,7 +73,7 @@ key_file = ${directory:etc}/jupyter_cert.key
[instance]
recipe = slapos.cookbook:wrapper
command-line =
{{ bin_directory }}/jupyter-lab
{{ bin_directory }}/jupyter-notebook
--no-browser
--ip=${instance-parameter:host}
--port=${instance-parameter:port}
......@@ -87,16 +87,16 @@ environment =
JUPYTER_PATH=${directory:jupyter_dir}
JUPYTER_CONFIG_DIR=${directory:jupyter_config_dir}
JUPYTER_RUNTIME_DIR=${directory:jupyter_runtime_dir}
JUPYTERLAB_DIR=${directory:jupyterlab-dir}
LANG=C.UTF-8
[jupyter-password]
recipe = slapos.cookbook:generate.password
bytes = 10
[jupyter-notebook-config]
recipe = slapos.recipe.template:jinja2
url = {{ jupyter_config_location }}/{{ jupyter_config_filename }}
output = ${directory:jupyter_config_dir}/jupyter_notebook_config.py
output = ${directory:jupyter_config_dir}/jupyter_server_config.py
context =
key password jupyter-password:passwd
raw gcc_location {{ gcc_location }}
......@@ -119,6 +119,7 @@ jupyter_runtime_dir = ${:jupyter_dir}/runtime
jupyter_custom_dir = ${:jupyter_config_dir}/custom
jupyter_nbextensions_dir = ${:jupyter_dir}/nbextensions
erp5_kernel_dir = ${:jupyter_kernel_dir}/ERP5
jupyterlab-dir = ${:jupyter_dir}/lab
[request-slave-frontend-base]
recipe = slapos.cookbook:requestoptional
......@@ -194,7 +195,7 @@ output = ${directory:erp5_kernel_dir}/kernel.json
context =
raw python_executable {{ python_executable }}
raw kernel_dir ${erp5-kernel:target-directory}/{{ erp5_kernel_filename }}
key erp5_url slapconfiguration:configuration.erp5-url
key erp5_url slapconfiguration:configuration.erp5-url
raw display_name ERP5
raw language_name python
......
......@@ -2,13 +2,47 @@
This script initializes Jupyter's configuration such as passwords and other
things. It is run by IPython hence why it can use functions like get_config().
'''
import configparser
from notebook.auth import passwd
import os
import pathlib
import sys
from jupyter_server.auth import passwd
import jupyterlab
c = get_config()
c.NotebookApp.password = passwd("{{ password }}")
c.ServerApp.password = passwd("{{ password }}")
jupyterlab_dir = pathlib.Path(os.environ['JUPYTERLAB_DIR'])
# symlink all schemas in a folder, jupyter seems to assume that everything is installed
# in the same place.
schemas_dir = jupyterlab_dir / 'schemas'
if not schemas_dir.exists():
schemas_dir.mkdir()
for p in sys.path:
for schema in (pathlib.Path(p) / 'share' / 'jupyter' / 'lab' / 'schemas').glob('*/'):
dest = (schemas_dir / schema.name)
if dest.exists():
dest.unlink()
dest.symlink_to(schema)
c.LabServerApp.schemas_dir = str(schemas_dir)
# static really needs to be a sub-folder of $JUPYTERLAB_DIR
static = pathlib.Path(jupyterlab.__file__).parent.parent / 'share' / 'jupyter' / 'lab' / 'static'
static_dir = jupyterlab_dir / 'static'
if static_dir.exists():
static_dir.unlink()
static_dir.symlink_to(static)
c.LabServerApp.themes_dir = str(pathlib.Path(jupyterlab.__file__).parent / 'themes')
c.ServerApp.jpserver_extensions = {
'notebook': True,
'jupyter_lsp':True,
'jupyter_server_terminals': True,
'jupyterlab': True,
'notebook_shim': True,
}
try:
os.environ['PATH'] = "{{ gcc_location }}/bin" + os.pathsep + os.environ['PATH']
......
......@@ -47,7 +47,7 @@ class TestJupyter(InstanceTestCase):
def test(self):
connection_dict = self.computer_partition.getConnectionParameterDict()
self.assertTrue('password' in connection_dict)
self.assertIn('password', connection_dict)
password = connection_dict['password']
self.assertEqual(
......
......@@ -145,16 +145,21 @@ zc.recipe.egg = 2.0.8.dev0+slapos002
aiohttp = 3.8.5:whl
aiosignal = 1.3.1:whl
annotated-types = 0.6.0:whl
anyio = 4.3.0:whl
apache-libcloud = 2.4.0
argon2-cffi = 20.1.0
asn1crypto = 1.3.0
astor = 0.5
astor = 0.8.1
asttokens = 2.4.1:whl
async-generator = 1.10
async-lru = 2.0.4:whl
async-timeout = 4.0.3
atomicwrites = 1.4.0
atomize = 0.2.0
attrs = 23.1.0:whl
backcall = 0.2.0:whl
Babel = 2.14.0
backcall = 0.2.0
backports-abc = 0.5
backports.functools-lru-cache = 1.6.1:whl
backports.lzma = 0.0.14
......@@ -175,12 +180,14 @@ cliff = 2.8.3:whl
cmd2 = 0.7.0
collective.recipe.shelloutput = 0.1
collective.recipe.template = 2.2
comm = 0.2.1:whl
configparser = 4.0.2:whl
contextlib2 = 0.6.0.post1
croniter = 0.3.25
cryptography = 3.3.2+SlapOSPatched001
dataclasses = 0.8
dateparser = 0.7.6
debugpy = 1.8.1
decorator = 4.3.0
defusedxml = 0.7.1
distro = 1.7.0
......@@ -190,6 +197,7 @@ enum34 = 1.1.10
erp5.util = 0.4.76
et-xmlfile = 1.0.1
exceptiongroup = 1.1.3:whl
executing = 2.0.1:whl
fastjsonschema = 2.18.1
feedparser = 6.0.10
Flask = 3.0.0:whl
......@@ -203,8 +211,10 @@ gitdb = 4.0.10
GitPython = 3.1.30
greenlet = 3.0.1
h11 = 0.14.0
h5py = 2.7.1
h5py = 3.11.0
httpcore = 1.0.4:whl
httplib2 = 0.22.0
httpx = 0.27.0:whl
idna = 3.4:whl
igmp = 1.0.4
Importing = 1.10
......@@ -212,22 +222,34 @@ importlib-metadata = 6.8.0:whl
importlib-resources = 5.10.2:whl
inotify-simple = 1.1.1
ipaddress = 1.0.23
ipykernel = 5.3.4:whl
ipython = 7.16.3
ipython-genutils = 0.1.0
ipywidgets = 6.0.0
ipykernel = 6.29.3:whl
ipython = 8.18.1:whl
ipython-genutils = 0.2.0
ipywidgets = 8.1.2:whl
itsdangerous = 2.1.2
jdcal = 1.4
jedi = 0.17.2
Jinja2 = 3.1.2:whl
joblib = 1.3.2:whl
json5 = 0.9.20:whl
jsonpointer = 2.2
jsonschema = 4.17.3:whl
jupyter = 1.0.0
jupyter-client = 7.3.1
jupyter-console = 6.4.4
jupyter-core = 4.9.2
jupyterlab = 0.26.3
jupyterlab-launcher = 0.3.1
jupyterlab-pygments = 0.1.2
jupyter-client = 8.6.1:whl
jupyter-console = 6.6.3:whl
jupyter-core = 5.7.1:whl
jupyter-events = 0.6.3:whl
isoduration = 20.11.0
jupyter-lsp = 2.2.3:whl
jupyter-server = 2.10.0:whl
jupyter-server-terminals = 0.5.2:whl
jupyterlab = 4.1.3:whl
jupyterlab-launcher = 0.13.1
jupyterlab-pygments = 0.3.0:whl
jupyterlab-server = 2.24.0:whl
jupyterlab-widgets = 3.0.10:whl
arrow = 1.2.3
fqdn = 1.5.1
lock-file = 2.0
lockfile = 0.12.2:whl
lsprotocol = 2023.0.0b1:whl
......@@ -240,18 +262,20 @@ meld3 = 1.0.2
mistune = 0.8.4
mock = 3.0.5
more-itertools = 5.0.0
mpmath = 1.0.0
mpmath = 1.3.0
msgpack = 1.0.5
multidict = 6.0.4
nbclient = 0.5.1
nbclient = 0.10.0:whl
nbconvert = 6.5.4
nbformat = 5.9.2:whl
nest-asyncio = 1.5.6
netaddr = 0.7.19
netifaces = 0.10.7
notebook = 6.1.5
notebook = 7.1.2:whl
notebook-shim = 0.2.4:whl
openpyxl = 2.5.2
outcome = 1.2.0
overrides = 7.7.0
packaging = 23.2:whl
pandocfilters = 1.4.3
paramiko = 2.11.0
......@@ -265,21 +289,24 @@ pickleshare = 0.7.4
pim-dm = 1.4.0nxd002
pkgconfig = 1.5.1:whl
pkgutil-resolve-name = 1.3.10:whl
platformdirs = 4.2.0:whl
plone.recipe.command = 1.1
pluggy = 0.13.1:whl
ply = 3.11
prettytable = 0.7.2
prometheus-client = 0.9.0
prompt-toolkit = 3.0.19
prompt-toolkit = 3.0.43
psutil = 5.8.0
psycopg2 = 2.9.9
ptyprocess = 0.6.0:whl
pure-eval = 0.2.2:whl
py = 1.11.0:whl
py-mld = 1.0.3
pyasn1 = 0.4.5
pyasn1 = 0.5.1
pycparser = 2.20
pycurl = 7.45.0
pydantic = 1.9.1
pydantic = 2.6.3:whl
pydantic-core = 2.16.3:whl
pygls = 1.1.0:whl
Pygments = 2.9.0
PyNaCl = 1.3.0
......@@ -291,19 +318,23 @@ PyRSS2Gen = 1.1
PySocks = 1.7.1
pytest-runner = 5.2:whl
python-dateutil = 2.8.2:whl
python-json-logger = 2.0.7
pytz = 2022.2.1
PyYAML = 5.4.1
pyzmq = 22.3.0
qtconsole = 4.3.0
pyzmq = 24.0.1
qtconsole = 5.5.1
qtpy = 2.4.1:whl
random2 = 1.0.1
regex = 2020.9.27
requests = 2.31.0
rfc3339-validator = 0.1.4
rfc3986-validator = 0.1.1:whl
rpdb = 0.1.5
rubygemsrecipe = 0.4.4
scandir = 1.10.0
scikit-learn = 0.20.4
scikit-learn = 0.24.2
seaborn = 0.7.1
Send2Trash = 1.5.0
Send2Trash = 1.8.2:whl
setproctitle = 1.1.10
setuptools-dso = 2.10
sgmllib3k = 1.0.0
......@@ -323,6 +354,7 @@ smmap = 5.0.0
sniffio = 1.3.0
sortedcontainers = 2.4.0
soupsieve = 1.9.5
stack-data = 0.6.3:whl
statsmodels = 0.13.5+SlapOSPatched001
stevedore = 1.21.0:whl
subprocess32 = 3.5.4
......@@ -330,24 +362,28 @@ supervisor = 4.1.0
sympy = 1.1.1
terminado = 0.9.1
testpath = 0.4.4
threadpoolctl = 3.3.0:whl
tinycss2 = 1.2.1:whl
tornado = 6.1
traitlets = 5.11.2:whl
tomli = 2.0.1:whl
tornado = 6.4
traitlets = 5.14.1:whl
trio = 0.22.0
trio-websocket = 0.9.2
typeguard = 3.0.2:whl
typing-extensions = 4.8.0:whl
tzlocal = 1.5.1
unicodecsv = 0.14.1
uri-template = 1.2.0
uritemplate = 4.1.1
urllib3 = 1.26.12
wcwidth = 0.2.5
webcolors = 1.12
webencodings = 0.5.1
websocket-client = 1.5.1
websockets = 10.4
Werkzeug = 3.0.0:whl
wheel = 0.41.2:whl
widgetsnbextension = 2.0.0
widgetsnbextension = 4.0.10:whl
wsproto = 1.2.0
xlrd = 1.1.0
xml-marshaller = 1.0.2
......@@ -391,28 +427,46 @@ gevent = 20.9.0
gitdb2 = 2.0.5
GitPython = 2.1.11
greenlet = 0.4.17
h5py = 2.7.1
idna = 2.9
importlib-metadata = 1.7.0:whl
itsdangerous = 0.24
Jinja2 = 2.11.3
ipykernel = 5.3.4:whl
ipython = 7.16.3
ipython-genutils = 0.1.0
ipywidgets = 6.0.0
jsonschema = 3.0.2:whl
jupyter-client = 7.3.1
jupyter-console = 6.4.4
jupyter-core = 4.9.2
jupyterlab = 0.26.3
jupyterlab-launcher = 0.3.1
jupyterlab-pygments = 0.1.2
lxml = 4.9.1
MarkupSafe = 1.0
mpmath = 1.0.0
msgpack = 0.6.2
nbclient = 0.5.1
notebook = 6.1.5
packaging = 16.8
prompt-toolkit = 3.0.19
psycopg2 = 2.8.6
pycurl = 7.43.0
pyparsing = 2.4.7
pyrsistent = 0.16.1
pyzmq = 22.3.0
qtconsole = 4.3.0
requests = 2.27.1
scikit-learn = 0.20.4
selectors34 = 1.2
Send2Trash = 1.5.0
slapos.toolbox = 0.128.1
smmap = 0.9.0
smmap2 = 2.0.5
statsmodels = 0.11.1
tornado = 6.1
traitlets = 4.3.3
uritemplate = 3.0.0
Werkzeug = 1.0.1
widgetsnbextension = 2.0.0
zipp = 1.2.0:whl
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment