diff --git a/docs/e2b.md b/docs/e2b.md index 69d2f970..8ef4739b 100644 --- a/docs/e2b.md +++ b/docs/e2b.md @@ -64,8 +64,11 @@ for the truth. ## The code-interpreter flavor `e2b-ci` is built from e2b's own recipe, `e2b-dev/code-interpreter` -`template/` at `f56a1edf75`, vendored under `os-image/e2b-ci/code-interpreter/` -with its license. On top of `e2b-rt`'s contents it runs a Jupyter server on +`template/` at the `CI_RECIPE_SHA` the Dockerfile pins: the build fetches that +commit's archive, checks its sha256, and applies the one local change listed in +`os-image/e2b-ci/PATCHES`; e2b's license ships in the image under +`/usr/share/doc/e2b-code-interpreter/`. Moving to a newer recipe is a bump of the +two `CI_RECIPE_*` args plus a refresh of the constraints files. On top of `e2b-rt`'s contents it runs a Jupyter server on loopback 8888 and the code-interpreter API on 49999, both enabled at boot, and the guard drops off-guest traffic to 49983, 49999 and 8888. Only the python kernel ships: the recipe installs its javascript, R and Java kernels from @@ -74,7 +77,10 @@ dependency, moving release archives), so they wait for a reproducible source. Every pip dependency is pinned by `constraints.txt` and `server-constraints.txt` beside the Dockerfile, and bytecode is compiled hash-checked at the end, so a -rebuild of one commit against the same base yields the same layer. +rebuild of one commit against the same base yields the same layer. That holds +for the OCI and registry exporters with `SOURCE_DATE_EPOCH` and +`rewrite-timestamp=true`, as the image workflow builds; the `docker` exporter +ignores `rewrite-timestamp`, so a local reproducibility check exports OCI. The pool's warmup waits on both daemons: diff --git a/os-image/e2b-ci/24.04/Dockerfile b/os-image/e2b-ci/24.04/Dockerfile index df08940f..c00c3416 100644 --- a/os-image/e2b-ci/24.04/Dockerfile +++ b/os-image/e2b-ci/24.04/Dockerfile @@ -1,25 +1,28 @@ # e2b's code interpreter on the e2b data plane: envd, a Jupyter server and the # code-interpreter API on 49999, both started at boot. The recipe is -# e2b-dev/code-interpreter template/ at f56a1edf75, vendored under -# code-interpreter/ with its LICENSE; constraints.txt and -# server-constraints.txt pin the full sets it resolves to, so a rebuild of one -# commit installs the same packages. Python kernel only: the recipe's -# javascript, R and Java kernels install from unpinned sources. +# e2b-dev/code-interpreter template/ at CI_RECIPE_SHA, fetched and checksummed +# here, with PATCHES applied; constraints.txt and server-constraints.txt pin the +# full sets it resolves to, so a rebuild of one commit installs the same +# packages. Python kernel only: the recipe's javascript, R and Java kernels +# install from unpinned sources. # # amd64 only: e2b publishes no arm64 build of envd. ARG BASE_IMAGE=ghcr.io/cocoonstack/sandbox/base:24.04 ARG ENVD_VERSION=v0.8.0 ARG ENVD_SHA256=752cfbc7ba66649ec1bffdc9b59529dd3527e7d3899c69351e05bd21dfc0dbf0 +ARG CI_RECIPE_SHA=f56a1edf750e20a96f847df57e0063eeeb5e13c3 +ARG CI_RECIPE_SHA256=3af064c471e41f72a3c56920f61219c4259ef4aacf466f3e8687b5af30d68c48 FROM ${BASE_IMAGE} AS src ARG ENVD_VERSION ARG ENVD_SHA256 +ARG CI_RECIPE_SHA +ARG CI_RECIPE_SHA256 ENV DEBIAN_FRONTEND=noninteractive # [repro] nothing writes a .pyc until compileall writes them all, hash-checked and with fixed set order. ENV PYTHONDONTWRITEBYTECODE=1 PYTHONHASHSEED=0 -COPY code-interpreter /tmp/ci -COPY constraints.txt server-constraints.txt /tmp/ci/ +COPY constraints.txt server-constraints.txt javascript-context.patch /tmp/ci/ COPY systemd/jupyter.service systemd/code-interpreter.service /etc/systemd/system/ RUN curl -fsSL -o /usr/local/bin/envd \ @@ -28,8 +31,14 @@ RUN curl -fsSL -o /usr/local/bin/envd \ chmod 0755 /usr/local/bin/envd && \ echo "${ENVD_VERSION#v}" > /etc/envd-version && \ apt-get update && apt-get install -y --no-install-recommends \ - nftables sudo jq util-linux fonts-noto-cjk build-essential \ + nftables sudo jq util-linux fonts-noto-cjk build-essential patch \ python3 python3-venv python3-dev && \ + curl -fsSL -o /tmp/ci/recipe.tar.gz \ + "https://github.com/e2b-dev/code-interpreter/archive/${CI_RECIPE_SHA}.tar.gz" && \ + echo "${CI_RECIPE_SHA256} /tmp/ci/recipe.tar.gz" | sha256sum -c - && \ + tar -xzf /tmp/ci/recipe.tar.gz -C /tmp/ci --strip-components=1 \ + "code-interpreter-${CI_RECIPE_SHA}/template" "code-interpreter-${CI_RECIPE_SHA}/LICENSE" && \ + patch -d /tmp/ci -p1 < /tmp/ci/javascript-context.patch && \ # [envd] loopback only, as in e2b-rt; uvicorn binds the interpreter API to 0.0.0.0 too. printf '%s\n' \ 'table inet e2bguard {' \ @@ -75,17 +84,18 @@ RUN curl -fsSL -o /usr/local/bin/envd \ useradd -m -s /bin/bash user && \ echo 'user ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/user && chmod 0440 /etc/sudoers.d/user && \ python3 -m venv /opt/python && \ - /opt/python/bin/pip install --no-cache-dir --no-compile -r /tmp/ci/requirements.txt -c /tmp/ci/constraints.txt && \ + /opt/python/bin/pip install --no-cache-dir --no-compile -r /tmp/ci/template/requirements.txt -c /tmp/ci/constraints.txt && \ /opt/python/bin/python -m ipykernel install --sys-prefix --name python3 && \ - mv /tmp/ci/server /root/.server && \ + mv /tmp/ci/template/server /root/.server && \ python3 -m venv /root/.server/.venv && \ /root/.server/.venv/bin/pip install --no-cache-dir --no-compile -r /root/.server/requirements.txt -c /tmp/ci/server-constraints.txt && \ /opt/python/bin/python -m compileall -q -f -j 1 --invalidation-mode unchecked-hash /opt/python/lib /root/.server && \ - install -D -m 0644 /tmp/ci/matplotlibrc /root/.config/matplotlib/.matplotlibrc && \ - install -D -m 0755 /tmp/ci/jupyter-healthcheck.sh /root/.jupyter/jupyter-healthcheck.sh && \ - install -D -m 0644 /tmp/ci/jupyter_server_config.py /root/.jupyter/jupyter_server_config.py && \ - install -D -m 0644 /tmp/ci/ipython_kernel_config.py /root/.ipython/profile_default/ipython_kernel_config.py && \ - install -D -m 0644 -t /root/.ipython/profile_default/startup /tmp/ci/startup_scripts/*.py && \ + install -D -m 0644 /tmp/ci/template/matplotlibrc /root/.config/matplotlib/.matplotlibrc && \ + install -D -m 0755 /tmp/ci/template/jupyter-healthcheck.sh /root/.jupyter/jupyter-healthcheck.sh && \ + install -D -m 0644 /tmp/ci/template/jupyter_server_config.py /root/.jupyter/jupyter_server_config.py && \ + install -D -m 0644 /tmp/ci/template/ipython_kernel_config.py /root/.ipython/profile_default/ipython_kernel_config.py && \ + install -D -m 0644 -t /root/.ipython/profile_default/startup /tmp/ci/template/startup_scripts/*.py && \ + install -D -m 0644 /tmp/ci/LICENSE /usr/share/doc/e2b-code-interpreter/LICENSE && \ systemctl enable envd-guard.service envd.service jupyter.service code-interpreter.service && \ rm -rf /tmp/* /var/lib/apt/lists/* /var/log/apt /var/log/dpkg.log /var/log/alternatives.log \ /var/cache/ldconfig/aux-cache /var/cache/debconf/*-old /root/.cache diff --git a/os-image/e2b-ci/PATCHES b/os-image/e2b-ci/PATCHES new file mode 100644 index 00000000..2aaefdc0 --- /dev/null +++ b/os-image/e2b-ci/PATCHES @@ -0,0 +1,5 @@ +javascript-context.patch applies to e2b-dev/code-interpreter at CI_RECIPE_SHA +and is re-applied on every recipe bump. template/server/main.py creates the +default javascript context only when Jupyter lists a javascript kernelspec; +this flavor ships the python kernel alone, and upstream's server otherwise +fails its startup on the missing kernel. diff --git a/os-image/e2b-ci/code-interpreter/LICENSE b/os-image/e2b-ci/code-interpreter/LICENSE deleted file mode 100644 index 261eeb9e..00000000 --- a/os-image/e2b-ci/code-interpreter/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/os-image/e2b-ci/code-interpreter/PATCHES b/os-image/e2b-ci/code-interpreter/PATCHES deleted file mode 100644 index c6cc9023..00000000 --- a/os-image/e2b-ci/code-interpreter/PATCHES +++ /dev/null @@ -1,7 +0,0 @@ -Vendored from e2b-dev/code-interpreter template/ at f56a1edf75 (Apache-2.0, see -LICENSE). Local changes: - -- requirements.txt: e2b_charts pinned to 1.0.0 (upstream leaves it unpinned). -- server/main.py: the default javascript context is created only when Jupyter - lists a javascript kernelspec; this flavor ships the python kernel alone, and - upstream's server otherwise fails its startup on the missing kernel. diff --git a/os-image/e2b-ci/code-interpreter/ipython_kernel_config.py b/os-image/e2b-ci/code-interpreter/ipython_kernel_config.py deleted file mode 100644 index 26900faa..00000000 --- a/os-image/e2b-ci/code-interpreter/ipython_kernel_config.py +++ /dev/null @@ -1,969 +0,0 @@ -# Configuration file for ipython-kernel. - -c = get_config() # noqa - -## Set the color scheme (NoColor, Neutral, Linux, or LightBG). -# Choices: any of ['Neutral', 'NoColor', 'LightBG', 'Linux'] (case-insensitive) -# Default: 'Neutral' -c.InteractiveShell.colors = "NoColor" - - -# ------------------------------------------------------------------------------ -# ConnectionFileMixin(LoggingConfigurable) configuration -# ------------------------------------------------------------------------------ -## Mixin for configurable classes that work with connection files - -## JSON file in which to store connection info [default: kernel-.json] -# -# This file will contain the IP, ports, and authentication key needed to connect -# clients to this kernel. By default, this file will be created in the security dir -# of the current profile, but can be specified by absolute path. -# Default: '' -# c.ConnectionFileMixin.connection_file = '' - -## set the control (ROUTER) port [default: random] -# Default: 0 -# c.ConnectionFileMixin.control_port = 0 - -## set the heartbeat port [default: random] -# Default: 0 -# c.ConnectionFileMixin.hb_port = 0 - -## set the iopub (PUB) port [default: random] -# Default: 0 -# c.ConnectionFileMixin.iopub_port = 0 - -## Set the kernel's IP address [default localhost]. -# If the IP address is something other than localhost, then -# Consoles on other machines will be able to connect -# to the Kernel, so be careful! -# Default: '' -# c.ConnectionFileMixin.ip = '' - -## set the shell (ROUTER) port [default: random] -# Default: 0 -# c.ConnectionFileMixin.shell_port = 0 - -## set the stdin (ROUTER) port [default: random] -# Default: 0 -# c.ConnectionFileMixin.stdin_port = 0 - -# Choices: any of ['tcp', 'ipc'] (case-insensitive) -# Default: 'tcp' -# c.ConnectionFileMixin.transport = 'tcp' - -# ------------------------------------------------------------------------------ -# InteractiveShellApp(Configurable) configuration -# ------------------------------------------------------------------------------ -## A Mixin for applications that start InteractiveShell instances. -# -# Provides configurables for loading extensions and executing files -# as part of configuring a Shell environment. -# -# The following methods should be called by the :meth:`initialize` method -# of the subclass: -# -# - :meth:`init_path` -# - :meth:`init_shell` (to be implemented by the subclass) -# - :meth:`init_gui_pylab` -# - :meth:`init_extensions` -# - :meth:`init_code` - -## Execute the given command string. -# Default: '' -# c.InteractiveShellApp.code_to_run = '' - -## Run the file referenced by the PYTHONSTARTUP environment -# variable at IPython startup. -# Default: True -# c.InteractiveShellApp.exec_PYTHONSTARTUP = True - -## List of files to run at IPython startup. -# Default: [] -# c.InteractiveShellApp.exec_files = [] - -## lines of code to run at IPython startup. -# Default: [] -# c.InteractiveShellApp.exec_lines = [] - -## A list of dotted module names of IPython extensions to load. -# Default: [] -# c.InteractiveShellApp.extensions = [] - -## Dotted module name(s) of one or more IPython extensions to load. -# -# For specifying extra extensions to load on the command-line. -# -# .. versionadded:: 7.10 -# Default: [] -# c.InteractiveShellApp.extra_extensions = [] - -## A file to be run -# Default: '' -# c.InteractiveShellApp.file_to_run = '' - -## Enable GUI event loop integration with any of ('asyncio', 'glut', 'gtk', -# 'gtk2', 'gtk3', 'gtk4', 'osx', 'pyglet', 'qt', 'qt5', 'qt6', 'tk', 'wx', -# 'gtk2', 'qt4'). -# Choices: any of ['asyncio', 'glut', 'gtk', 'gtk2', 'gtk3', 'gtk4', 'osx', 'pyglet', 'qt', 'qt5', 'qt6', 'tk', 'wx', 'gtk2', 'qt4'] (case-insensitive) or None -# Default: None -# c.InteractiveShellApp.gui = None - -## Should variables loaded at startup (by startup files, exec_lines, etc.) -# be hidden from tools like %who? -# Default: True -# c.InteractiveShellApp.hide_initial_ns = True - -## If True, IPython will not add the current working directory to sys.path. -# When False, the current working directory is added to sys.path, allowing imports -# of modules defined in the current directory. -# Default: False -# c.InteractiveShellApp.ignore_cwd = False - -## Configure matplotlib for interactive use with -# the default matplotlib backend. -# Choices: any of ['auto', 'agg', 'gtk', 'gtk3', 'gtk4', 'inline', 'ipympl', 'nbagg', 'notebook', 'osx', 'pdf', 'ps', 'qt', 'qt4', 'qt5', 'qt6', 'svg', 'tk', 'webagg', 'widget', 'wx'] (case-insensitive) or None -# Default: None -# c.InteractiveShellApp.matplotlib = None - -## Run the module as a script. -# Default: '' -# c.InteractiveShellApp.module_to_run = '' - -## Pre-load matplotlib and numpy for interactive use, -# selecting a particular matplotlib backend and loop integration. -# Choices: any of ['auto', 'agg', 'gtk', 'gtk3', 'gtk4', 'inline', 'ipympl', 'nbagg', 'notebook', 'osx', 'pdf', 'ps', 'qt', 'qt4', 'qt5', 'qt6', 'svg', 'tk', 'webagg', 'widget', 'wx'] (case-insensitive) or None -# Default: None -# c.InteractiveShellApp.pylab = None - -## If true, IPython will populate the user namespace with numpy, pylab, etc. -# and an ``import *`` is done from numpy and pylab, when using pylab mode. -# -# When False, pylab mode should not import any names into the user -# namespace. -# Default: True -# c.InteractiveShellApp.pylab_import_all = True - -## Reraise exceptions encountered loading IPython extensions? -# Default: False -# c.InteractiveShellApp.reraise_ipython_extension_failures = False - -# ------------------------------------------------------------------------------ -# Application(SingletonConfigurable) configuration -# ------------------------------------------------------------------------------ -## This is an application. - -## The date format used by logging formatters for %(asctime)s -# Default: '%Y-%m-%d %H:%M:%S' -# c.Application.log_datefmt = '%Y-%m-%d %H:%M:%S' - -## The Logging format template -# Default: '[%(name)s]%(highlevel)s %(message)s' -# c.Application.log_format = '[%(name)s]%(highlevel)s %(message)s' - -## Set the log level by value or name. -# Choices: any of [0, 10, 20, 30, 40, 50, 'DEBUG', 'INFO', 'WARN', 'ERROR', 'CRITICAL'] -# Default: 30 -# c.Application.log_level = 30 - -## Configure additional log handlers. -# -# The default stderr logs handler is configured by the log_level, log_datefmt -# and log_format settings. -# -# This configuration can be used to configure additional handlers (e.g. to -# output the log to a file) or for finer control over the default handlers. -# -# If provided this should be a logging configuration dictionary, for more -# information see: -# https://docs.python.org/3/library/logging.config.html#logging-config- -# dictschema -# -# This dictionary is merged with the base logging configuration which defines -# the following: -# -# * A logging formatter intended for interactive use called -# ``console``. -# * A logging handler that writes to stderr called -# ``console`` which uses the formatter ``console``. -# * A logger with the name of this application set to ``DEBUG`` -# level. -# -# This example adds a new handler that writes to a file: -# -# .. code-block:: python -# -# c.Application.logging_config = { -# "handlers": { -# "file": { -# "class": "logging.FileHandler", -# "level": "DEBUG", -# "filename": "", -# } -# }, -# "loggers": { -# "": { -# "level": "DEBUG", -# # NOTE: if you don't list the default "console" -# # handler here then it will be disabled -# "handlers": ["console", "file"], -# }, -# }, -# } -# Default: {} -# c.Application.logging_config = {} - -## Instead of starting the Application, dump configuration to stdout -# Default: False -# c.Application.show_config = False - -## Instead of starting the Application, dump configuration to stdout (as JSON) -# Default: False -# c.Application.show_config_json = False - -# ------------------------------------------------------------------------------ -# BaseIPythonApplication(Application) configuration -# ------------------------------------------------------------------------------ -# Default: False -# c.BaseIPythonApplication.add_ipython_dir_to_sys_path = False - -## Whether to create profile dir if it doesn't exist -# Default: False -# c.BaseIPythonApplication.auto_create = False - -## Whether to install the default config files into the profile dir. -# If a new profile is being created, and IPython contains config files for that -# profile, then they will be staged into the new directory. Otherwise, -# default config files will be automatically generated. -# Default: False -# c.BaseIPythonApplication.copy_config_files = False - -## Path to an extra config file to load. -# -# If specified, load this config file in addition to any other IPython -# config. -# Default: '' -# c.BaseIPythonApplication.extra_config_file = '' - -## The name of the IPython directory. This directory is used for logging -# configuration (through profiles), history storage, etc. The default is usually -# $HOME/.ipython. This option can also be specified through the environment -# variable IPYTHONDIR. -# Default: '' -# c.BaseIPythonApplication.ipython_dir = '' - -## The date format used by logging formatters for %(asctime)s -# See also: Application.log_datefmt -# c.BaseIPythonApplication.log_datefmt = '%Y-%m-%d %H:%M:%S' - -## The Logging format template -# See also: Application.log_format -# c.BaseIPythonApplication.log_format = '[%(name)s]%(highlevel)s %(message)s' - -## Set the log level by value or name. -# See also: Application.log_level -# c.BaseIPythonApplication.log_level = 30 - -## -# See also: Application.logging_config -# c.BaseIPythonApplication.logging_config = {} - -## Whether to overwrite existing config files when copying -# Default: False -# c.BaseIPythonApplication.overwrite = False - -## The IPython profile to use. -# Default: 'default' -# c.BaseIPythonApplication.profile = 'default' - -## Instead of starting the Application, dump configuration to stdout -# See also: Application.show_config -# c.BaseIPythonApplication.show_config = False - -## Instead of starting the Application, dump configuration to stdout (as JSON) -# See also: Application.show_config_json -# c.BaseIPythonApplication.show_config_json = False - -## Create a massive crash report when IPython encounters what may be an -# internal error. The default is to append a short message to the -# usual traceback -# Default: False -# c.BaseIPythonApplication.verbose_crash = False - -# ------------------------------------------------------------------------------ -# IPKernelApp(BaseIPythonApplication, InteractiveShellApp, ConnectionFileMixin) configuration -# ------------------------------------------------------------------------------ -## The IPYKernel application class. - -# See also: BaseIPythonApplication.add_ipython_dir_to_sys_path -# c.IPKernelApp.add_ipython_dir_to_sys_path = False - -## Whether to create profile dir if it doesn't exist -# See also: BaseIPythonApplication.auto_create -# c.IPKernelApp.auto_create = False - -## Attempt to capture and forward low-level output, e.g. produced by Extension -# libraries. -# Default: True -# c.IPKernelApp.capture_fd_output = True - -## Execute the given command string. -# See also: InteractiveShellApp.code_to_run -# c.IPKernelApp.code_to_run = '' - -## JSON file in which to store connection info [default: kernel-.json] -# See also: ConnectionFileMixin.connection_file -# c.IPKernelApp.connection_file = '' - -## set the control (ROUTER) port [default: random] -# See also: ConnectionFileMixin.control_port -# c.IPKernelApp.control_port = 0 - -## Whether to install the default config files into the profile dir. -# See also: BaseIPythonApplication.copy_config_files -# c.IPKernelApp.copy_config_files = False - -## The importstring for the DisplayHook factory -# Default: 'ipykernel.displayhook.ZMQDisplayHook' -# c.IPKernelApp.displayhook_class = 'ipykernel.displayhook.ZMQDisplayHook' - -## Run the file referenced by the PYTHONSTARTUP environment -# See also: InteractiveShellApp.exec_PYTHONSTARTUP -# c.IPKernelApp.exec_PYTHONSTARTUP = True - -## List of files to run at IPython startup. -# See also: InteractiveShellApp.exec_files -# c.IPKernelApp.exec_files = [] - -## lines of code to run at IPython startup. -# See also: InteractiveShellApp.exec_lines -# c.IPKernelApp.exec_lines = [] - -## A list of dotted module names of IPython extensions to load. -# See also: InteractiveShellApp.extensions -# c.IPKernelApp.extensions = [] - -## Path to an extra config file to load. -# See also: BaseIPythonApplication.extra_config_file -# c.IPKernelApp.extra_config_file = '' - -## -# See also: InteractiveShellApp.extra_extensions -# c.IPKernelApp.extra_extensions = [] - -## A file to be run -# See also: InteractiveShellApp.file_to_run -# c.IPKernelApp.file_to_run = '' - -## Enable GUI event loop integration with any of ('asyncio', 'glut', 'gtk', -# 'gtk2', 'gtk3', 'gtk4', 'osx', 'pyglet', 'qt', 'qt5', 'qt6', 'tk', 'wx', -# 'gtk2', 'qt4'). -# See also: InteractiveShellApp.gui -# c.IPKernelApp.gui = None - -## set the heartbeat port [default: random] -# See also: ConnectionFileMixin.hb_port -# c.IPKernelApp.hb_port = 0 - -## Should variables loaded at startup (by startup files, exec_lines, etc.) -# See also: InteractiveShellApp.hide_initial_ns -# c.IPKernelApp.hide_initial_ns = True - -## If True, IPython will not add the current working directory to sys.path. -# See also: InteractiveShellApp.ignore_cwd -# c.IPKernelApp.ignore_cwd = False - -## ONLY USED ON WINDOWS -# Interrupt this process when the parent is signaled. -# Default: 0 -# c.IPKernelApp.interrupt = 0 - -## set the iopub (PUB) port [default: random] -# See also: ConnectionFileMixin.iopub_port -# c.IPKernelApp.iopub_port = 0 - -## Set the kernel's IP address [default localhost]. -# See also: ConnectionFileMixin.ip -# c.IPKernelApp.ip = '' - -## -# See also: BaseIPythonApplication.ipython_dir -# c.IPKernelApp.ipython_dir = '' - -## The Kernel subclass to be used. -# -# This should allow easy reuse of the IPKernelApp entry point -# to configure and launch kernels other than IPython's own. -# Default: 'ipykernel.ipkernel.IPythonKernel' -# c.IPKernelApp.kernel_class = 'ipykernel.ipkernel.IPythonKernel' - -## The date format used by logging formatters for %(asctime)s -# See also: Application.log_datefmt -# c.IPKernelApp.log_datefmt = '%Y-%m-%d %H:%M:%S' - -## The Logging format template -# See also: Application.log_format -# c.IPKernelApp.log_format = '[%(name)s]%(highlevel)s %(message)s' - -## Set the log level by value or name. -# See also: Application.log_level -# c.IPKernelApp.log_level = 30 - -## -# See also: Application.logging_config -# c.IPKernelApp.logging_config = {} - -## Configure matplotlib for interactive use with -# See also: InteractiveShellApp.matplotlib -# c.IPKernelApp.matplotlib = None - -## Run the module as a script. -# See also: InteractiveShellApp.module_to_run -# c.IPKernelApp.module_to_run = '' - -## redirect stderr to the null device -# Default: False -# c.IPKernelApp.no_stderr = False - -## redirect stdout to the null device -# Default: False -# c.IPKernelApp.no_stdout = False - -## The importstring for the OutStream factory -# Default: 'ipykernel.iostream.OutStream' -# c.IPKernelApp.outstream_class = 'ipykernel.iostream.OutStream' - -## Whether to overwrite existing config files when copying -# See also: BaseIPythonApplication.overwrite -# c.IPKernelApp.overwrite = False - -## kill this process if its parent dies. On Windows, the argument -# specifies the HANDLE of the parent process, otherwise it is simply boolean. -# Default: 0 -# c.IPKernelApp.parent_handle = 0 - -## The IPython profile to use. -# See also: BaseIPythonApplication.profile -# c.IPKernelApp.profile = 'default' - -## Pre-load matplotlib and numpy for interactive use, -# See also: InteractiveShellApp.pylab -# c.IPKernelApp.pylab = None - -## If true, IPython will populate the user namespace with numpy, pylab, etc. -# See also: InteractiveShellApp.pylab_import_all -# c.IPKernelApp.pylab_import_all = True - -## Only send stdout/stderr to output stream -# Default: True -# c.IPKernelApp.quiet = True - -## Reraise exceptions encountered loading IPython extensions? -# See also: InteractiveShellApp.reraise_ipython_extension_failures -# c.IPKernelApp.reraise_ipython_extension_failures = False - -## set the shell (ROUTER) port [default: random] -# See also: ConnectionFileMixin.shell_port -# c.IPKernelApp.shell_port = 0 - -## Instead of starting the Application, dump configuration to stdout -# See also: Application.show_config -# c.IPKernelApp.show_config = False - -## Instead of starting the Application, dump configuration to stdout (as JSON) -# See also: Application.show_config_json -# c.IPKernelApp.show_config_json = False - -## set the stdin (ROUTER) port [default: random] -# See also: ConnectionFileMixin.stdin_port -# c.IPKernelApp.stdin_port = 0 - -# See also: ConnectionFileMixin.transport -# c.IPKernelApp.transport = 'tcp' - -## Set main event loop. -# Default: False -# c.IPKernelApp.trio_loop = False - -## Create a massive crash report when IPython encounters what may be an -# See also: BaseIPythonApplication.verbose_crash -# c.IPKernelApp.verbose_crash = False - -# ------------------------------------------------------------------------------ -# Kernel(SingletonConfigurable) configuration -# ------------------------------------------------------------------------------ -## The base kernel class. - -## Whether to use appnope for compatibility with OS X App Nap. -# -# Only affects OS X >= 10.9. -# Default: True -# c.Kernel._darwin_app_nap = True - -# Default: 0.0005 -# c.Kernel._execute_sleep = 0.0005 - -# Default: 0.01 -# c.Kernel._poll_interval = 0.01 - -## Set to False if you want to debug python standard and dependent libraries. -# Default: True -# c.Kernel.debug_just_my_code = True - -## time (in seconds) to wait for messages to arrive -# when aborting queued requests after an error. -# -# Requests that arrive within this window after an error -# will be cancelled. -# -# Increase in the event of unusually slow network -# causing significant delays, -# which can manifest as e.g. "Run all" in a notebook -# aborting some, but not all, messages after an error. -# Default: 0.0 -# c.Kernel.stop_on_error_timeout = 0.0 - -# ------------------------------------------------------------------------------ -# IPythonKernel(Kernel) configuration -# ------------------------------------------------------------------------------ -## The IPython Kernel class. - -## Whether to use appnope for compatibility with OS X App Nap. -# See also: Kernel._darwin_app_nap -# c.IPythonKernel._darwin_app_nap = True - -# See also: Kernel._execute_sleep -# c.IPythonKernel._execute_sleep = 0.0005 - -# See also: Kernel._poll_interval -# c.IPythonKernel._poll_interval = 0.01 - -## Set to False if you want to debug python standard and dependent libraries. -# See also: Kernel.debug_just_my_code -# c.IPythonKernel.debug_just_my_code = True - -# Default: [{'text': 'Python Reference', 'url': 'https://docs.python.org/3.10'}, {'text': 'IPython Reference', 'url': 'https://ipython.org/documentation.html'}, {'text': 'NumPy Reference', 'url': 'https://docs.scipy.org/doc/numpy/reference/'}, {'text': 'SciPy Reference', 'url': 'https://docs.scipy.org/doc/scipy/reference/'}, {'text': 'Matplotlib Reference', 'url': 'https://matplotlib.org/contents.html'}, {'text': 'SymPy Reference', 'url': 'http://docs.sympy.org/latest/index.html'}, {'text': 'pandas Reference', 'url': 'https://pandas.pydata.org/pandas-docs/stable/'}] -# c.IPythonKernel.help_links = [{'text': 'Python Reference', 'url': 'https://docs.python.org/3.10'}, {'text': 'IPython Reference', 'url': 'https://ipython.org/documentation.html'}, {'text': 'NumPy Reference', 'url': 'https://docs.scipy.org/doc/numpy/reference/'}, {'text': 'SciPy Reference', 'url': 'https://docs.scipy.org/doc/scipy/reference/'}, {'text': 'Matplotlib Reference', 'url': 'https://matplotlib.org/contents.html'}, {'text': 'SymPy Reference', 'url': 'http://docs.sympy.org/latest/index.html'}, {'text': 'pandas Reference', 'url': 'https://pandas.pydata.org/pandas-docs/stable/'}] - -## time (in seconds) to wait for messages to arrive -# See also: Kernel.stop_on_error_timeout -# c.IPythonKernel.stop_on_error_timeout = 0.0 - -## Set this flag to False to deactivate the use of experimental IPython -# completion APIs. -# Default: True -# c.IPythonKernel.use_experimental_completions = True - -# ------------------------------------------------------------------------------ -# InteractiveShell(SingletonConfigurable) configuration -# ------------------------------------------------------------------------------ -## An enhanced, interactive shell for Python. - -## 'all', 'last', 'last_expr' or 'none', 'last_expr_or_assign' specifying which -# nodes should be run interactively (displaying output from expressions). -# Choices: any of ['all', 'last', 'last_expr', 'none', 'last_expr_or_assign'] -# Default: 'last_expr' -# c.InteractiveShell.ast_node_interactivity = 'last_expr' - -## A list of ast.NodeTransformer subclass instances, which will be applied to -# user input before code is run. -# Default: [] -# c.InteractiveShell.ast_transformers = [] - -## Automatically run await statement in the top level repl. -# Default: True -# c.InteractiveShell.autoawait = True - -## Make IPython automatically call any callable object even if you didn't type -# explicit parentheses. For example, 'str 43' becomes 'str(43)' automatically. -# The value can be '0' to disable the feature, '1' for 'smart' autocall, where -# it is not applied if there are no more arguments on the line, and '2' for -# 'full' autocall, where all callable objects are automatically called (even if -# no arguments are present). -# Choices: any of [0, 1, 2] -# Default: 0 -# c.InteractiveShell.autocall = 0 - -## Autoindent IPython code entered interactively. -# Default: True -# c.InteractiveShell.autoindent = True - -## Enable magic commands to be called without the leading %. -# Default: True -# c.InteractiveShell.automagic = True - -## The part of the banner to be printed before the profile -# Default: "Python 3.10.14 (main, Mar 25 2024, 21:45:25) [GCC 12.2.0]\nType 'copyright', 'credits' or 'license' for more information\nIPython 8.22.2 -- An enhanced Interactive Python. Type '?' for help.\n" -# c.InteractiveShell.banner1 = "Python 3.10.14 (main, Mar 25 2024, 21:45:25) [GCC 12.2.0]\nType 'copyright', 'credits' or 'license' for more information\nIPython 8.22.2 -- An enhanced Interactive Python. Type '?' for help.\n" - -## The part of the banner to be printed after the profile -# Default: '' -# c.InteractiveShell.banner2 = '' - -## Set the size of the output cache. The default is 1000, you can change it -# permanently in your config file. Setting it to 0 completely disables the -# caching system, and the minimum value accepted is 3 (if you provide a value -# less than 3, it is reset to 0 and a warning is issued). This limit is defined -# because otherwise you'll spend more time re-flushing a too small cache than -# working -# Default: 1000 -# c.InteractiveShell.cache_size = 1000 - -## Use colors for displaying information about objects. Because this information -# is passed through a pager (like 'less'), and some pagers get confused with -# color codes, this capability can be turned off. -# Default: True -# c.InteractiveShell.color_info = True -# Default: False -# c.InteractiveShell.debug = False - -## Don't call post-execute functions that have failed in the past. -# Default: False -# c.InteractiveShell.disable_failing_post_execute = False - -## If True, anything that would be passed to the pager -# will be displayed as regular output instead. -# Default: False -# c.InteractiveShell.display_page = False - -## (Provisional API) enables html representation in mime bundles sent to pagers. -# Default: False -# c.InteractiveShell.enable_html_pager = False - -## Total length of command history -# Default: 10000 -# c.InteractiveShell.history_length = 10000 - -## The number of saved history entries to be loaded into the history buffer at -# startup. -# Default: 1000 -# c.InteractiveShell.history_load_length = 1000 - -## Class to use to instantiate the shell inspector -# Default: 'IPython.core.oinspect.Inspector' -# c.InteractiveShell.inspector_class = 'IPython.core.oinspect.Inspector' - -# Default: '' -# c.InteractiveShell.ipython_dir = '' - -## Start logging to the given file in append mode. Use `logfile` to specify a log -# file to **overwrite** logs to. -# Default: '' -# c.InteractiveShell.logappend = '' - -## The name of the logfile to use. -# Default: '' -# c.InteractiveShell.logfile = '' - -## Start logging to the default log file in overwrite mode. Use `logappend` to -# specify a log file to **append** logs to. -# Default: False -# c.InteractiveShell.logstart = False - -## Select the loop runner that will be used to execute top-level asynchronous -# code -# Default: 'IPython.core.interactiveshell._asyncio_runner' -# c.InteractiveShell.loop_runner = 'IPython.core.interactiveshell._asyncio_runner' - -# Choices: any of [0, 1, 2] -# Default: 0 -# c.InteractiveShell.object_info_string_level = 0 - -## Automatically call the pdb debugger after every exception. -# Default: False -# c.InteractiveShell.pdb = False - -# Default: False -# c.InteractiveShell.quiet = False - -# Default: '\n' -# c.InteractiveShell.separate_in = '\n' - -# Default: '' -# c.InteractiveShell.separate_out = '' - -# Default: '' -# c.InteractiveShell.separate_out2 = '' - -## Show rewritten input, e.g. for autocall. -# Default: True -# c.InteractiveShell.show_rewritten_input = True - -## Enables rich html representation of docstrings. (This requires the docrepr -# module). -# Default: False -# c.InteractiveShell.sphinxify_docstring = False - -## Warn if running in a virtual environment with no IPython installed (so IPython -# from the global environment is used). -# Default: True -# c.InteractiveShell.warn_venv = True - -# Default: True -# c.InteractiveShell.wildcards_case_sensitive = True - -## Switch modes for the IPython exception handlers. -# Choices: any of ['Context', 'Plain', 'Verbose', 'Minimal'] (case-insensitive) -# Default: 'Context' -# c.InteractiveShell.xmode = 'Context' - -# ------------------------------------------------------------------------------ -# ZMQInteractiveShell(InteractiveShell) configuration -# ------------------------------------------------------------------------------ -## A subclass of InteractiveShell for ZMQ. - -## -# See also: InteractiveShell.ast_node_interactivity -# c.ZMQInteractiveShell.ast_node_interactivity = 'last_expr' - -## -# See also: InteractiveShell.ast_transformers -# c.ZMQInteractiveShell.ast_transformers = [] - -## -# See also: InteractiveShell.autoawait -# c.ZMQInteractiveShell.autoawait = True - -## -# See also: InteractiveShell.autocall -# c.ZMQInteractiveShell.autocall = 0 - -## -# See also: InteractiveShell.automagic -# c.ZMQInteractiveShell.automagic = True - -## The part of the banner to be printed before the profile -# See also: InteractiveShell.banner1 -# c.ZMQInteractiveShell.banner1 = "Python 3.10.14 (main, Mar 25 2024, 21:45:25) [GCC 12.2.0]\nType 'copyright', 'credits' or 'license' for more information\nIPython 8.22.2 -- An enhanced Interactive Python. Type '?' for help.\n" - -## The part of the banner to be printed after the profile -# See also: InteractiveShell.banner2 -# c.ZMQInteractiveShell.banner2 = '' - -## -# See also: InteractiveShell.cache_size -# c.ZMQInteractiveShell.cache_size = 1000 - -## -# See also: InteractiveShell.color_info -# c.ZMQInteractiveShell.color_info = True - -## Set the color scheme (NoColor, Neutral, Linux, or LightBG). -# See also: InteractiveShell.colors -# c.ZMQInteractiveShell.colors = 'Neutral' - -# See also: InteractiveShell.debug -# c.ZMQInteractiveShell.debug = False - -## Don't call post-execute functions that have failed in the past. -# See also: InteractiveShell.disable_failing_post_execute -# c.ZMQInteractiveShell.disable_failing_post_execute = False - -## If True, anything that would be passed to the pager -# See also: InteractiveShell.display_page -# c.ZMQInteractiveShell.display_page = False - -## -# See also: InteractiveShell.enable_html_pager -# c.ZMQInteractiveShell.enable_html_pager = False - -## Total length of command history -# See also: InteractiveShell.history_length -# c.ZMQInteractiveShell.history_length = 10000 - -## -# See also: InteractiveShell.history_load_length -# c.ZMQInteractiveShell.history_load_length = 1000 - -## Class to use to instantiate the shell inspector -# See also: InteractiveShell.inspector_class -# c.ZMQInteractiveShell.inspector_class = 'IPython.core.oinspect.Inspector' - -# See also: InteractiveShell.ipython_dir -# c.ZMQInteractiveShell.ipython_dir = '' - -## -# See also: InteractiveShell.logappend -# c.ZMQInteractiveShell.logappend = '' - -## -# See also: InteractiveShell.logfile -# c.ZMQInteractiveShell.logfile = '' - -## -# See also: InteractiveShell.logstart -# c.ZMQInteractiveShell.logstart = False - -## Select the loop runner that will be used to execute top-level asynchronous -# code -# See also: InteractiveShell.loop_runner -# c.ZMQInteractiveShell.loop_runner = 'IPython.core.interactiveshell._asyncio_runner' - -# See also: InteractiveShell.object_info_string_level -# c.ZMQInteractiveShell.object_info_string_level = 0 - -## -# See also: InteractiveShell.pdb -# c.ZMQInteractiveShell.pdb = False - -# See also: InteractiveShell.quiet -# c.ZMQInteractiveShell.quiet = False - -# See also: InteractiveShell.separate_in -# c.ZMQInteractiveShell.separate_in = '\n' - -# See also: InteractiveShell.separate_out -# c.ZMQInteractiveShell.separate_out = '' - -# See also: InteractiveShell.separate_out2 -# c.ZMQInteractiveShell.separate_out2 = '' - -## Show rewritten input, e.g. for autocall. -# See also: InteractiveShell.show_rewritten_input -# c.ZMQInteractiveShell.show_rewritten_input = True - -## -# See also: InteractiveShell.sphinxify_docstring -# c.ZMQInteractiveShell.sphinxify_docstring = False - -## Warn if running in a virtual environment with no IPython installed (so IPython -# from the global environment is used). -# See also: InteractiveShell.warn_venv -# c.ZMQInteractiveShell.warn_venv = True - -# See also: InteractiveShell.wildcards_case_sensitive -# c.ZMQInteractiveShell.wildcards_case_sensitive = True - -## Switch modes for the IPython exception handlers. -# See also: InteractiveShell.xmode -# c.ZMQInteractiveShell.xmode = 'Context' - -# ------------------------------------------------------------------------------ -# ProfileDir(LoggingConfigurable) configuration -# ------------------------------------------------------------------------------ -## An object to manage the profile directory and its resources. -# -# The profile directory is used by all IPython applications, to manage -# configuration, logging and security. -# -# This object knows how to find, create and manage these directories. This -# should be used by any code that wants to handle profiles. - -## Set the profile location directly. This overrides the logic used by the -# `profile` option. -# Default: '' -# c.ProfileDir.location = '' - -# ------------------------------------------------------------------------------ -# Session(Configurable) configuration -# ------------------------------------------------------------------------------ -## Object for handling serialization and sending of messages. -# -# The Session object handles building messages and sending them -# with ZMQ sockets or ZMQStream objects. Objects can communicate with each -# other over the network via Session objects, and only need to work with the -# dict-based IPython message spec. The Session will handle -# serialization/deserialization, security, and metadata. -# -# Sessions support configurable serialization via packer/unpacker traits, -# and signing with HMAC digests via the key/keyfile traits. -# -# Parameters -# ---------- -# -# debug : bool -# whether to trigger extra debugging statements -# packer/unpacker : str : 'json', 'pickle' or import_string -# importstrings for methods to serialize message parts. If just -# 'json' or 'pickle', predefined JSON and pickle packers will be used. -# Otherwise, the entire importstring must be used. -# -# The functions must accept at least valid JSON input, and output -# *bytes*. -# -# For example, to use msgpack: -# packer = 'msgpack.packb', unpacker='msgpack.unpackb' -# pack/unpack : callables -# You can also set the pack/unpack callables for serialization directly. -# session : bytes -# the ID of this Session object. The default is to generate a new UUID. -# username : unicode -# username added to message headers. The default is to ask the OS. -# key : bytes -# The key used to initialize an HMAC signature. If unset, messages -# will not be signed or checked. -# keyfile : filepath -# The file containing a key. If this is set, `key` will be initialized -# to the contents of the file. - -## Threshold (in bytes) beyond which an object's buffer should be extracted to -# avoid pickling. -# Default: 1024 -# c.Session.buffer_threshold = 1024 - -## Whether to check PID to protect against calls after fork. -# -# This check can be disabled if fork-safety is handled elsewhere. -# Default: True -# c.Session.check_pid = True - -## Threshold (in bytes) beyond which a buffer should be sent without copying. -# Default: 65536 -# c.Session.copy_threshold = 65536 - -## Debug output in the Session -# Default: False -# c.Session.debug = False - -## The maximum number of digests to remember. -# -# The digest history will be culled when it exceeds this value. -# Default: 65536 -# c.Session.digest_history_size = 65536 - -## The maximum number of items for a container to be introspected for custom serialization. -# Containers larger than this are pickled outright. -# Default: 64 -# c.Session.item_threshold = 64 - -## execution key, for signing messages. -# Default: b'' -# c.Session.key = b'' - -## path to file containing execution key. -# Default: '' -# c.Session.keyfile = '' - -## Metadata dictionary, which serves as the default top-level metadata dict for -# each message. -# Default: {} -# c.Session.metadata = {} - -## The name of the packer for serializing messages. -# Should be one of 'json', 'pickle', or an import name -# for a custom callable serializer. -# Default: 'json' -# c.Session.packer = 'json' - -## The UUID identifying this session. -# Default: '' -# c.Session.session = '' - -## The digest scheme used to construct the message signatures. -# Must have the form 'hmac-HASH'. -# Default: 'hmac-sha256' -# c.Session.signature_scheme = 'hmac-sha256' - -## The name of the unpacker for unserializing messages. -# Only used with custom functions for `packer`. -# Default: 'json' -# c.Session.unpacker = 'json' - -## Username for the Session. Default is your system username. -# Default: 'user' -# c.Session.username = 'user' - -# Truncate large collections (lists, dicts, tuples, sets) to this size. -# Set to 0 to disable truncation. -# Default: 1000 -c.PlainTextFormatter.max_seq_length = 0 diff --git a/os-image/e2b-ci/code-interpreter/jupyter-healthcheck.sh b/os-image/e2b-ci/code-interpreter/jupyter-healthcheck.sh deleted file mode 100644 index a4c00131..00000000 --- a/os-image/e2b-ci/code-interpreter/jupyter-healthcheck.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash -# Custom health check for Jupyter Server -# Verifies the server is responsive via the /api/status endpoint - -MAX_RETRIES=50 -RETRY_INTERVAL=0.2 - -for i in $(seq 1 $MAX_RETRIES); do - status_code=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:8888/api/status") - - if [ "$status_code" -eq 200 ]; then - echo "Jupyter Server is healthy" - exit 0 - fi - - if [ $((i % 10)) -eq 0 ]; then - echo "Waiting for Jupyter Server to become healthy... (attempt $i/$MAX_RETRIES)" - fi - sleep $RETRY_INTERVAL -done - -echo "Jupyter Server health check failed after $MAX_RETRIES attempts" -exit 1 diff --git a/os-image/e2b-ci/code-interpreter/jupyter_server_config.py b/os-image/e2b-ci/code-interpreter/jupyter_server_config.py deleted file mode 100644 index 663682cc..00000000 --- a/os-image/e2b-ci/code-interpreter/jupyter_server_config.py +++ /dev/null @@ -1,64 +0,0 @@ -# Configuration file for jupyter-server. - -c = get_config() # noqa - - -# Pin the contents root directory. -# -# Sessions are created with a relative path (a bare uuid, see -# server/contexts.py). Without an explicit root_dir, jupyter-server -# inherits the process working directory as its root — which is "/" -# under systemd (jupyter.service has no WorkingDirectory). Since -# jupyter-server 2.18.0 (CVE-2026-35397 path-traversal hardening), a -# root_dir of "/" makes every POST /api/sessions fail with -# " is outside root contents directory", so the server never -# starts. Pinning it to /home/user matches the execution cwd. -c.ServerApp.root_dir = "/home/user" - - -# Set the Access-Control-Allow-Origin header -# -# Use '*' to allow any origin to access your server. -# -# Takes precedence over allow_origin_pat. -# Default: '' -c.ServerApp.allow_origin = "*" - - -# Allow requests where the Host header doesn't point to a local server -# -# By default, requests get a 403 forbidden response if the 'Host' header -# shows that the browser thinks it's on a non-local domain. -# Setting this option to True disables this check. -# -# This protects against 'DNS rebinding' attacks, where a remote web server -# serves you a page and then changes its DNS to send later requests to a -# local IP, bypassing same-origin checks. -# -# Local IP addresses (such as 127.0.0.1 and ::1) are allowed as local, -# along with hostnames configured in local_hostnames. -# Default: False -c.ServerApp.allow_remote_access = True - -# Disable cross-site-request-forgery protection -# -# Jupyter server includes protection from cross-site request forgeries, -# requiring API requests to either: -# -# - originate from pages served by this server (validated with XSRF cookie and token), or -# - authenticate with a token -# -# Some anonymous compute resources still desire the ability to run code, -# completely without authentication. -# These services can disable all authentication and security checks, -# with the full knowledge of what that implies. -# Default: False -c.ServerApp.disable_check_xsrf = True - -# Whether to allow the user to run the server as root. -# Default: False -c.ServerApp.allow_root = True - -# (bytes/sec) Maximum rate at which messages can be sent on iopub before they are limited. -# Default: 1000000 -c.ServerApp.iopub_data_rate_limit = 1000000000 diff --git a/os-image/e2b-ci/code-interpreter/matplotlibrc b/os-image/e2b-ci/code-interpreter/matplotlibrc deleted file mode 100644 index b71d01f3..00000000 --- a/os-image/e2b-ci/code-interpreter/matplotlibrc +++ /dev/null @@ -1 +0,0 @@ -font.family: sans-serif, Noto Sans CJK JP diff --git a/os-image/e2b-ci/code-interpreter/requirements.txt b/os-image/e2b-ci/code-interpreter/requirements.txt deleted file mode 100644 index b1d620b2..00000000 --- a/os-image/e2b-ci/code-interpreter/requirements.txt +++ /dev/null @@ -1,44 +0,0 @@ -# Jupyter server requirements -jupyter-server==2.20.0 -ipykernel==6.31.0 -ipython==9.15.0 - -orjson==3.11.9 -pandas==2.2.3 -matplotlib==3.10.9 -pillow==12.3.0 - -# Latest version for -e2b_charts==1.0.0 - -# Other packages -aiohttp==3.14.3 -beautifulsoup4==4.15.0 -bokeh==3.9.1 -gensim==4.4.0 -imageio==2.37.3 -joblib==1.5.3 -librosa==0.11.0 -nltk==3.10.3 -numpy==2.3.5 -numba==0.66.0 -opencv-python==4.11.0.86 -openpyxl==3.1.5 -plotly==6.0.1 -kaleido==1.3.0 -pytest==9.0.3 -python-docx==1.1.2 -pytz==2025.2 -requests==2.33.0 -scikit-image==0.25.2 -scikit-learn==1.6.1 -scipy==1.17.1 -seaborn==0.13.2 -soundfile==0.13.1 -spacy==3.8.14 -textblob==0.19.0 -tornado==6.5.8 -urllib3==2.7.0 -xarray==2025.4.0 -xlrd==2.0.2 -sympy==1.14.0 diff --git a/os-image/e2b-ci/code-interpreter/server/api/models/__init__.py b/os-image/e2b-ci/code-interpreter/server/api/models/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/os-image/e2b-ci/code-interpreter/server/api/models/context.py b/os-image/e2b-ci/code-interpreter/server/api/models/context.py deleted file mode 100644 index 5efb850e..00000000 --- a/os-image/e2b-ci/code-interpreter/server/api/models/context.py +++ /dev/null @@ -1,11 +0,0 @@ -from pydantic import BaseModel, StrictStr -from pydantic import Field - - -class Context(BaseModel): - id: StrictStr = Field(description="Context ID") - language: StrictStr = Field(description="Language of the context") - cwd: StrictStr = Field(description="Current working directory of the context") - - def __hash__(self): - return hash(self.id) diff --git a/os-image/e2b-ci/code-interpreter/server/api/models/create_context.py b/os-image/e2b-ci/code-interpreter/server/api/models/create_context.py deleted file mode 100644 index 1e1fefb7..00000000 --- a/os-image/e2b-ci/code-interpreter/server/api/models/create_context.py +++ /dev/null @@ -1,13 +0,0 @@ -from pydantic import BaseModel, StrictStr -from pydantic import Field -from typing import Optional - - -class CreateContext(BaseModel): - cwd: Optional[StrictStr] = Field( - default="/home/user", - description="Current working directory", - ) - language: Optional[StrictStr] = Field( - default="python", description="Language of the context" - ) diff --git a/os-image/e2b-ci/code-interpreter/server/api/models/env_vars.py b/os-image/e2b-ci/code-interpreter/server/api/models/env_vars.py deleted file mode 100644 index 7c51bcca..00000000 --- a/os-image/e2b-ci/code-interpreter/server/api/models/env_vars.py +++ /dev/null @@ -1,4 +0,0 @@ -from typing import Dict -from pydantic import StrictStr - -EnvVars = Dict[StrictStr, str] diff --git a/os-image/e2b-ci/code-interpreter/server/api/models/error.py b/os-image/e2b-ci/code-interpreter/server/api/models/error.py deleted file mode 100644 index b17a6abc..00000000 --- a/os-image/e2b-ci/code-interpreter/server/api/models/error.py +++ /dev/null @@ -1,17 +0,0 @@ -from typing import Optional -from pydantic import BaseModel, StrictStr -from pydantic import Field - -from api.models.output import OutputType - - -class Error(BaseModel): - type: OutputType = OutputType.ERROR - - name: Optional[StrictStr] = Field(default=None, description="Name of the exception") - value: Optional[StrictStr] = Field( - default=None, description="Value of the exception" - ) - traceback: Optional[StrictStr] = Field( - default=None, description="Traceback of the exception" - ) diff --git a/os-image/e2b-ci/code-interpreter/server/api/models/execution_request.py b/os-image/e2b-ci/code-interpreter/server/api/models/execution_request.py deleted file mode 100644 index bb691e9e..00000000 --- a/os-image/e2b-ci/code-interpreter/server/api/models/execution_request.py +++ /dev/null @@ -1,16 +0,0 @@ -from typing import Optional -from pydantic import BaseModel, StrictStr -from pydantic import Field - -from .env_vars import EnvVars - - -class ExecutionRequest(BaseModel): - code: StrictStr = Field(description="Code to be executed") - context_id: Optional[StrictStr] = Field(default=None, description="Context ID") - language: Optional[StrictStr] = Field( - default=None, description="Language of the code" - ) - env_vars: Optional[EnvVars] = Field( - description="Environment variables", default=None - ) diff --git a/os-image/e2b-ci/code-interpreter/server/api/models/logs.py b/os-image/e2b-ci/code-interpreter/server/api/models/logs.py deleted file mode 100644 index d9a433fe..00000000 --- a/os-image/e2b-ci/code-interpreter/server/api/models/logs.py +++ /dev/null @@ -1,19 +0,0 @@ -import datetime - - -from typing import Optional -from pydantic import BaseModel, StrictStr - -from api.models.output import OutputType - - -class Stdout(BaseModel): - type: OutputType = OutputType.STDOUT - text: Optional[StrictStr] = None - timestamp: Optional[datetime.datetime] = None - - -class Stderr(BaseModel): - type: OutputType = OutputType.STDERR - text: Optional[StrictStr] = None - timestamp: Optional[datetime.datetime] = None diff --git a/os-image/e2b-ci/code-interpreter/server/api/models/output.py b/os-image/e2b-ci/code-interpreter/server/api/models/output.py deleted file mode 100644 index 8e620c38..00000000 --- a/os-image/e2b-ci/code-interpreter/server/api/models/output.py +++ /dev/null @@ -1,31 +0,0 @@ -from __future__ import annotations - -from enum import Enum -from pydantic import BaseModel - - -class OutputType(Enum): - """ - Represents the type of the data to send to the client. - """ - - STDOUT = "stdout" - STDERR = "stderr" - RESULT = "result" - ERROR = "error" - NUMBER_OF_EXECUTIONS = "number_of_executions" - END_OF_EXECUTION = "end_of_execution" - UNEXPECTED_END_OF_EXECUTION = "unexpected_end_of_execution" - - -class EndOfExecution(BaseModel): - type: OutputType = OutputType.END_OF_EXECUTION - - -class UnexpectedEndOfExecution(BaseModel): - type: OutputType = OutputType.UNEXPECTED_END_OF_EXECUTION - - -class NumberOfExecutions(BaseModel): - type: OutputType = OutputType.NUMBER_OF_EXECUTIONS - execution_count: int diff --git a/os-image/e2b-ci/code-interpreter/server/api/models/result.py b/os-image/e2b-ci/code-interpreter/server/api/models/result.py deleted file mode 100644 index 90e24a9a..00000000 --- a/os-image/e2b-ci/code-interpreter/server/api/models/result.py +++ /dev/null @@ -1,105 +0,0 @@ -from __future__ import annotations - -import warnings - -from typing import Optional, Iterable -from pydantic import BaseModel - -from api.models.output import OutputType - -warnings.filterwarnings("ignore", category=UserWarning) - - -class Result(BaseModel): - """ - Represents the data to be displayed as a result of executing a cell in a Jupyter notebook. - The result is similar to the structure returned by ipython kernel: https://ipython.readthedocs.io/en/stable/development/execution.html#execution-semantics - - The result can contain multiple types of data, such as text, images, plots, etc. Each type of data is represented - as a string, and the result can contain multiple types of data. The display calls don't have to have text representation, - for the actual result the representation is always present for the result, the other representations are always optional. - """ - - type: OutputType = OutputType.RESULT - - text: Optional[str] = None - html: Optional[str] = None - markdown: Optional[str] = None - svg: Optional[str] = None - png: Optional[str] = None - jpeg: Optional[str] = None - pdf: Optional[str] = None - latex: Optional[str] = None - json: Optional[dict] = None - javascript: Optional[str] = None - data: Optional[dict] = None - chart: Optional[dict] = None - extra: Optional[dict] = None - "Extra data that can be included. Not part of the standard types." - - is_main_result: Optional[bool] = None - "Whether this data is the result of the execetution. Data can be produced by display calls of which can be multiple in a cell." - - def __init__(self, is_main_result: bool, data: [str, str]): - super().__init__() - self.is_main_result = is_main_result - - self.text = data.pop("text/plain", None) - if self.text and ( - (self.text.startswith("'") and self.text.endswith("'")) - or (self.text.startswith('"') and self.text.endswith('"')) - ): - self.text = self.text[1:-1] - - self.html = data.pop("text/html", None) - self.markdown = data.pop("text/markdown", None) - self.svg = data.pop("image/svg+xml", None) - self.png = data.pop("image/png", None) - self.jpeg = data.pop("image/jpeg", None) - self.pdf = data.pop("application/pdf", None) - self.latex = data.pop("text/latex", None) - self.json = data.pop("application/json", None) - self.javascript = data.pop("application/javascript", None) - self.data = data.pop("e2b/data", None) - self.chart = data.pop("e2b/chart", None) - self.extra = data - - def formats(self) -> Iterable[str]: - formats = [] - - for key in [ - "text", - "html", - "markdown", - "svg", - "png", - "jpeg", - "pdf", - "latex", - "json", - "javascript", - "data", - "chart", - ]: - if getattr(self, key): - formats.append(key) - - if self.extra: - for key in self.extra: - formats.append(key) - - return formats - - def __str__(self) -> str: - """ - Returns the text representation of the data. - - :return: The text representation of the data. - """ - return self.__repr__() - - def __repr__(self) -> str: - if self.text: - return f"Result({self.text})" - formats = self.formats() - return f"Result with formats: {formats}" diff --git a/os-image/e2b-ci/code-interpreter/server/consts.py b/os-image/e2b-ci/code-interpreter/server/consts.py deleted file mode 100644 index e1b92a06..00000000 --- a/os-image/e2b-ci/code-interpreter/server/consts.py +++ /dev/null @@ -1 +0,0 @@ -JUPYTER_BASE_URL = "http://localhost:8888" diff --git a/os-image/e2b-ci/code-interpreter/server/contexts.py b/os-image/e2b-ci/code-interpreter/server/contexts.py deleted file mode 100644 index 773f3fa8..00000000 --- a/os-image/e2b-ci/code-interpreter/server/contexts.py +++ /dev/null @@ -1,72 +0,0 @@ -import logging -import uuid -from typing import Optional - -from api.models.context import Context -from fastapi.responses import PlainTextResponse - -from consts import JUPYTER_BASE_URL -from errors import ExecutionError -from messaging import ContextWebSocket - -logger = logging.Logger(__name__) - - -def get_kernel_for_language(language: str) -> str: - if language == "typescript": - return "javascript" - - return language - - -def normalize_language(language: Optional[str]) -> str: - if not language: - return "python" - - language = language.lower().strip() - - if language == "js": - return "javascript" - - if language == "ts": - return "typescript" - - return language - - -async def create_context(client, websockets: dict, language: str, cwd: str) -> Context: - data = { - "path": str(uuid.uuid4()), - "kernel": {"name": get_kernel_for_language(language)}, - "type": "notebook", - "name": str(uuid.uuid4()), - } - logger.debug(f"Creating new {language} context") - - response = await client.post(f"{JUPYTER_BASE_URL}/api/sessions", json=data) - - if not response.is_success: - raise Exception( - f"Failed to create context: {response.text}", - ) - - session_data = response.json() - session_id = session_data["id"] - context_id = session_data["kernel"]["id"] - - logger.debug(f"Created context {context_id}") - - ws = ContextWebSocket(context_id, session_id, language, cwd) - await ws.connect() - websockets[context_id] = ws - - logger.info(f"Setting working directory to {cwd}") - try: - await ws.change_current_directory(cwd, language) - except ExecutionError: - return PlainTextResponse( - "Failed to set working directory", - status_code=500, - ) - - return Context(language=language, id=context_id, cwd=cwd) diff --git a/os-image/e2b-ci/code-interpreter/server/envs.py b/os-image/e2b-ci/code-interpreter/server/envs.py deleted file mode 100644 index 8752e7b9..00000000 --- a/os-image/e2b-ci/code-interpreter/server/envs.py +++ /dev/null @@ -1,20 +0,0 @@ -import os -from typing import Optional - -import httpx - -LOCAL = os.getenv("E2B_LOCAL", False) -ENVD_PORT = 49983 - - -async def get_envs(access_token: Optional[str]) -> dict: - if LOCAL: - return {"E2B_TEST_VARIABLE": "true"} - async with httpx.AsyncClient() as client: - headers = {} - if access_token: - headers["X-Access-Token"] = f"{access_token}" - response = await client.get( - f"http://localhost:{ENVD_PORT}/envs", headers=headers - ) - return response.json() diff --git a/os-image/e2b-ci/code-interpreter/server/errors.py b/os-image/e2b-ci/code-interpreter/server/errors.py deleted file mode 100644 index ab4b43bc..00000000 --- a/os-image/e2b-ci/code-interpreter/server/errors.py +++ /dev/null @@ -1,2 +0,0 @@ -class ExecutionError(Exception): - pass diff --git a/os-image/e2b-ci/code-interpreter/server/main.py b/os-image/e2b-ci/code-interpreter/server/main.py deleted file mode 100644 index 43e4b283..00000000 --- a/os-image/e2b-ci/code-interpreter/server/main.py +++ /dev/null @@ -1,219 +0,0 @@ -import logging -import sys -import httpx - -from typing import Dict, Union, Literal, List - -from contextlib import asynccontextmanager -from fastapi import FastAPI, Request -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import PlainTextResponse - -from api.models.context import Context -from api.models.create_context import CreateContext -from api.models.execution_request import ExecutionRequest -from consts import JUPYTER_BASE_URL -from contexts import create_context, normalize_language -from messaging import ContextWebSocket -from stream import StreamingListJsonResponse -from utils.locks import LockedMap - -logging.basicConfig(level=logging.DEBUG, stream=sys.stdout) -logger = logging.Logger(__name__) -http_logger = logging.getLogger("httpcore.http11") -http_logger.setLevel(logging.WARNING) - - -websockets: Dict[Union[str, Literal["default"]], ContextWebSocket] = {} -default_websockets = LockedMap() -global client - - -@asynccontextmanager -async def lifespan(app: FastAPI): - global client - client = httpx.AsyncClient() - - try: - python_context = await create_context( - client, websockets, "python", "/home/user" - ) - default_websockets["python"] = python_context.id - websockets["default"] = websockets[python_context.id] - - kernelspecs = (await client.get(f"{JUPYTER_BASE_URL}/api/kernelspecs")).json() - if "javascript" in kernelspecs.get("kernelspecs", {}): - javascript_context = await create_context( - client, websockets, "javascript", "/home/user" - ) - default_websockets["javascript"] = javascript_context.id - - logger.info("Connected to default runtime") - yield - - # Will cleanup after application shuts down - for ws in websockets.values(): - await ws.close() - - await client.aclose() - except Exception as e: - logger.error(f"Failed to initialize default context: {e}") - raise - - -app = FastAPI(lifespan=lifespan) - -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_methods=["*"], - allow_headers=["*"], -) - -logger.info("Starting Code Interpreter server") - - -@app.get("/health") -async def get_health(): - return "OK" - - -@app.post("/execute") -async def post_execute(request: Request, exec_request: ExecutionRequest): - logger.info(f"Executing code: {exec_request.code}") - - if exec_request.context_id and exec_request.language: - return PlainTextResponse( - "Only one of context_id or language can be provided", - status_code=400, - ) - - context_id = None - if exec_request.language: - language = normalize_language(exec_request.language) - - async with await default_websockets.get_lock(language): - context_id = default_websockets.get(language) - - if not context_id: - try: - context = await create_context( - client, websockets, language, "/home/user" - ) - except Exception as e: - return PlainTextResponse(str(e), status_code=500) - - context_id = context.id - default_websockets[language] = context_id - - elif exec_request.context_id: - context_id = exec_request.context_id - - if context_id: - ws = websockets.get(context_id, None) - else: - ws = websockets["default"] - - if not ws: - return PlainTextResponse( - f"Context {exec_request.context_id} not found", - status_code=404, - ) - - return StreamingListJsonResponse( - ws.execute( - exec_request.code, - env_vars=exec_request.env_vars, - access_token=request.headers.get("X-Access-Token", None), - ) - ) - - -@app.post("/contexts") -async def post_contexts(request: CreateContext) -> Context: - logger.info("Creating a new context") - - language = normalize_language(request.language) - cwd = request.cwd or "/home/user" - - try: - return await create_context(client, websockets, language, cwd) - except Exception as e: - return PlainTextResponse(str(e), status_code=500) - - -@app.get("/contexts") -async def get_contexts() -> List[Context]: - logger.info("Listing contexts") - - return [ - Context( - id=ws.context_id, - language=ws.language, - cwd=ws.cwd, - ) - for key, ws in websockets.items() - if key != "default" - ] - - -@app.post("/contexts/{context_id}/restart") -async def restart_context(context_id: str) -> None: - logger.info(f"Restarting context {context_id}") - - ws = websockets.get(context_id, None) - if not ws: - return PlainTextResponse( - f"Context {context_id} not found", - status_code=404, - ) - - session_id = ws.session_id - - await ws.close() - - response = await client.post( - f"{JUPYTER_BASE_URL}/api/kernels/{ws.context_id}/restart" - ) - if not response.is_success: - return PlainTextResponse( - f"Failed to restart context {context_id}", - status_code=500, - ) - - ws = ContextWebSocket( - ws.context_id, - session_id, - ws.language, - ws.cwd, - ) - - await ws.connect() - - websockets[context_id] = ws - - -@app.delete("/contexts/{context_id}") -async def remove_context(context_id: str) -> None: - logger.info(f"Removing context {context_id}") - - ws = websockets.get(context_id, None) - if not ws: - return PlainTextResponse( - f"Context {context_id} not found", - status_code=404, - ) - - try: - await ws.close() - except: # noqa: E722 - pass - - response = await client.delete(f"{JUPYTER_BASE_URL}/api/kernels/{ws.context_id}") - if not response.is_success: - return PlainTextResponse( - f"Failed to remove context {context_id}", - status_code=500, - ) - - del websockets[context_id] diff --git a/os-image/e2b-ci/code-interpreter/server/messaging.py b/os-image/e2b-ci/code-interpreter/server/messaging.py deleted file mode 100644 index c51f8b21..00000000 --- a/os-image/e2b-ci/code-interpreter/server/messaging.py +++ /dev/null @@ -1,608 +0,0 @@ -import datetime -import json -import logging -import uuid -import asyncio - -import httpx - -from asyncio import Queue -from typing import ( - Dict, - Optional, - Union, -) -from pydantic import StrictStr -from websockets.client import WebSocketClientProtocol, connect -from websockets.exceptions import ( - ConnectionClosedError, - WebSocketException, -) - -from api.models.error import Error -from api.models.logs import Stdout, Stderr -from api.models.result import Result -from api.models.output import ( - EndOfExecution, - NumberOfExecutions, - OutputType, - UnexpectedEndOfExecution, -) -from consts import JUPYTER_BASE_URL -from errors import ExecutionError -from envs import get_envs - -logger = logging.getLogger(__name__) - -MAX_RECONNECT_RETRIES = 3 -PING_TIMEOUT = 30 -KEEPALIVE_INTERVAL = 5 # seconds between keepalive pings during streaming - - -class Execution: - def __init__(self, in_background: bool = False): - self.queue = Queue[ - Union[ - Result, - Error, - Stdout, - Stderr, - EndOfExecution, - NumberOfExecutions, - UnexpectedEndOfExecution, - ] - ]() - self.input_accepted = False - self.errored = False - self.in_background = in_background - - -class ContextWebSocket: - _ws: Optional[WebSocketClientProtocol] = None - _receive_task: Optional[asyncio.Task] = None - _global_env_vars: Optional[Dict[StrictStr, str]] = None - _cleanup_task: Optional[asyncio.Task] = None - - def __init__(self, context_id: str, session_id: str, language: str, cwd: str): - self.language = language - self.cwd = cwd - self.context_id = context_id - self.url = f"ws://localhost:8888/api/kernels/{context_id}/channels" - self.session_id = session_id - self._executions: Dict[str, Execution] = {} - self._lock = asyncio.Lock() - - async def reconnect(self): - if self._ws is not None: - await self._ws.close(reason="Reconnecting") - - if self._receive_task is not None: - await self._receive_task - - await self.connect() - - async def connect(self): - logger.debug(f"WebSocket connecting to {self.url}") - - ws_logger = logger.getChild("websockets.client") - ws_logger.setLevel(logging.ERROR) - - self._ws = await connect( - self.url, - ping_timeout=PING_TIMEOUT, - max_size=None, - max_queue=None, - logger=ws_logger, - ) - - logger.info(f"WebSocket connected to {self.url}") - self._receive_task = asyncio.create_task( - self._receive_message(), - name="receive_message", - ) - - async def interrupt(self): - """Interrupt the current kernel execution via the Jupyter REST API.""" - try: - async with httpx.AsyncClient() as client: - response = await client.post( - f"{JUPYTER_BASE_URL}/api/kernels/{self.context_id}/interrupt" - ) - if response.is_success: - logger.info(f"Kernel {self.context_id} interrupted successfully") - else: - logger.error( - f"Failed to interrupt kernel {self.context_id}: {response.status_code}" - ) - except Exception as e: - logger.error(f"Error interrupting kernel {self.context_id}: {e}") - - def _get_execute_request( - self, msg_id: str, code: Union[str, StrictStr], background: bool - ) -> str: - return json.dumps( - { - "header": { - "msg_id": msg_id, - "username": "e2b", - "session": self.session_id, - "msg_type": "execute_request", - "version": "5.3", - "date": datetime.datetime.now(datetime.timezone.utc).isoformat(), - }, - "parent_header": {}, - "metadata": { - "trusted": True, - "deletedCells": [], - "recordTiming": False, - "cellId": str(uuid.uuid4()), - }, - "content": { - "code": code, - "silent": background, - "store_history": True, - "user_expressions": {}, - "stop_on_error": True, - "allow_stdin": False, - }, - } - ) - - def _set_env_var_snippet(self, key: str, value: str) -> str: - """Get environment variable set command for the current language.""" - if self.language == "python": - return f"import os; os.environ['{key}'] = '{value}'" - elif self.language in ["javascript", "typescript"]: - return f"process.env['{key}'] = '{value}'" - elif self.language == "r": - return f'Sys.setenv({key} = "{value}")' - elif self.language == "java": - return f'System.setProperty("{key}", "{value}");' - elif self.language == "bash": - return f"export {key}='{value}'" - return "" - - def _delete_env_var_snippet(self, key: str) -> str: - """Get environment variable delete command for the current language.""" - if self.language == "python": - return f"import os; del os.environ['{key}']" - elif self.language in ["javascript", "typescript"]: - return f"delete process.env['{key}']" - elif self.language == "r": - return f"Sys.unsetenv('{key}')" - elif self.language == "java": - return f'System.clearProperty("{key}");' - elif self.language == "bash": - return f"unset {key}" - return "" - - def _set_env_vars_code(self, env_vars: Dict[StrictStr, str]) -> str: - """Build environment variable code for the current language.""" - env_commands = [] - for k, v in env_vars.items(): - command = self._set_env_var_snippet(k, v) - if command: - env_commands.append(command) - - return "\n".join(env_commands) - - def _reset_env_vars_code(self, env_vars: Dict[StrictStr, str]) -> str: - """Build environment variable cleanup code for the current language.""" - cleanup_commands = [] - - for key in env_vars: - # Check if this var exists in global env vars - if self._global_env_vars and key in self._global_env_vars: - # Reset to global value - value = self._global_env_vars[key] - command = self._set_env_var_snippet(key, value) - else: - # Remove the variable - command = self._delete_env_var_snippet(key) - - if command: - cleanup_commands.append(command) - - return "\n".join(cleanup_commands) - - def _get_code_indentation(self, code: str) -> str: - """Get the indentation from the first non-empty line of code.""" - if not code or not code.strip(): - return "" - - lines = code.split("\n") - for line in lines: - if line.strip(): # First non-empty line - return line[: len(line) - len(line.lstrip())] - - return "" - - def _indent_code_with_level(self, code: str, indent_level: str) -> str: - """Apply the given indentation level to each line of code.""" - if not code or not indent_level: - return code - - lines = code.split("\n") - indented_lines = [] - - for line in lines: - if line.strip(): # Non-empty lines - indented_lines.append(indent_level + line) - else: - indented_lines.append(line) - - return "\n".join(indented_lines) - - async def _cleanup_env_vars(self, env_vars: Dict[StrictStr, str]): - """Clean up environment variables in a separate execution request.""" - message_id = str(uuid.uuid4()) - self._executions[message_id] = Execution(in_background=True) - - try: - cleanup_code = self._reset_env_vars_code(env_vars) - if cleanup_code: - logger.info(f"Cleaning up env vars: {cleanup_code}") - request = self._get_execute_request(message_id, cleanup_code, True) - await self._ws.send(request) - - async for item in self._wait_for_result(message_id): - if item["type"] == "error": - logger.error(f"Error during env var cleanup: {item}") - finally: - del self._executions[message_id] - - async def _wait_for_result(self, message_id: str): - queue = self._executions[message_id].queue - - # Use a timeout on queue.get() to periodically send keepalives. - # Without keepalives, the generator blocks indefinitely waiting for - # kernel output. If the client silently disappears (e.g. network - # failure), uvicorn can only detect the broken connection when it - # tries to write — so we force a write every KEEPALIVE_INTERVAL - # seconds. This ensures timely disconnect detection and kernel - # interrupt for abandoned executions (see #213). - while True: - try: - output = await asyncio.wait_for(queue.get(), timeout=KEEPALIVE_INTERVAL) - except asyncio.TimeoutError: - # Yield a keepalive so Starlette writes to the socket. - # If the client has disconnected, the write fails and - # uvicorn delivers http.disconnect, which cancels this - # generator via CancelledError. - yield {"type": "keepalive"} - continue - - if output.type == OutputType.END_OF_EXECUTION: - break - - if output.type == OutputType.UNEXPECTED_END_OF_EXECUTION: - logger.error(f"Unexpected end of execution for code ({message_id})") - yield Error( - name="UnexpectedEndOfExecution", - value="Connection to the execution was closed before the execution was finished", - traceback="", - ) - break - - yield output.model_dump(exclude_none=True) - - async def change_current_directory( - self, path: Union[str, StrictStr], language: str - ): - message_id = str(uuid.uuid4()) - self._executions[message_id] = Execution(in_background=True) - if language == "python": - request = self._get_execute_request(message_id, f"%cd {path}", True) - elif language in ("javascript", "typescript"): - request = self._get_execute_request( - message_id, f"process.chdir('{path}')", True - ) - elif language == "r": - request = self._get_execute_request(message_id, f"setwd('{path}')", True) - # This does not actually change the working directory, but sets the user.dir property - elif language == "java": - request = self._get_execute_request( - message_id, f'System.setProperty("user.dir", "{path}");', True - ) - elif language == "bash": - request = self._get_execute_request(message_id, f"cd '{path}'", True) - else: - return - - await self._ws.send(request) - - async for item in self._wait_for_result(message_id): - if item["type"] == "error": - raise ExecutionError(f"Error during execution: {item}") - - async def execute( - self, - code: Union[str, StrictStr], - env_vars: Dict[StrictStr, str], - access_token: str, - ): - if self._ws is None: - raise Exception("WebSocket not connected") - - async with self._lock: - # Wait for any pending cleanup task to complete - if self._cleanup_task and not self._cleanup_task.done(): - logger.debug("Waiting for pending cleanup task to complete") - try: - await self._cleanup_task - except Exception as e: - logger.warning(f"Cleanup task failed: {e}") - finally: - self._cleanup_task = None - - # Get the indentation level from the code - code_indent = self._get_code_indentation(code) - - # Build the complete code snippet with env vars - complete_code = code - - global_env_vars_snippet = "" - env_vars_snippet = "" - - if self._global_env_vars is None: - self._global_env_vars = await get_envs(access_token=access_token) - global_env_vars_snippet = self._set_env_vars_code(self._global_env_vars) - - if env_vars: - env_vars_snippet = self._set_env_vars_code(env_vars) - - if global_env_vars_snippet or env_vars_snippet: - indented_env_code = self._indent_code_with_level( - f"{global_env_vars_snippet}\n{env_vars_snippet}", code_indent - ) - complete_code = f"{indented_env_code}\n{complete_code}" - - message_id = str(uuid.uuid4()) - execution = Execution() - self._executions[message_id] = execution - - # Send the code for execution - # Initial request and retries - for i in range(1 + MAX_RECONNECT_RETRIES): - try: - logger.info( - f"Sending code for the execution ({message_id}): {complete_code}" - ) - request = self._get_execute_request( - message_id, complete_code, False - ) - await self._ws.send(request) - break - except (ConnectionClosedError, WebSocketException) as e: - # Keep the last result, even if error - if i < MAX_RECONNECT_RETRIES: - logger.warning( - f"WebSocket connection lost while sending execution request, {i + 1}. reconnecting...: {str(e)}" - ) - await self.reconnect() - else: - # The retry didn't help, request wasn't sent successfully - logger.error("Failed to send execution request") - await execution.queue.put( - Error( - name="WebSocketError", - value="Failed to send execution request", - traceback="", - ) - ) - await execution.queue.put(UnexpectedEndOfExecution()) - - # Stream the results. - # If the client disconnects (Starlette cancels the task), we - # interrupt the kernel so the next execution isn't blocked (#213). - try: - async for item in self._wait_for_result(message_id): - yield item - except (asyncio.CancelledError, GeneratorExit): - logger.warning( - f"Client disconnected during execution ({message_id}), interrupting kernel" - ) - # Shield the interrupt from the ongoing cancellation so - # the HTTP request to the kernel actually completes. - try: - await asyncio.shield(self.interrupt()) - except asyncio.CancelledError: - pass - raise - finally: - if message_id in self._executions: - del self._executions[message_id] - - # Clean up env vars in a separate request after the main code has run - if env_vars: - self._cleanup_task = asyncio.create_task( - self._cleanup_env_vars(env_vars) - ) - - async def _receive_message(self): - if not self._ws: - logger.error("No WebSocket connection") - return - - try: - async for message in self._ws: - await self._process_message(json.loads(message)) - except Exception as e: - logger.error(f"WebSocket received error while receiving messages: {str(e)}") - finally: - # To prevent infinite hang, we need to cancel all ongoing execution as we could lost results during the reconnect - # Thanks to the locking, there can be either no ongoing execution or just one. - for key, execution in self._executions.items(): - await execution.queue.put( - Error( - name="WebSocketError", - value="The connections was lost, rerun the code to get the results", - traceback="", - ) - ) - await execution.queue.put(UnexpectedEndOfExecution()) - - async def _process_message(self, data: dict): - """ - Process messages from the WebSocket - - Message types: - https://jupyter-client.readthedocs.io/en/stable/messaging.html - - :param data: The message data - """ - if ( - data["msg_type"] == "status" - and data["content"]["execution_state"] == "restarting" - ): - logger.error("Context is restarting") - for execution in self._executions.values(): - await execution.queue.put( - Error( - name="ContextRestarting", - value="Context was restarted", - traceback="", - ) - ) - await execution.queue.put(EndOfExecution()) - return - - parent_msg_ig = data["parent_header"].get("msg_id", None) - if parent_msg_ig is None: - logger.warning("Parent message ID not found. %s", data) - return - - execution = self._executions.get(parent_msg_ig) - if not execution: - return - - queue = execution.queue - if data["msg_type"] == "error": - logger.debug( - f"Execution {parent_msg_ig} finished execution with error: {data['content']['ename']}: {data['content']['evalue']}" - ) - - if execution.errored: - return - - execution.errored = True - await queue.put( - Error( - name=data["content"]["ename"], - value=data["content"]["evalue"], - traceback="".join(data["content"]["traceback"]), - ) - ) - - elif data["msg_type"] == "stream": - if data["content"]["name"] == "stdout": - logger.debug( - f"Execution {parent_msg_ig} received stdout: {data['content']['text']}" - ) - await queue.put( - Stdout( - text=data["content"]["text"], timestamp=data["header"]["date"] - ) - ) - - elif data["content"]["name"] == "stderr": - logger.debug( - f"Execution {parent_msg_ig} received stderr: {data['content']['text']}" - ) - await queue.put( - Stderr( - text=data["content"]["text"], timestamp=data["header"]["date"] - ) - ) - - elif data["msg_type"] in "display_data": - result = Result(is_main_result=False, data=data["content"]["data"]) - logger.debug( - f"Execution {parent_msg_ig} received display data with following formats: {result.formats()}" - ) - await queue.put(result) - - elif data["msg_type"] == "execute_result": - result = Result(is_main_result=True, data=data["content"]["data"]) - logger.debug( - f"Execution {parent_msg_ig} received execution result with following formats: {result.formats()}" - ) - await queue.put(result) - - elif data["msg_type"] == "status": - if data["content"]["execution_state"] == "busy" and execution.in_background: - logger.debug(f"Execution {parent_msg_ig} started execution") - execution.input_accepted = True - - if data["content"]["execution_state"] == "idle": - if execution.input_accepted: - logger.debug(f"Execution {parent_msg_ig} finished execution") - await queue.put(EndOfExecution()) - - elif data["content"]["execution_state"] == "error": - logger.debug(f"Execution {parent_msg_ig} finished execution with error") - await queue.put( - Error( - name=data["content"]["ename"], - value=data["content"]["evalue"], - traceback="".join(data["content"]["traceback"]), - ) - ) - await queue.put(EndOfExecution()) - - elif data["msg_type"] == "execute_reply": - if data["content"]["status"] == "error": - logger.debug(f"Execution {parent_msg_ig} finished execution with error") - - if execution.errored: - return - - execution.errored = True - await queue.put( - Error( - name=data["content"].get("ename", ""), - value=data["content"].get("evalue", ""), - traceback="".join(data["content"].get("traceback", [])), - ) - ) - elif data["content"]["status"] == "abort": - logger.debug(f"Execution {parent_msg_ig} was aborted") - await queue.put( - Error( - name="ExecutionAborted", - value="Execution was aborted", - traceback="", - ) - ) - await queue.put(EndOfExecution()) - elif data["content"]["status"] == "ok": - pass - - elif data["msg_type"] == "execute_input": - logger.debug(f"Input accepted for {parent_msg_ig}") - await queue.put( - NumberOfExecutions(execution_count=data["content"]["execution_count"]) - ) - execution.input_accepted = True - else: - logger.warning(f"[UNHANDLED MESSAGE TYPE]: {data['msg_type']}") - - async def close(self): - logger.debug(f"Closing WebSocket {self.context_id}") - - if self._ws is not None: - await self._ws.close() - - if self._receive_task is not None: - self._receive_task.cancel() - - # Cancel any pending cleanup task - if self._cleanup_task and not self._cleanup_task.done(): - self._cleanup_task.cancel() - try: - await self._cleanup_task - except asyncio.CancelledError: - pass - - for execution in self._executions.values(): - execution.queue.put_nowait(UnexpectedEndOfExecution()) diff --git a/os-image/e2b-ci/code-interpreter/server/requirements.txt b/os-image/e2b-ci/code-interpreter/server/requirements.txt deleted file mode 100644 index b3e44b71..00000000 --- a/os-image/e2b-ci/code-interpreter/server/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -fastapi==0.111.0 -httpx==0.28.1 -websockets==12.0 -uvicorn[standard]==0.30.1 -requests==2.33.0 -pydantic==2.9.1 diff --git a/os-image/e2b-ci/code-interpreter/server/stream.py b/os-image/e2b-ci/code-interpreter/server/stream.py deleted file mode 100644 index 0ef0195b..00000000 --- a/os-image/e2b-ci/code-interpreter/server/stream.py +++ /dev/null @@ -1,40 +0,0 @@ -import json -from typing import Mapping, Optional, AsyncIterable - -from fastapi.encoders import jsonable_encoder -from starlette.background import BackgroundTask -from fastapi.responses import StreamingResponse - - -class StreamingListJsonResponse(StreamingResponse): - """Converts a pydantic model generator into a streaming HTTP Response - that streams a JSON list, one element at a time. - - See https://github.com/tiangolo/fastapi/issues/1978 - """ - - def __init__( - self, - content_generator: AsyncIterable, - status_code: int = 200, - headers: Optional[Mapping[str, str]] = None, - media_type: Optional[str] = None, - background: Optional[BackgroundTask] = None, - ) -> None: - body_iterator = self._encoded_async_generator(content_generator) - - super().__init__( - content=body_iterator, - status_code=status_code, - headers=headers, - media_type=media_type, - background=background, - ) - - async def _encoded_async_generator(self, async_generator: AsyncIterable): - """Converts an asynchronous pydantic model generator - into a streaming JSON list - """ - async for item in async_generator: - yield f"{json.dumps(jsonable_encoder(item))}\n" - yield '{"type": "end_of_execution"}\n' diff --git a/os-image/e2b-ci/code-interpreter/server/utils/locks.py b/os-image/e2b-ci/code-interpreter/server/utils/locks.py deleted file mode 100644 index f4a5c6ea..00000000 --- a/os-image/e2b-ci/code-interpreter/server/utils/locks.py +++ /dev/null @@ -1,18 +0,0 @@ -import asyncio - - -class LockedMap(dict): - def __init__(self): - super().__init__() - self._map_lock = asyncio.Lock() - self._locks = {} - - async def get_lock(self, key): - await self._map_lock.acquire() - if key not in self._locks: - self._locks[key] = asyncio.Lock() - - lock = self._locks[key] - print(f"Lock acquired for {key}") - self._map_lock.release() - return lock diff --git a/os-image/e2b-ci/code-interpreter/startup_scripts/0002_data.py b/os-image/e2b-ci/code-interpreter/startup_scripts/0002_data.py deleted file mode 100644 index 3e994487..00000000 --- a/os-image/e2b-ci/code-interpreter/startup_scripts/0002_data.py +++ /dev/null @@ -1,85 +0,0 @@ -import sys - -import IPython -from IPython.core.formatters import BaseFormatter, JSONFormatter -from traitlets.traitlets import Unicode, ObjectName - - -class E2BDataFormatter(BaseFormatter): - format_type = Unicode("e2b/data") - - print_method = ObjectName("_repr_e2b_data_") - _return_type = (dict, str) - - def __call__(self, obj): - # IPython invokes every registered formatter for every displayed - # object. Gate on sys.modules so a non-DataFrame output (e.g. an - # int from `1 + 1`) doesn't pay the pandas import cost — a - # pandas.DataFrame can only exist if the user already imported - # pandas. - pandas = sys.modules.get("pandas") - if pandas is None or not isinstance(obj, pandas.DataFrame): - return super().__call__(obj) - - result = obj.to_dict(orient="list") - for key, value in result.items(): - # Check each column's values - result[key] = [ - v.isoformat() if isinstance(v, pandas.Timestamp) else v for v in value - ] - return result - - -class E2BChartFormatter(BaseFormatter): - format_type = Unicode("e2b/chart") - - print_method = ObjectName("_repr_e2b_chart_") - _return_type = (dict, str) - - def __call__(self, obj): - # Same sys.modules gate as E2BDataFormatter: a matplotlib Figure - # can only exist if the user already imported matplotlib. - if sys.modules.get("matplotlib") is None: - return super().__call__(obj) - - from matplotlib.pyplot import Figure - - if not isinstance(obj, Figure): - return super().__call__(obj) - - from e2b_charts import chart_figure_to_dict - - try: - return chart_figure_to_dict(obj) - except: # noqa: E722 - return {} - - -class E2BJSONFormatter(JSONFormatter): - def __call__(self, obj): - if isinstance(obj, (list, dict)): - try: - import orjson - - return orjson.loads( - orjson.dumps( - obj, option=orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_NON_STR_KEYS - ) - ), {"expanded": True} - except TypeError: - pass - - return super().__call__(obj) - - -ip = IPython.get_ipython() -ip.display_formatter.formatters["e2b/data"] = E2BDataFormatter( - parent=ip.display_formatter -) -ip.display_formatter.formatters["e2b/chart"] = E2BChartFormatter( - parent=ip.display_formatter -) - -ip.display_formatter.formatters["application/json"] = E2BJSONFormatter( - parent=ip.display_formatter -) diff --git a/os-image/e2b-ci/code-interpreter/startup_scripts/0003_images.py b/os-image/e2b-ci/code-interpreter/startup_scripts/0003_images.py deleted file mode 100644 index 4732d77d..00000000 --- a/os-image/e2b-ci/code-interpreter/startup_scripts/0003_images.py +++ /dev/null @@ -1,24 +0,0 @@ -from typing import Any - -from IPython.core.display_functions import display -from PIL.Image import Image -from PIL.ImageShow import UnixViewer - - -def show_file(self, path: str, **options: Any) -> int: - # To prevent errors from trying to display image without any display - return 0 - - -UnixViewer.show_file = show_file -original_save = Image.save - - -def save(image, fp, format=None, **options): - if isinstance(fp, str): - display(image) - - original_save(image, fp, format, **options) - - -Image.save = save diff --git a/os-image/e2b-ci/javascript-context.patch b/os-image/e2b-ci/javascript-context.patch new file mode 100644 index 00000000..103a81a0 --- /dev/null +++ b/os-image/e2b-ci/javascript-context.patch @@ -0,0 +1,19 @@ +--- a/template/server/main.py ++++ b/template/server/main.py +@@ -41,10 +41,12 @@ + default_websockets["python"] = python_context.id + websockets["default"] = websockets[python_context.id] + +- javascript_context = await create_context( +- client, websockets, "javascript", "/home/user" +- ) +- default_websockets["javascript"] = javascript_context.id ++ kernelspecs = (await client.get(f"{JUPYTER_BASE_URL}/api/kernelspecs")).json() ++ if "javascript" in kernelspecs.get("kernelspecs", {}): ++ javascript_context = await create_context( ++ client, websockets, "javascript", "/home/user" ++ ) ++ default_websockets["javascript"] = javascript_context.id + + logger.info("Connected to default runtime") + yield