diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a407985c..1aee2a47 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -7,6 +7,7 @@ on: required: true type: choice options: + - terra-base - terra-jupyter-aou - terra-jupyter-base - terra-jupyter-bioconductor diff --git a/README.md b/README.md index 58b97e04..f2560ccf 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ This repo provides docker images for running jupyter notebook in [Terra](https:/ Make sure to go through the [contributing guide](https://github.com/DataBiosphere/terra-docker/blob/master/CONTRIBUTING.md#contributing) as you make changes to this repo. # Terra Base Images -[terra-jupyter-base](terra-jupyter-base/README.md) +[terra-base](terra-base/README.md) [terra-jupyter-python](terra-jupyter-python/README.md) @@ -20,15 +20,18 @@ Make sure to go through the [contributing guide](https://github.com/DataBiospher # How to create your own Custom image to use with notebooks on Terra Custom docker images need to use a Terra base image (see above) in order to work with the service that runs notebooks on Terra. * You can use any of the base images above -* Here is an example of how to build off of a base image: Add `FROM us.gcr.io/broad-dsp-gcr-public/terra-jupyter-base:0.0.1` to your dockerfile (`terra-jupyter-base` is the smallest image you can extend from) + * `terra-base` is the smallest image, but doesn't include any scientific packages on top of Jupyter and R +* Here is an example of how to build off of a base image: Add `FROM us.gcr.io/broad-dsp-gcr-public/terra-base:0.0.1` to your dockerfile * Customize your image (see the [terra-jupyter-python](terra-jupyter-python/Dockerfile) dockerfile for an example of how to extend from one of our base images -* Publish the image to either GCR or Dockerhub; the image must be public to be used +* Publish the image to either GAR or Dockerhub; + * If using Dockerhub, the image **must be public** to be used * Use the published container image location when creating notebook runtime * Dockerhub image example: [image name]:[tag] -* GCR image example: us.gcr.io/repository/[image name]:[tag] -* Since 6/28/2021, we introduced a few changes that might impact building custom images - - Home directory of new images will be `/home/jupyter`. This means if your dockerfile is referencing `/home/jupyter-user` directory, you need to update it to $HOME (recommended) or `/home/jupyter`. - - Creating VMs with custom images will take much longer than terra supported images because `docker pull` will take a few min. If the custom image ends up being too large, VM creation may time out. New base images are much larger in size than previous versions. +* GAR image example: us.gcr.io/repository/[image name]:[tag] +* Some things to keep in mind when creating custom images: + - The home directory of new images will be `/home/jupyter`. This means if your dockerfile is referencing the `/home/jupyter-user` directory, you need to update it to $HOME (recommended) or `/home/jupyter`. + - Creating VMs with custom images may take longer than terra supported images because `docker pull` will take a few min. If the custom image ends up being too large, VM creation may time out. +- # Development ## Using git secrets @@ -56,25 +59,25 @@ Once you have the container running, you should be able to access jupyter at htt Detailed documentation on how to integrate the terra-docker image with Leonardo can be found [here](https://broadworkbench.atlassian.net/wiki/spaces/IA/pages/2519564289/Integrating+new+Terra+docker+images+with+Leonardo) ### If you are adding a new image: -- Create a new directory with the Dockerfile and a CHANGELOG.md. +- Create a new directory with the Dockerfile and a CHANGELOG.md. - Add the directory name (also referred to as the image name) as an entry to the image_data array in the file in config/conf.json. For more info on what is needed for a new image, see the section on the config - If you wish the image to be baked into our custom image, which makes the runtime load significantly faster (recommended), make a PR into the leonardo [repo](https://github.com/DataBiosphere/leonardo) doing the following within the `jenkins` folder: - - Add the image to the parameter list in the Jenkinsfile - - Update the relevant `prepare` script in each subdirectory. Currently there is a prepare script for gce and dataproc. - - It is recommended to add a test in the `automation` directory (`automation/src/test/resources/reference.conf`) - - Add your image to the `reference.conf` in the automation directory. This will be the only place any future version updates to your image happen. This ensures, along with the test in the previous step, that any changes to the image are tested. - - Run the GHA to generate the image, and add it to `reference.conf` in the http directory (`http/src/main/resources/reference.conf`) + - Add the image to the parameter list in the Jenkinsfile + - Update the relevant `prepare` script in each subdirectory. Currently there is a prepare script for gce and dataproc. + - It is recommended to add a test in the `automation` directory (`automation/src/test/resources/reference.conf`) + - Add your image to the `reference.conf` in the automation directory. This will be the only place any future version updates to your image happen. This ensures, along with the test in the previous step, that any changes to the image are tested. + - Run the GHA to generate the image, and add it to `reference.conf` in the http directory (`http/src/main/resources/reference.conf`) ### If you are updating an existing image: - [Create your terra-docker PR](https://broadworkbench.atlassian.net/wiki/spaces/IA/pages/2519564289/Integrating+new+Terra+docker+images+with+Leonardo#1.-Create-a-terra-docker-PR) - - Update the version in config/conf.json - - Update CHANGELOG.md and VERSION file - - Ensure that no `From` statements need to be updated based on the image you updated (i.e., if you update the base image, you will need to update several other images) - - Run updateVersions.sc to bump all images dependent on the base + - Update the version in config/conf.json + - Update CHANGELOG.md and VERSION file + - Ensure that no `From` statements need to be updated based on the image you updated (i.e., if you update the base image, you will need to update several other images) + - Run updateVersions.sc to bump all images dependent on the base - [Merge your terra-docker PR and check if the image(s) and version json files are created](https://broadworkbench.atlassian.net/wiki/spaces/IA/pages/2519564289/Integrating+new+Terra+docker+images+with+Leonardo#2.-Merge-your-terra-docker-PR-and-check-images-are-created) - [Open a PR in leonardo](https://broadworkbench.atlassian.net/wiki/spaces/IA/pages/2519564289/Integrating+new+Terra+docker+images+with+Leonardo#3.-Create-a-new-leo-PR-that-integrates-the-new-images) - - Update the relevant `prepare` script within the `jenkins` folder - - Update the automation `reference.conf` file + - Update the relevant `prepare` script within the `jenkins` folder + - Update the automation `reference.conf` file - [Run the GHA on your branch to generate the new image](https://broadworkbench.atlassian.net/wiki/spaces/IA/pages/2519564289/Integrating+new+Terra+docker+images+with+Leonardo#4.-Run-the-Github-Action-in-leo-to-generate-a-new-custom-COS-image) - [Update the leonardo PR to use the newly generated image](https://broadworkbench.atlassian.net/wiki/spaces/IA/pages/2519564289/Integrating+new+Terra+docker+images+with+Leonardo#5.-Update-the-Leo-PR-to-use-the-generated-OS-images) - Ensure that the `terra-docker-versions-candidate.json` file (which is what the UI sources the dropdown from) in the `terra-docjker-image-documentation-[env]` bucket correclty references your new docker image @@ -85,11 +88,11 @@ Detailed documentation on how to integrate the terra-docker image with Leonardo Build the image: run `docker build [your_dir] -t [name]`. -`docker build terra-jupyter-base -t terra-jupyter-base` +`docker build terra-base -t terra-base` If you're on an M1 and building an image from a locally built image, replace the current FROM command: -`FROM --platform=linux/amd64 terra-jupyter-base` +`FROM --platform=linux/amd64 terra-base` Apple Silicon chips later then M1 (ex. M3) need: @@ -120,9 +123,9 @@ To launch an image through Terra, navigate to https://app.terra.bio or your BEE' ## Config -There is a config file located at `config/conf.json` that contains the configuration used by all automated jobs and build scripts that interface with this repo. +There is a config file located at `config/conf.json` that contains the configuration used by all automated jobs and build scripts that interface with this repo. -There is a field for "spark_version" top-level which must be updated if we update the debian version used in the custom image. +There is a field for "spark_version" top-level which must be updated if we update the debian version used in the custom image. Currently it assumes 1.4x https://cloud.google.com/dataproc/docs/concepts/versioning/dataproc-release-1.4 There are some constants included, such as the tools supported by this repo. Of particular interest is the image_data array. @@ -163,7 +166,7 @@ Each time you update or add an image, you will need to update the appropriate en The scripts folder has scripts used for building. - `generate_package_docs.py` This script is run once by build.sh each time an image is built. It is used to generate a .json with the versions for the packages in the image. -- `generate_version_docs.py` This script is run each time an image is built. It builds a new file master version file for the UI to look up the current versions to reference. +- `generate_version_docs.py` This script is run each time an image is built. It builds a new file master version file for the UI to look up the current versions to reference. ## Image dependencies diff --git a/build_all.sh b/build_all.sh index 1faa8d6b..04912cc2 100755 --- a/build_all.sh +++ b/build_all.sh @@ -3,16 +3,17 @@ # Example: ./build_all.sh # Create the ordered list of images to build -# 1- terra-jupyter-base -# 2- terra-jupyter-python -# 3- terra-jupyter-r -# 4- terra-jupyter-gatk -# 5- terra-jupyter-hail -# 6- terra-jupyter-aou -# 7- terra-jupyter-bioconductor -# 8- terra-rstudio-aou -# 9- wondershaper -images=("terra-jupyter-base" "terra-jupyter-python" "terra-jupyter-r" "terra-jupyter-gatk" "terra-jupyter-hail" "terra-jupyter-aou" "terra-jupyter-bioconductor" "terra-rstudio-aou" "wondershaper") +# 1- terra-base +# 2- terra-jupyter-base +# 3- terra-jupyter-python +# 4- terra-jupyter-r +# 5- terra-jupyter-gatk +# 6- terra-jupyter-hail +# 7- terra-jupyter-aou +# 8- terra-jupyter-bioconductor +# 9- terra-rstudio-aou +# 10- wondershaper +images=("terra-base", "terra-jupyter-base" "terra-jupyter-python" "terra-jupyter-r" "terra-jupyter-gatk" "terra-jupyter-hail" "terra-jupyter-aou" "terra-jupyter-bioconductor" "terra-rstudio-aou" "wondershaper") # Loop over each image to build in the correct order for image in "${images[@]}"; do @@ -22,4 +23,4 @@ for image in "${images[@]}"; do done # Once all images have been built, generate and push the 'terra-docker-versions-new' doc -python scripts/generate_version_docs.py \ No newline at end of file +python scripts/generate_version_docs.py diff --git a/config/conf.json b/config/conf.json index d36a6d2a..7758b4b8 100644 --- a/config/conf.json +++ b/config/conf.json @@ -95,6 +95,24 @@ "include_in_custom_gce" : true } }, + { + "name" : "terra-base", + "base_label" : "New Base", + "tools" : [ + "python" + ], + "packages" : { + + }, + "version" : "1.0.0", + "automated_flags" : { + "generate_docs" : true, + "include_in_ui" : true, + "build" : true, + "include_in_custom_dataproc" : true, + "include_in_custom_gce" : true + } + }, { "name" : "terra-jupyter-r", "base_label" : "R", diff --git a/terra-base/CHANGELOG.md b/terra-base/CHANGELOG.md new file mode 100644 index 00000000..920a33d8 --- /dev/null +++ b/terra-base/CHANGELOG.md @@ -0,0 +1,10 @@ +## 1.0.0 - 12/16/2025 + +- Extends GPU-enabled Ubuntu base image +- Uses Python 3.10 +- Add UV +- Add conda +- Add Jupyter +- Add Leonardo customizations/extensions + +Image URL: `us.gcr.io/broad-dsp-gcr-public/terra-base:1.0.0` diff --git a/terra-base/Dockerfile b/terra-base/Dockerfile new file mode 100644 index 00000000..7e43dbca --- /dev/null +++ b/terra-base/Dockerfile @@ -0,0 +1,250 @@ +# Latest gpu-enabled base image on Ubuntu 22, 132 MB compressed +FROM --platform=linux/amd64 nvidia/cuda:13.0.1-base-ubuntu22.04 + +LABEL maintainer="DSP Analysis Team " + +# want the command to fail due to an error at any stage in the pipe: https://github.com/hadolint/hadolint/wiki/DL4006 +SHELL ["/usr/bin/bash", "-o", "pipefail", "-c"] + +####################### +# General Environment Variables +####################### +ENV DEBIAN_FRONTEND=noninteractive +ENV LC_ALL=en_US.UTF-8 + +# Version of python to be installed and used +ENV PYTHON_VERSION=3.10 +# Paired conda installer +ENV CONDA_INSTALLER=https://repo.anaconda.com/miniconda/Miniconda3-py310_25.9.1-1-Linux-x86_64.sh +ENV JUPYTER_VERSION=5.7.2 +ENV NODE_MAJOR=20 + +################# +# Install Prerequisites +################# +RUN apt-get update && apt-get install -yq --no-install-recommends \ + # basic necessities + sudo \ + ca-certificates \ + curl \ + jq \ + # gnupg requirement + gnupg \ + dirmngr \ + # useful utilities for debugging within docker itself \ + nano \ + less \ + procps \ + lsb-release \ + # gcc compiler + build-essential \ + locales \ + # for ssh-agent and ssh-add + keychain \ + # extras \ + wget \ + bzip2 \ + git \ + # Uncomment en_US.UTF-8 for inclusion in generation + && sed -i 's/^# *\(en_US.UTF-8\)/\1/' /etc/locale.gen \ + # Generate locale + && locale-gen \ + # cleanup + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +############################## +# Set up Node for Jupyterlab +############################## +RUN curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key --keyring /usr/share/keyrings/cloud.google.gpg add - +RUN curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - + +# Install Node >18 +RUN apt-get update && apt-get install -yq --no-install-recommends +RUN mkdir -p /etc/apt/keyrings +RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg + +RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list +RUN dpkg --remove --force-remove-reinstreq libnode-dev +RUN apt-get update && apt-get install -f -yq nodejs + +########################## +# Create the User's Jupyter User +########################## +# Create the jupyter user and give sudo permission +ENV USER=jupyter +# This UID must stay static to be one greater than the welder user (1001) +ENV USER_UID=1002 + +# Create the user home and add to the users group +ENV USER_HOME=/home/$USER +RUN useradd -m -s /bin/bash -d $USER_HOME -N -u $USER_UID $USER +RUN usermod -g users $USER + +# We want to grant the user sudo permissions without password + # so they can install the necessary packages that they want to use on the docker container +RUN echo "$USER ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/$USER \ + && chmod 0440 /etc/sudoers.d/$USER + +############ +# Install R +############ +# add R repo for later R package installation \ +RUN wget -qO- https://cloud.r-project.org/bin/linux/ubuntu/marutter_pubkey.asc | tee -a /etc/apt/trusted.gpg.d/cran_ubuntu_key.asc \ + && apt-key adv --keyserver keyserver.ubuntu.com --recv-keys E298A3A825C0D65DFD57CBB651716619E084DAB9 \ + && apt-get update && apt-get install -yq --no-install-recommends software-properties-common \ + && add-apt-repository --no-update "deb https://cloud.r-project.org/bin/linux/ubuntu jammy-cran40/" \ + # Install R base + && apt-get update && apt-get install -y r-base \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +############################################ +# Install Miniconda and setup base conda environment +############################################ +## CONDA should not be used by devs to manage package dependencies, +# but is a widely used tool to manage python environments in a runtime + # and we should provide it to users + +# download and install conda +ENV CONDA_HOME=/opt/conda + +RUN curl -so $HOME/miniconda.sh $CONDA_INSTALLER \ + && chmod +x $HOME/miniconda.sh \ + && $HOME/miniconda.sh -b -p $CONDA_HOME \ + && rm $HOME/miniconda.sh +# ENV PATH="${PATH}:${CONDA_ENV_HOME}/bin:${CONDA_HOME}/bin" +ENV PATH="${PATH}:${CONDA_HOME}/bin" + +# In order to override the default kernel, the conda environment must be named 'python3' +ENV CONDA_ENV_NAME=python3 +ENV CONDA_ENV_HOME=$USER_HOME/.envs/$CONDA_ENV_NAME +ENV CONDA_FILES=$CONDA_HOME/custom + +# Copy over the conda environment files +COPY conda/ $CONDA_FILES +ENV CONDA_FILES=$CONDA_HOME/custom + +# Set up the path to the user python from conda +ENV BASE_PYTHON_PATH=$CONDA_HOME/bin/python$PYTHON_VERSION +# Tell conda to NOT write bite code (aka these.pyc files) +ENV PYTHONDONTWRITEBYTECODE=true + +# Have to accept the conda terms of service to be able to install packages +RUN conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/main \ + && conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/r + +RUN conda env create --prefix $CONDA_ENV_HOME --file $CONDA_FILES/conda-environment.yaml \ + # Remove packages tarballs and python bytecode files from the image + && conda clean -afy \ + && rm $CONDA_FILES/conda-environment.yaml \ + # Make sure the USER is the owner of the folder where the base conda is installed + && chown -R $USER:users $USER_HOME + +# Create the base conda environment that will be used by the jupyter user +RUN conda run -p ${CONDA_ENV_HOME} python -m ipykernel install --name=$CONDA_ENV_NAME + + +############################## +# Setup UV Environment for Jupyter +############################## +# Using UV (Universal Virtualenv) to create a virtual environment +# UV is used in place of poetry for speed and simplicity. + +# NOTE: this is for the environment the jupyter server will be running in, +# separate from the jupyter user's conda environment. +COPY uv.lock . +COPY pyproject.toml . + +# Setup UV environment variables +# - tells uv to copy the Python files into the container from the cache mount, +# - tell uv to byte-compile packages for faster application startups, +# - don't seed venv, we need to install separately +# - don't cache to keep the image size small +ENV UV_LINK_MODE=copy \ + UV_COMPILE_BYTECODE=1 \ + UV_VENV_SEED=false \ + UV_NO_CACHE=true + +# Download the latest installer +ADD https://astral.sh/uv/install.sh /uv-installer.sh +RUN sh /uv-installer.sh && rm /uv-installer.sh + +# Add local bin to PATH for uv +ENV PATH="/root/.local/bin/:$PATH" + +ENV JUPYTER_HOME=/etc/jupyter + +# Create a virtual environment and install jupyter packages +# setuptools and wheel are required for some of the jupyter extensions +RUN uv venv $JUPYTER_HOME --python $BASE_PYTHON_PATH \ + && source $JUPYTER_HOME/bin/activate \ + && uv pip install wheel \ + && uv pip install setuptools \ + && uv pip install -r pyproject.toml --no-cache --no-build-isolation \ + # Cleanup + && rm uv.lock && rm pyproject.toml + +# add jupyter to path +ENV PATH="${PATH}:${JUPYTER_HOME}/bin" + +# Remove default jupyter kernel (to force use of the conda python kernel) +RUN $JUPYTER_HOME/bin/jupyter kernelspec remove python3 -y + +# ####################### +# # Terra-specific Utilities +# ####################### \ + +# Copy over utility scripts and config files +ENV JUPYTER_SCRIPTS $JUPYTER_HOME/scripts +ENV JUPYTER_CUSTOM $JUPYTER_HOME/custom +ENV JUPYTER_EXTENSIONS $JUPYTER_SCRIPTS/extension +ENV JUPYTER_KERNEL $JUPYTER_SCRIPTS/kernel + +COPY scripts $JUPYTER_HOME/scripts +COPY custom $JUPYTER_HOME/custom +COPY jupyter_notebook_config.py $JUPYTER_HOME + +# give user ownership of jupyter home and conda env home +# and make extension files executable +RUN chown -R $USER:users $JUPYTER_HOME $CONDA_HOME \ +&& find $JUPYTER_EXTENSIONS -name '*.sh' -type f | xargs chmod +x + +# make the run-jupyter script executable +RUN chmod +x -R $JUPYTER_SCRIPTS/run-jupyter.sh + +# Setup jupyter and r kernels +ENV JUPYTER_KERNELSPEC_DIR=/usr/local/share/jupyter/ +RUN chown -R $USER:users $JUPYTER_KERNELSPEC_DIR \ + && find $JUPYTER_KERNEL -name '*.sh' -type f | xargs chmod +x \ + # You can get kernel directory by running `jupyter kernelspec list` + && $JUPYTER_KERNEL/kernelspec.sh $JUPYTER_KERNEL $JUPYTER_KERNELSPEC_DIR/kernels + +# Create the welder user +# The welder uid is consistent with the Welder docker definition here: +# https://github.com/DataBiosphere/welder/blob/master/project/Settings.scala +# Adding welder-user to the Jupyter container isn't strictly required, but it makes welder-added +# files display nicer when viewed in a terminal. +ENV WELDER_USER welder-user +# This UID must stay consistent with the UID defined in Welder +ENV WELDER_UID 1001 +RUN useradd -m -s /bin/bash -N -u $WELDER_UID $WELDER_USER + +# Add an empty file to indicate that this is a terra-base image with a separate jupyer server environment +# Need a flag to check in the startup scripts to know whether to use the conda environment jupyter +RUN touch /etc/jupyter/.terra-base-marker + +# Make sure that the jupyter user will have access to the jupyter path in the working directory +EXPOSE $JUPYTER_PORT +WORKDIR $USER_HOME + +# make pip install to a user directory, instead of a system directory which requires root. +# this is useful so `pip install` commands can be run in the context of a notebook. +ENV PIP_USER=true +USER $USER + +# Note: this entrypoint is provided for running Jupyter independently of Leonardo. +# When Leonardo deploys this image onto a cluster, the entrypoint is overwritten to enable +# additional setup inside the container before execution. Jupyter execution occurs when the +# init-actions.sh script uses 'docker exec' to call run-jupyter.sh. +ENTRYPOINT ["/etc/jupyter/bin/jupyter", "notebook"] \ No newline at end of file diff --git a/terra-base/README.md b/terra-base/README.md new file mode 100644 index 00000000..35c4ca07 --- /dev/null +++ b/terra-base/README.md @@ -0,0 +1,62 @@ +# terra-jupyter-base image + +This repo contains the terra-jupyter-base image that is compatible with notebook service in [Terra]("https://app.terra.bio/") called Leonardo. For example, use `us.gcr.io/broad-dsp-gcr-public/terra-base:{version}` in terra. + +## Context +This image is different from the legacy base image that is currently use to build the docker images cached in Terra (terra-jupyter-base). +The current purpose of this new base image is to to unblock users from creating custom images in Terra. + +## Image contents + +The terra-jupyter-base extends the [Ubuntu base image](https://hub.docker.com/layers/nvidia/cuda/13.0.1-base-ubuntu24.04/images/sha256-995e80db6d0c3a53d56bd00bba48a0ebd633b67b99a57e16acf9a306e7c744a7) by including the following: + +- OS prerequisites +- google-cloud-sdk +- Python 3.10 +- Conda +- Jupyter & JupyterLab +- Leonardo customizations/extensions +- Terra notebook utils + +To see the complete contents of this image please see the [Dockerfile](./Dockerfile). + +## Selecting prior versions of this image + +To select an older version this image, you can search the [CHANGELOG.md](./CHANGELOG.md) for a specific package version you need. + +Once you find an image version that you want, simply copy and paste the image url from the changelog into the corresponding custom docker field in the Terra notebook runtime widget. + +## Updating the UV packages + To update UV packages, first cd into the`terra-base` directory, then either: + - run `uv add ` or remove to add or remove a specific package in the project + - modify the pyproject.toml file, then run `uv lock` + +To activate a virtual environment for local testing: +```bash +source .venv/bin/activate +uv sync +``` +(.venv is created when you run `uv install`) + +## Building the image +To build the image locally, run the following command in the root of the repo: + +```bash + docker build terra-base -t us.gcr.io/broad-dsp-gcr-public/terra-base:{version} + docker push us.gcr.io/broad-dsp-gcr-public/terra-base:{version} +``` + +## NOTE: Changing paths +If you change the following paths: +- `JUPYTER_HOME` +- `JUPYTER_USER` +- `CONDA_HOME` + +You may need to update the following files appropriately here and in Leonardo: +- terra-docker + - `run_jupyter.sh` + - the notebook extension scripts +- leonardo + - `gce-init.sh` + - `init-action.sh` + - the `jupyterUserhome` in RuntimeTemplateValues \ No newline at end of file diff --git a/terra-base/conda/conda-environment.yaml b/terra-base/conda/conda-environment.yaml new file mode 100644 index 00000000..11b599cd --- /dev/null +++ b/terra-base/conda/conda-environment.yaml @@ -0,0 +1,7 @@ +name: python3 +channels: + - defaults +dependencies: + - pip=23.1.2 + - python=3.10 + - ipykernel \ No newline at end of file diff --git a/terra-base/conda/conda_init.txt b/terra-base/conda/conda_init.txt new file mode 100644 index 00000000..b36849ea --- /dev/null +++ b/terra-base/conda/conda_init.txt @@ -0,0 +1,15 @@ +# >>> conda initialize >>> +# !! Contents within this block are managed by 'conda init' !! +# python3 is the name of the conda environment, must match environment name in environment.yml +__conda_setup="$('/home/jupyter/.envs/python3/bin/conda' 'shell.bash' 'hook' 2> /dev/null)" +if [ $? -eq 0 ]; then +eval "$__conda_setup" +else +if [ -f "/home/jupyter/.envs/python3/etc/profile.d/conda.sh" ]; then +. "/home/jupyter/.envs/python3/etc/profile.d/conda.sh" +else +export PATH="/home/jupyter/.envs/python3/bin:$PATH" +fi +fi +unset __conda_setup +# <<< conda initialize <<< \ No newline at end of file diff --git a/terra-base/custom/.eslintrc.js b/terra-base/custom/.eslintrc.js new file mode 100644 index 00000000..a1781ad1 --- /dev/null +++ b/terra-base/custom/.eslintrc.js @@ -0,0 +1,32 @@ +module.exports = { + 'env': { + 'browser': true, + 'es6': true, + }, + 'extends': [ + 'google', + ], + 'globals': { + 'Atomics': 'readonly', + 'SharedArrayBuffer': 'readonly', + }, + 'parserOptions': { + 'ecmaVersion': 2018, + 'sourceType': 'module', + }, + 'rules': { + 'comma-dangle': ['error', 'never'], + //80 is the standard, but this required the least refactoring. + 'max-len': ['error', { 'code': 150 }], + //TODO: remove the following rules and fix the offenses + 'require-jsdoc': ['error', { + 'require': { + 'FunctionDeclaration': false, + 'MethodDefinition': false, + 'ClassDeclaration': false, + 'ArrowFunctionExpression': false, + 'FunctionExpression': false + } + }] + }, +}; \ No newline at end of file diff --git a/terra-base/custom/README.md b/terra-base/custom/README.md new file mode 100644 index 00000000..54a6e25e --- /dev/null +++ b/terra-base/custom/README.md @@ -0,0 +1,6 @@ +#Before you merge, lint the files with eslint. + +# `brew install eslint` +# `brew install npm` +# `npm i` +# `eslint --fix *.js` diff --git a/terra-base/custom/edit-mode.js b/terra-base/custom/edit-mode.js new file mode 100644 index 00000000..3670fb1f --- /dev/null +++ b/terra-base/custom/edit-mode.js @@ -0,0 +1,447 @@ +define(() => { + // define default values for config parameters + const params = { + googleProject: '', + clusterName: '', + welderEnabled: 'false' + }; + + // update params with any specified in the server's config file + function updateParams() { + const config = Jupyter.notebook.config; + for (const key in params) { + if (config.data.hasOwnProperty(key)) { + params[key] = config.data[key]; + } + } + + // generate URLs based on params + const leoUrl = ''; // we are choosing to use a relative path here + const welderUrl = leoUrl + `/proxy/${params.googleProject}/${params.clusterName}/welder`; + + // URLS for local testing + // let leoUrl = 'http://localhost:8080' //for testing against local server + // let welderUrl = leoUrl + // params.jupyterServerApi = '/api/contents/' + // params.jupyterFsHref = '/notebooks/' + + params.jupyterServerApi = leoUrl + `/notebooks/${params.googleProject}/${params.clusterName}` + '/api/contents/'; + params.jupyterFsHref = leoUrl + `/notebooks/${params.googleProject}/${params.clusterName}/notebooks/`; + params.localizeUrl = welderUrl + '/objects'; + params.checkMetaUrl = welderUrl + '/objects/metadata'; + params.lockUrl = welderUrl + '/objects/lock'; + } + + let modalOpen = false; + // this needs to be available so the loop can be cancelled where needed + let syncMaintainer; + let shouldExit = false; + + const syncIssueButtons = (res) => { + return { + 'Make a Copy': { + 'click': () => notebookSaveAs(), + 'class': 'btn-primary', + 'id': 'modal-copy-1' + }, + 'Reload the workspace version and discard your changes': { + 'click': () => updateLocalCopyWithRemote(res), + 'id': 'modal-reload' + } + }; + }; + + const lockIssueButtons = (res) => { + return { + 'Run in Playground Mode': { + 'click': () => openPlaygroundMode(res), + 'class': 'btn-primary', + 'id': 'modal-playground' + }, + 'Make a Copy': { + 'click': () => notebookSaveAs(), + 'id': 'modal-copy-2' + } + }; + }; + + const noRemoteFileButtons = { + 'Continue working': { + 'click': () => {}, + 'class': 'btn-primary' + } + }; + + const modeBannerId = 'notification_mode'; + const lockConflictTitle = 'File is in use'; + const syncIssueTitle = 'File versions out of sync'; + const syncIssueBody = 'Your version of this file does not match the version in the workspace. What would you like to do?'; + const syncIssueNotFoundBody = 'This file was either deleted from or was never saved to the workspace.'; + const lastLockedTimer = 60000; + + const headers = { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Headers': '*' + }; + + const basePayload = { + mode: 'no-cors', + headers: headers + }; + + function init() { + console.info('edit mode plugin initialized'); + + if (!Jupyter.notebook) { + return; // exit, they are in list view + } + + updateParams(); + + if (!(params.welderEnabled == 'true')) { + console.info('welder is not enabled'); + return; + } + + checkMeta(); + + const initialSyncDelay = 27000; + // we want subsequent checkMeta + lock calls to be off-cycle of autosave + setTimeout(() => initSyncMaintainer(), initialSyncDelay); + } + + function initSyncMaintainer() { + syncMaintainer = setInterval(() => { + checkMeta(); + }, lastLockedTimer); + + window.onbeforeunload(function() { + clearInterval(syncMaintainer); + }); + } + + function checkMeta() { + const localPath = { + localPath: Jupyter.notebook.notebook_path + }; + + console.info('calling /objects/metadata/ with payload: ', JSON.stringify(localPath)); + + const payload = { + ...basePayload, + body: JSON.stringify(localPath), + method: 'POST' + }; + + return fetch(params.checkMetaUrl, payload) + .then((res) => { + processInitialCheckMeta(res); + return res.json(); + }) + .then((res) => { + handleMetaSuccess(res); + return res; + }) + .catch((err) => { + handleMetaFailure(err); + }); + } + + function renderFileNotTrackedBanner() { + removeElementById(modeBannerId); + + const toolTipText = '

Your changes are not being saved to the workspace.

'; + + $('#notification_area').append( + $('
').attr({ + 'id': 'notification_not_saving', + 'class': 'btn-warning btn btn-xs navbar-btn', + 'data-toggle': 'tooltip', + 'data-html': 'true', + 'title': toolTipText + }) + .tooltip({ + 'content': function() { + return $(this).prop('title'); + }, + 'placement': 'bottom' + }) + .append( + $('').html('Remote Save Disabled') + .append(' ') + ) + ); + } + + function processInitialCheckMeta(res) { + if (!res.ok) { + if (res.status == 412) { + console.warn('detected 412 from /objects/metadata. stopping loop'); + renderFileNotTrackedBanner(); + shouldExit = true; + clearInterval(syncMaintainer); + } + + throw Error(res.statusText); + } + } + + function handleMetaSuccess(res) { + toggleMetaFailureBanner(false); // sets banner for meta status + + const isEditMode = res.syncMode == 'EDIT'; + + if (isEditMode) { + handleCheckMetaResp(res); // displays modal if theres an issue in the payload + getLock(res); // gets lock if in edit mode + } + renderModeBanner(isEditMode); // sets edit/safe mode banner + } + + function handleMetaFailure(err) { + console.error(err); + if (!shouldExit) { + removeElementById(modeBannerId); + toggleMetaFailureBanner(true); + } + } + + // this function assumes any status not included in these lists represents a notebook out of sync + // this is done to defend against future fields being added being auto-categorized as failures + function handleCheckMetaResp(res) { + const healthySyncStatuses = ['LIVE']; + // below is included for reference, but commented out so it does not cause errors with linting + // const outOfSyncStatuses = ['DESYNCHRONIZED', 'REMOTE_CHANGED']; + const notFoundStatus = ['REMOTE_NOT_FOUND']; + const saveNeededStatus = ['LOCAL_CHANGED']; + + if (healthySyncStatuses.includes(res.syncStatus)) { + console.info('healthy sync status detected: ', res.syncStatus); + } else if (notFoundStatus.includes(res.syncStatus)) { + promptUserWithModal(syncIssueTitle, noRemoteFileButtons, syncIssueNotFoundBody); + } else if (saveNeededStatus.includes(res.syncStatus)) { + console.info('detected that we have changes that have not been delocalized.'); + // It is possible saving is the right call here (aka $("#save-notbook > button").click()), but we already do that on a periodic tick + // adding it here could possibly cause confusion + } else { + promptUserWithModal(syncIssueTitle, syncIssueButtons(res), syncIssueBody); + } + } + + function getLock(metaRes) { + const payload = { + ...basePayload, + method: 'POST', + body: JSON.stringify({localPath: Jupyter.notebook.notebook_path}) + }; + + fetch(params.lockUrl, payload) + .then((res) => { + handleLockStatus(res, metaRes); + }) + .catch((err) => { + console.error(err); + }); + } + + function toggleMetaFailureBanner(shouldShow) { + const bannerId = 'notification_metaFailure'; + const bannerText = 'Failed to check notebook status, changes may not be saved to workspace. Retrying...'; + + removeElementById(bannerId); + + if (shouldShow) { + const bannerStyling = 'btn btn-xs navbar-btn btn-danger'; + + $('#notification_area').append( + $('
').attr({ + 'id': bannerId, + 'class': bannerStyling + }) + .append($('').html(' ' + bannerText)) + ); + } + } + + const lockConflictBody = `

This file is currently being edited by another user. ` + + `Please allow 2-3 minutes after this user has closed the file for it to become available for editing.

` + + `

You can make a copy, or run it in Playground Mode to explore and execute its contents without saving any changes.`; + + function handleLockStatus(res, metaRes) { + if (!res.ok) { + const status = res.status; + const errorText = res.statusText; + + if (status == 409) { + res.json().then((res) => { + promptUserWithModal(lockConflictTitle, lockIssueButtons(metaRes), lockConflictBody); + }); + } + // for the lock endpoint, we consider all non 'ok' statuses an error + throw new Error(errorText); + } + } + + + function promptUserWithModal(title, buttons, htmlBody) { + if (modalOpen) return; + + modalOpen = true; + + // we need to require here because otherwise we haven't checked if the module is loaded + // (i.e. if we are in terminal view) + const dialog = require('base/js/dialog'); + + dialog.modal({ + body: $('

').html(htmlBody), + title: title, + buttons: buttons, + notebook: Jupyter.notebook, + keyboard_manager: Jupyter.notebook.keyboard_manager + }) + .on('hidden.bs.modal', () => modalOpen = false) + .attr('id', 'leoUserModal') + .find('.close').remove(); // TODO: test going back + } + + async function openPlaygroundMode(metaRes) { + const originalNotebookName = Jupyter.notebook.notebook_name; + + const safeModeDir = metaRes.storageLink.localSafeModeBaseDirectory; + + if (Jupyter.notebook.notebook_path.includes(safeModeDir)) { + console.warn('Attempted to navigate to enter safe mode while already in safe mode. Exitting.'); + return; // we're here already + } + + // create a new file with the contents + const postPayload = { + ...basePayload, + method: 'POST', + body: JSON.stringify({ + copy_from: Jupyter.notebook.notebook_path + }) + }; + + const patchPayload = { + headers: headers, + method: 'PATCH', + body: JSON.stringify({ + path: safeModeDir + '/' + originalNotebookName + }) + }; + + fetch(params.jupyterServerApi + safeModeDir, postPayload) + .then((res) => handleJupyterServerResponse(res)) + .then((res) => { + // then we rename the file, as POST does not allow us to specify the file name + fetch(params.jupyterServerApi + res.path, patchPayload) + .then((res) => { + // navigate to new file + window.location.href = params.jupyterFsHref + safeModeDir + '/' + originalNotebookName; + }); + }); + } + + function notebookSaveAs() { + // we need to require here because otherwise we haven't checked if the module is loaded + // (i.e. if we are in terminal view) + const utils = require('base/js/utils'); + // guarantees a path in [0] and file name in [1]. [0] is "" if just a file is passed + const originalPathSplit = utils.url_path_split(Jupyter.notebook.notebook_path); + const newNotebookPath = originalPathSplit[0]; + + // create a new file with the contents + const payload = { + ...basePayload, + method: 'POST', + body: JSON.stringify({ + copy_from: Jupyter.notebook.notebook_path + }) + }; + + fetch(params.jupyterServerApi + newNotebookPath, payload) + .then((res) => handleJupyterServerResponse(res)) + .then((res) => { + // navigate to new file. we rely on the jupyter post api to supply the name of the file we have created as it ensures it does not exist + // POST also does not allow for the specification of a file name + window.location.href = params.jupyterFsHref + res.path; + }); + } + + function handleJupyterServerResponse(res) { + if (!res.ok) { + throw new Error('failed to perform requested action, the jupyter server is unavailable'); + } + return res.json(); + } + + function removeElementById(id) { + if (!$('#' + id).length == 0) { + $('#' + id).remove(); + } + } + + // shows the user whether they are in playground mode or edit mode + function renderModeBanner(isEditMode) { + removeElementById(modeBannerId); // we always remove the banner because we re-render each loop + + let bannerText; + let toolTipText; + let bannerStyling; + + const baseStyling = 'btn btn-xs navbar-btn'; + + if (isEditMode) { + bannerText = 'Edit Mode'; + toolTipText = '

You have locked this file for editing and your changes are being automatically saved to the workspace.

'; + bannerStyling = 'notification_widget ' + baseStyling; + } else { + bannerText = 'Playground Mode (Edits not saved)'; + toolTipText = '

Playground mode allows you to explore, change, and run the notebook, but changes are not saved to the workspace.

'; + bannerStyling = 'btn-warning ' + baseStyling; + } + + $('#notification_area').append( + $('
').attr({ + 'id': modeBannerId, + 'class': bannerStyling, + 'data-toggle': 'tooltip', + 'data-html': 'true', + 'title': toolTipText + }) + .tooltip({ + 'content': function() { + return $(this).prop('title'); + }, + 'placement': 'bottom' + }) + .append( + $('').html(bannerText) + .append(' ') + ) + ); + } + + async function updateLocalCopyWithRemote(meta) { + const entries = { + action: 'localize', + entries: [{ + sourceUri: meta.storageLink.cloudStorageDirectory + '/' + Jupyter.notebook.notebook_name, + localDestinationPath: Jupyter.notebook.notebook_path + }] + }; + + const payload = { + ...basePayload, + method: 'POST', + body: JSON.stringify(entries) + }; + + await fetch(params.localizeUrl, payload); + + location.reload(true); + } + + init(); +}); diff --git a/terra-base/custom/extension_entry_jupyter.js b/terra-base/custom/extension_entry_jupyter.js new file mode 100644 index 00000000..0515b726 --- /dev/null +++ b/terra-base/custom/extension_entry_jupyter.js @@ -0,0 +1,7 @@ +define([ + 'base/js/events' +], function(events) { + require(['custom/google_sign_in']); + require(['custom/edit-mode']); + require(['custom/safe-mode']); +}); diff --git a/terra-base/custom/extension_entry_jupyterlab.js b/terra-base/custom/extension_entry_jupyterlab.js new file mode 100644 index 00000000..333ccad9 --- /dev/null +++ b/terra-base/custom/extension_entry_jupyterlab.js @@ -0,0 +1,7 @@ +module.exports = [{ + id: 'google_plugin_jupyterlab', + autoStart: true, + activate: function(app) { + require('/home/jupyter/.jupyter/custom/google_sign_in'); + } +}]; diff --git a/terra-base/custom/google_sign_in.js b/terra-base/custom/google_sign_in.js new file mode 100644 index 00000000..c63e58bf --- /dev/null +++ b/terra-base/custom/google_sign_in.js @@ -0,0 +1,174 @@ +/* + * This library is designed to run as a Jupyter/JupyterLab extension to refresh the user's + * Google credentials while using a notebook. This flow is described in more detail here: + * https://github.com/DataBiosphere/leonardo/wiki/Connecting-to-a-Leo-Notebook#token-refresh + * + * Note since this runs inside both Jupyter and JupyterLab, it should not use any + * libraries/functionality that exists in one but not the other. Examples: node, requireJS. + */ + + +// define default values for config parameters +const params = { + loginHint: '', + googleClientId: '', + googleProject: '', + clusterName: '' +}; + +// update params with any specified in the server's config file, +// or retrieve that info from the url if we cannot access it (as is the case in terminal view) +function updateParams() { + if (!Jupyter.notebook || !Jupyter.notebook.config || !Jupyter.notebook.config.data) { + console.warn('Unable to read notebook config. This is expected in terminal view, but not elsewhere. Attempting to read fallback config.'); + readFallbackConfig(); + } else { + readNotebookConfig(Jupyter.notebook.config.data); + } +} + +function readNotebookConfig(config) { + for (const key in params) { + if (config.hasOwnProperty(key)) { + params[key] = config[key]; + } + } +} + +// here we attempt to parse the url for the googleProject and the clusterName +function readFallbackConfig() { + const url = window.location.href; + const initialSearch = 'proxy/'; + + const projectSubstringStartLocation = url.search(initialSearch) + initialSearch.length; + const projectSubstring = url.substring(projectSubstringStartLocation, url.length); + const projectEndLocation = projectSubstring.search('/'); + const googleProject = projectSubstring.substring(0, projectEndLocation); + + // we add 1 for the slash between project and cluster + const clusterSubstring = projectSubstring.substring(projectEndLocation + 1, projectSubstring.length); + const clusterEndLocation = clusterSubstring.search('/'); + const clusterName = clusterSubstring.substring(0, clusterEndLocation); + + console.info(`Attempted to parse the url for a fallback configuration. + Found googleProject: '${googleProject}' and clusterName '${clusterName}'`); + + params.googleProject = googleProject; + params.clusterName = clusterName; + + fallbackReadNotebookConfig(googleProject, clusterName); +} + +function fallbackReadNotebookConfig(googleProject, clusterName) { + const url = `/proxy/${googleProject}/${clusterName}/jupyter/api/config/notebook`; + xhttpGet(url, (res) => { + readNotebookConfig(res); + }); +} + +function receive(event) { + if (event.data.type == 'bootstrap-auth.response') { + if (event.source !== window.opener) { + return; + } + params.googleClientId = event.data.body.googleClientId; + } else if (event.data.type == 'bootstrap-auth.request') { + if (event.origin !== window.origin) { + return; + } + if (!params.googleClientId) { + return; + } + event.source.postMessage({ + 'type': 'bootstrap-auth.response', + 'body': { + 'googleClientId': params.googleClientId + } + }, event.origin); + } +} + +// https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/response +function xhttpGet(url, callback) { + const xhr = new XMLHttpRequest(); + + xhr.onreadystatechange = function() { + if (xhr.readyState === 4 && xhr.status == 200) { + const jsonResp = JSON.parse(xhr.responseText); + callback(jsonResp); + } + }; + + xhr.open('GET', url, true); + xhr.send(''); +} + +function startTimer() { + loadGapi('auth2', function() { + function doAuth() { + if (params.googleClientId) { + gapi.auth2.authorize({ + 'client_id': params.googleClientId, + 'scope': 'openid profile email', + 'login_hint': params.loginHint, + 'prompt': 'none' + }, function(result) { + if (result.error) { + console.error('Error occurred authorizing with Google: ' + result.error); + return; + } + setCookie(result.access_token, result.expires_in); + }); + } + } + + // refresh token every 3 minutes + console.log('Starting token refresh timer'); + setInterval(doAuth, 180000); + }); + + function statusCheck() { + const url = `/proxy/${params.googleProject}/${params.clusterName}/jupyter/api/status`; + // logging the statusCheck can be noisy, and it will tell us in the console if it fails + xhttpGet(url, () => {}); + } + + setInterval(statusCheck, 60000); +} + +// Note: this should match +// https://github.com/DataBiosphere/leonardo/blob/develop/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/CookieHelper.scala +function setCookie(token, expiresIn) { + document.cookie = 'LeoToken=' + token + '; Max-Age=' + expiresIn + '; Path=/; Secure; SameSite=None'; +} + +function loadGapi(googleLib, continuation) { + console.log('Loading Google APIs'); + // Get the gapi script from Google. + const gapiScript = document.createElement('script'); + gapiScript.src = 'https://apis.google.com/js/api.js'; + gapiScript.type = 'text/javascript'; + gapiScript.async = true; + + // Load requested API scripts onto the page. + gapiScript.onload = function() { + console.log('Loading Google library \'' + googleLib + '\''); + gapi.load(googleLib, continuation); + }; + gapiScript.onerror = function() { + console.error('Unable to load Google APIs'); + }; + document.head.appendChild(gapiScript); +} + +function init() { + console.log('Starting google_sign_in extension'); + updateParams(); + startTimer(); + window.addEventListener('message', receive); + if (!params.googleClientId && window.opener) { + window.opener.postMessage({'type': 'bootstrap-auth.request'}, '*'); + } +} + +init(); diff --git a/terra-base/custom/jupyter_delocalize.py b/terra-base/custom/jupyter_delocalize.py new file mode 100644 index 00000000..e0e77bc9 --- /dev/null +++ b/terra-base/custom/jupyter_delocalize.py @@ -0,0 +1,148 @@ +from datetime import timedelta +import json +import os +import requests +import tornado +from notebook.services.contents.largefilemanager import LargeFileManager + +METADATA_TTL = timedelta(minutes=5) + +class WelderContentsManager(LargeFileManager): + """ + A contents manager which integrates with the Leo Welder service. + + Blocking Welder API calls are made before files are persisted. After a + successful call to Welder, files are persisted to the local Jupyter file + system as usual. + """ + + def __init__(self, *args, **kwargs): + # This log line shouldn't be necessary, but Jupyter's built-in logging is + # lacking and its configuration can be complex. Having this in the server + # logs is useful for confirming which ContentsManager is in use. + self.log.info('initializing WelderContentsManager') + self.welder_base_url = 'http://welder:8080' + super(WelderContentsManager, self).__init__(*args, **kwargs) + + def _extract_welder_error(self, resp): + try: + return json.dumps(resp.json()) + except: + return resp.reason or 'unknown Welder error' + + def _is_nonempty_dir(self, path): + os_path = self._get_os_path(path) + return os.path.isdir(os_path) and len(os.listdir(os_path)) > 0 + + + def _check_welder_edit_mode(self, path): + resp = requests.post(self.welder_base_url + '/objects/metadata', data=json.dumps({ + # Sometimes the Jupyter UI provided "path" contains a leading /, sometimes + # not; strip for Welder. + 'localPath': path.lstrip('/') + })) + if resp.status_code == 412: + return False + + if not resp.ok: + raise IOError("checkMetadata failed: '{}'".format(self._extract_welder_error(resp))) + + return resp.json().get("syncMode") == "EDIT" + + + def _post_welder(self, action, path): + # Ignore storage link failure, throw other errors. + resp = requests.post(self.welder_base_url + '/objects', data=json.dumps({ + 'action': action, + # Sometimes the Jupyter UI provided "path" contains a leading /, sometimes + # not; strip for Welder. + 'localPath': path.lstrip('/') + })) + if not resp.ok: + error_json = {} + try: + error_json = resp.json() + except: + pass + + # See https://github.com/DataBiosphere/welder/blob/cd39caba30989e9f2b1c76986abccf22d8e8a1c5/server/src/main/resources/api-docs.yaml#L197 + ignore_codes = set([ + 1, # Storage Link not found; expected for unmanaged files. + 2, 3 # Delocalize/delete safe mode file; expected in safe mode directories. + ]) + if resp.status_code == 412 and error_json.get('errorCode', -1) in ignore_codes: + return + + raise IOError("welder action '{}' failed: '{}'".format(action, self._extract_welder_error(resp))) + + def save(self, model, path=''): + # Don't intefere with intermediate chunks during multipart upload: + # https://jupyter-notebook.readthedocs.io/en/stable/extending/contents.html#chunked-saving + if model.get("chunk", -1) >= 0: + return super(WelderContentsManager, self).save(model, path) + + # Capture the pre-save file so we can revert if Welder fails. + orig_model = None + try: + orig_model = self.get(path) + except tornado.web.HTTPError as err: + if err.status_code != 404: + self.log.warn('failed to get file "{}", cannot revert: {}'.format(path, err.log_message)) + + # Welder reads the file from local disk, so we need to write the updated file + # before calling Welder. + # TODO(calbach): Consider changing the safeDelocalize API to support either + # direct passing of contents, or passing a file via a temporary transfer file. + ret = super(WelderContentsManager, self).save(model, path) + if not path or model['type'] == 'directory': + return ret + + try: + self._post_welder('safeDelocalize', path) + except IOError as werr: + self.log.warn("welder save failed, attempting to revert local file: " + str(werr)) + try: + if orig_model: + super(WelderContentsManager, self).save(orig_model, path) + else: + super(WelderContentsManager, self).delete_file(path) + except Exception as rerr: + self.log.error("failed to revert after Welder error, local disk is in an inconsistent state: " + str(rerr)) + raise werr + return ret + + def rename_file(self, old_path, new_path): + from_edit_mode = self._check_welder_edit_mode(old_path) + to_edit_mode = self._check_welder_edit_mode(new_path) + if not from_edit_mode and not to_edit_mode: + # If we're not touching any edit mode files, just do a normal move. + return super(WelderContentsManager, self).rename_file(old_path, new_path) + + if self._is_nonempty_dir(old_path): + raise NotImplementedError("renaming of non-empty edit mode directories is not supported") + + # These methods already properly handle edit mode semantics. + self.save(self.get(old_path), new_path) + try: + self.delete_file(old_path, from_edit_mode) + except Exception as err: + self.log.error("failed to delete old file during two-phase rename, " + + "attempting to revert save from the first phase: " + str(err)) + try: + self.delete_file(new_path, to_edit_mode) + except Exception as rerr: + self.log.error("failed to revert first phase of rename via delete, " + + "extra file will remain on disk and/or GCS: " + str(rerr)) + raise rerr + raise err + + def delete_file(self, path, edit_mode=None): + if edit_mode is None: + edit_mode = self._check_welder_edit_mode(path) + + if edit_mode: + if self._is_nonempty_dir(path): + raise NotImplementedError("deletion of non-empty edit mode directories is not supported") + self._post_welder('delete', path) + + super(WelderContentsManager, self).delete_file(path) diff --git a/terra-base/custom/jupyter_localize_extension.py b/terra-base/custom/jupyter_localize_extension.py new file mode 100644 index 00000000..bd7221ee --- /dev/null +++ b/terra-base/custom/jupyter_localize_extension.py @@ -0,0 +1,149 @@ +import distutils.util +import subprocess +import os +import sys +import tornado +from tornado import gen +from tornado.web import HTTPError +from notebook.base.handlers import IPythonHandler +from notebook.utils import url_path_join +from datauri import DataURI + +class LocalizeHandler(IPythonHandler): + def _sanitize(self, pathstr): + """Sanitizes paths. Handles local paths, gs: URIs, and data: URIs.""" + # return gs or data uris as is + if pathstr.startswith("gs:") or pathstr.startswith("data:"): + return pathstr + # expand user directories and make intermediate directories + else: + expanded = os.path.expanduser(pathstr) + try: + os.makedirs(os.path.dirname(expanded)) + except OSError: #thrown if dirs already exist + pass + return expanded + + def _localize_gcs_uri(self, locout, source, dest): + """Localizes an entry where either the source or destination is a gs: path. + Simply invokes gsutil in a subprocess.""" + + # Use a sequence of arguments with Shell=False. The subprocess module takes care + # of quoting/escaping arguments. See: + # https://docs.python.org/2/library/subprocess.html#subprocess.call + # https://docs.python.org/2/library/subprocess.html#frequently-used-arguments + cmd = ['gsutil', '-m', '-q', 'cp', '-R', '-c', '-e', source, dest] + locout.write(' '.join(cmd) + '\n') + result = subprocess.call(cmd, stderr=locout) + return result == 0 + + def check_gcs_object_status(self, locout, source): + if source.startswith("gs:"): + source_check = ['gsutil', '-m', '-q', 'ls', source] + locout.write(' '.join(source_check) + '\n') + source_status = subprocess.call(source_check, stderr=locout) + else: + source_status = 0 + return source_status == 0 + + def _localize_data_uri(self, locout, source, dest): + """Localizes an entry where the source is a data: URI""" + try: + uri = DataURI(source) + except ValueError: + locout.write('Could not parse "{}" as a data URI: {}\n'.format(source, str(e))) + return False + + try: + with open(dest, 'w+', buffering=1) as destout: + try: + uri_data = uri.data.decode() + except AttributeError: + uri_data = uri.data + destout.write(uri_data) + locout.write('{}: wrote {} bytes\n'.format(dest, len(uri_data))) + except IOError as e: + locout.write('{}: I/O error({0}): {1}\n'.format(dest, e.errno, e.strerror)) + return False + except: + locout.write('{}: unexpected error: {}\n'.format(sys.exc_info()[0])) + return False + + return True + + @gen.coroutine + def localize(self, pathdict): + """Treats the given dict as a string/string map and localizes each entry one by one. + Returns a list of any failed entries.""" + failures = [] + #This gets dropped inside the user's notebook working directory + with open("localization.log", 'a', buffering=1) as locout: + for key in pathdict: + #NOTE: keys are destinations, values are sources + source = self._sanitize(pathdict[key]) + dest = self._sanitize(key) + + if source.startswith('gs:') or dest.startswith('gs:'): + status = self.check_gcs_object_status(locout, source) + if status: + success = self._localize_gcs_uri(locout, source, dest) + else: + locout.write('Could not validate source or destination: {} -> {}.\n'.format(source, dest)) + success = False + elif source.startswith('data:'): + success = self._localize_data_uri(locout, source, dest) + else: + locout.write('Unhandled localization entry: {} -> {}. Required gs: or data: URIs.\n'.format(source, dest)) + success = False + + if not success: + failures.append((dest, source)) + + return failures + + def post(self): + try: + pathdict = tornado.escape.json_decode(self.request.body) + except ValueError: + raise HTTPError(400, "Body must be JSON object of type string/string") + + if type(pathdict) is not dict: + raise HTTPError(400, "Body must be JSON object of type string/string") + + if not all(map(lambda v: type(v) is str, pathdict.values())): + raise HTTPError(400, "Body must be JSON object of type string/string") + + try: + raw = self.get_query_argument('async', 'false') + isAsync = distutils.util.strtobool(raw) + except ValueError: + raise HTTPError(400, "Could not parse isAsync parameter as a boolean: '{}'".format(raw)) + + if isAsync: + #complete the request HERE, without waiting for the localize to run + self.set_status(200) + self.finish() + + #fire and forget the actual work -- it'll log to a file in the user's homedir + tornado.ioloop.IOLoop.current().spawn_callback(self.localize, pathdict) + + else: + #run localize synchronous to the HTTP request + #run_sync() doesn't take arguments, so we must wrap the call in a lambda. + failures = tornado.ioloop.IOLoop().run_sync(lambda: self.localize(pathdict)) + + #complete the request only after localize completes + if failures: + raise HTTPError(500, "Error occurred localizing the following {} entries: {}. See localization.log for details.".format( + len(failures), str(failures))) + else: + self.set_status(200) + self.finish() + +def load_jupyter_server_extension(nb_server_app): + """Entrypoint for the Jupyter extension.""" + web_app = nb_server_app.web_app + host_pattern = '.*$' + route_pattern = url_path_join(web_app.settings['base_url'], '/api/localize') + web_app.add_handlers(host_pattern, [(route_pattern, LocalizeHandler)]) + nb_server_app.log.info('initialized jupyter_localize_extension') diff --git a/terra-base/custom/package.json b/terra-base/custom/package.json new file mode 100644 index 00000000..1c1a6692 --- /dev/null +++ b/terra-base/custom/package.json @@ -0,0 +1,19 @@ +{ + "name": "jupyter-nbextension", + "version": "1.0.0", + "description": "Used exclusively for linting the nbextension files. See the README for instructions.", + "main": "edit-mode.js", + "directories": { + "test": "test" + }, + "dependencies": {}, + "devDependencies": { + "eslint": "^6.1.0", + "eslint-config-google": "^0.13.0" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC" +} diff --git a/terra-base/custom/safe-mode.js b/terra-base/custom/safe-mode.js new file mode 100644 index 00000000..6adda657 --- /dev/null +++ b/terra-base/custom/safe-mode.js @@ -0,0 +1,128 @@ +// Adapted from the All of Us Researcher Workbench "Playground Mode" +// https://github.com/all-of-us/workbench/blob/master/api/cluster-resources/playground-extension.js + +// In "Safe Mode", changes are not saved back to GCS. This extension makes +// minor UI tweaks to differentiate this mode from normal Jupyter usage, and +// also removes/hides controls relating to persistence. Technically +// this does not stop autosave from continuing to happen in the background, but +// the intended use of this plugin is in a separate space from normal operation +// which does not support localization features. + +// const namespace = require('base/js/namespace') + +define(() => { + // define default values for config parameters + const params = { + googleProject: '', + clusterName: '', + welderEnabled: 'false' + }; + + // update params with any specified in the server's config file + function updateParams() { + const config = Jupyter.notebook.config; + for (const key in params) { + if (config.data.hasOwnProperty(key)) { + params[key] = config.data[key]; + } + } + + // generate URLs based on params + const welderUrl = `/proxy/${params.googleProject}/${params.clusterName}/welder`; + params.checkMetaUrl = welderUrl + '/objects/metadata'; + } + + const headers = { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Headers': '*' + }; + + const basePayload = { + mode: 'no-cors', + headers: headers + }; + + function load() { + console.info('safe mode plugin initialized'); + + if (!Jupyter.notebook) { + return; // exit, they are in list view + } + + updateParams(); + + if (!(params.welderEnabled == 'true')) { + console.info('welder is not enabled'); + return; + } + + checkMetaLoop(); + } + + function checkMetaLoop() { + triggerUIToggle(); + + const interval = setInterval(() => { + triggerUIToggle(); + }, 60000); + + window.onbeforeunload(() => { + clearInterval(interval); + }); + } + + async function triggerUIToggle() { + checkMeta() + .then((res) => { + if (res.syncMode == 'EDIT') { + toggleUIControls(false); + } else { + // there is an icon in the jupyter UI that has a tool tip that says 'edit mode'. + // this shows up whenever a user types, so we change the tool-tip to avoid confusion + $('#modal_indicator').tooltip({'content': 'Adding code'}); + toggleUIControls(true); + } + }) + .catch((err) => { + console.error(err); + toggleUIControls(false); // we always assume safe mode if the check meta call fails + }); + } + + function checkMeta() { + const payload = { + ...basePayload, + body: JSON.stringify({localPath: Jupyter.notebook.notebook_path}), + method: 'POST' + }; + + return fetch(params.checkMetaUrl, payload) + .then((res) => { + if (!res.ok) { + throw Error('check metadata call failed due to status code'); + } + return res.json(); + }); + } + + function toggleUIControls(shouldHide) { + // these are the jquery selectors for the elements we will toggle + // "notbook" is an intentional typo to match the Jupyter UI HTML. + const selectorsToHide = ['#save-notbook', '#new_notebook', + '#open_notebook', '#copy_notebook', '#save_notebook_as', + '#save_checkpoint', '#restore_checkpoint', '.checkpoint_status', + '.autosave_status', '#notification_notebook', '#file_menu > li.divider:eq(0)', + '#file_menu > li.divider:eq(2)' + ]; + + selectorsToHide.forEach((selector) => { + if (shouldHide) { + $(selector).hide(); + } else { + $(selector).show(); + } + }); + } + + load(); +}); diff --git a/terra-base/custom/test/README.md b/terra-base/custom/test/README.md new file mode 100644 index 00000000..985b09cf --- /dev/null +++ b/terra-base/custom/test/README.md @@ -0,0 +1,16 @@ +## Python testing + +TODO: Integrate this into automated testing processes. + +- Initialize and activate a virtualenv +- Install requirements: + + ``` + jupyter-server$ pip3 install -r test-requirements.txt + ``` +- Run the tests: + + ``` + jupyter-server$ PYTHONPATH=$PYTHONPATH:. python3 -m nose + ``` + diff --git a/terra-base/custom/test/jupyter_delocalize_test.py b/terra-base/custom/test/jupyter_delocalize_test.py new file mode 100644 index 00000000..377a0274 --- /dev/null +++ b/terra-base/custom/test/jupyter_delocalize_test.py @@ -0,0 +1,448 @@ +import copy +import datetime +import json +import os +import requests_mock +import shutil +import tempfile +import unittest +from datetime import timedelta +from nbformat.v4 import new_notebook +from tornado.testing import AsyncTestCase, gen_test +from unittest.mock import patch + +# Import this first; see https://github.com/jupyter/notebook/issues/2798 +import notebook.transutils +import jupyter_delocalize + +class TestDelocalizingContentsManager(AsyncTestCase): + """DelocalizingContentsManager tests""" + + def setUp(self): + super(TestDelocalizingContentsManager, self).setUp() + self.orig_ttl = jupyter_delocalize.METADATA_TTL + jupyter_delocalize.METADATA_TTL = timedelta() + self.manager = jupyter_delocalize.DelocalizingContentsManager( + root_dir=tempfile.mkdtemp(), + delete_to_trash=False + ) + # Replaces gsutil with normal file commands. + self.manager.file_cmd = [] + self.manager.new(model={'type': 'directory'}, path='dir') + self.out_dir = tempfile.mkdtemp() + + def tearDown(self): + jupyter_delocalize.METADATA_TTL = self.orig_ttl + shutil.rmtree(self.manager.root_dir) + shutil.rmtree(self.out_dir) + super(TestDelocalizingContentsManager, self).tearDown() + + def _await_tornado(self): + # We spawn the delocalize processes in a Tornado callback, which executes + # asynchronously. + self.io_loop.add_callback(self.stop) + self.wait() + + def _save_new_notebook(self, path): + content = new_notebook() + self.manager.save({ + 'type': 'notebook', + 'content': content, + 'format': 'text' + }, path=path) + self._await_tornado() + return content.dict() + + def _save_delocalize_config(self, dir_path, config=None): + if not config: + config = { + 'destination': self.out_dir + } + self.manager.save({ + 'type': 'file', + 'content': json.dumps(config), + 'format': 'text' + }, path=dir_path + '/.delocalize.json') + self._await_tornado() + + def _rename_file(self, from_path, to_path): + self.manager.rename_file(from_path, to_path) + self._await_tornado() + + def _delete_file(self, path): + self.manager.delete_file(path) + self._await_tornado() + + def test_save_normal(self): + want = self._save_new_notebook('dir/foo.ipynb') + self.assertEqual(os.listdir(self.out_dir), []) + with open(self.manager.root_dir + '/dir/foo.ipynb', 'r') as got: + self.assertEqual(json.load(got), want) + + def test_save_delocalize(self): + self._save_delocalize_config('dir') + want = self._save_new_notebook('dir/foo.ipynb') + + with open(self.out_dir + '/foo.ipynb', 'r') as got: + self.assertEqual(json.load(got), want) + with open(self.manager.root_dir + '/dir/foo.ipynb', 'r') as got: + self.assertEqual(json.load(got), want) + + def test_save_delocalize_other_dirs(self): + self.manager.new(model={'type': 'directory'}, path='dirA') + self.manager.new(model={'type': 'directory'}, path='dir/dirB') + self._save_delocalize_config('dir') + + self._save_new_notebook('foo.ipynb') + self._save_new_notebook('dirA/fizz.ipynb') + self._save_new_notebook('dir/dirB/bar.ipynb') + self.assertEqual(os.listdir(self.out_dir), []) + + def test_save_delocalize_with_pattern(self): + self._save_delocalize_config('.', config={ + 'destination': self.out_dir, + 'pattern': '.*\.ipynb$' + }) + + self._save_new_notebook('foo.ipynb') + self._save_new_notebook('falco.jpg') + self._save_new_notebook('lombardi.pdf') + self.assertEqual(os.listdir(self.out_dir), ['foo.ipynb']) + + def test_rename_normal(self): + want = self._save_new_notebook('dir/foo.ipynb') + self._rename_file('dir/foo.ipynb', 'dir/bar.ipynb') + + self.assertFalse(os.path.isfile(self.manager.root_dir + '/dir/foo.ipynb')) + with open(self.manager.root_dir + '/dir/bar.ipynb', 'r') as got: + self.assertEqual(json.load(got), want) + + def test_rename_delocalize(self): + self._save_delocalize_config('dir') + want = self._save_new_notebook('dir/foo.ipynb') + self._rename_file('dir/foo.ipynb', 'dir/bar.ipynb') + + self.assertFalse(os.path.isfile(self.out_dir + '/foo.ipynb')) + self.assertFalse(os.path.isfile(self.manager.root_dir + '/foo.ipynb')) + with open(self.out_dir + '/bar.ipynb', 'r') as got: + self.assertEqual(json.load(got), want) + with open(self.manager.root_dir + '/dir/bar.ipynb', 'r') as got: + self.assertEqual(json.load(got), want) + + def test_rename_delocalize_with_pattern(self): + self._save_delocalize_config('dir', config={ + 'destination': self.out_dir, + 'pattern': 'foo' + }) + want = self._save_new_notebook('dir/foo.ipynb') + self._rename_file('dir/foo.ipynb', 'dir/bar.ipynb') + + # The delocalization behavior for foo.ipynb in this case is unspecified; + # currently it won't delete the file, but would be better if it did. + self.assertFalse(os.path.isfile(self.out_dir + '/bar.ipynb')) + self.assertFalse(os.path.isfile(self.manager.root_dir + '/foo.ipynb')) + with open(self.manager.root_dir + '/dir/bar.ipynb', 'r') as got: + self.assertEqual(json.load(got), want) + + def test_delete_normal(self): + self._save_new_notebook('foo.ipynb') + self._delete_file('foo.ipynb') + + self.assertFalse(os.path.isfile(self.manager.root_dir + '/foo.ipynb')) + + def test_delete_delocalize(self): + self._save_delocalize_config('.') + self._save_new_notebook('foo.ipynb') + self._delete_file('foo.ipynb') + + self.assertFalse(os.path.isfile(self.out_dir + '/foo.ipynb')) + self.assertFalse(os.path.isfile(self.manager.root_dir + '/foo.ipynb')) + + def test_delete_delocalize_with_pattern(self): + self._save_delocalize_config('.') + self._save_new_notebook('foo.ipynb') + + self._save_delocalize_config('.', config={ + 'destination': self.out_dir, + 'pattern': 'doesnt match' + }) + self._delete_file('foo.ipynb') + + self.assertTrue(os.path.isfile(self.out_dir + '/foo.ipynb')) + self.assertFalse(os.path.isfile(self.manager.root_dir + '/foo.ipynb')) + + def test_metadata_cache(self): + jupyter_delocalize.METADATA_TTL = timedelta(minutes=5) + # Stub out and advance time manually. + now = datetime.datetime(2018, 3, 20) + self.manager._now = lambda: now + self._save_new_notebook('foo.ipynb') + self.assertEqual(os.listdir(self.out_dir), []) + + # A "not-found" should be cached, only 1 minute passed + now += timedelta(minutes=1) + self._save_delocalize_config('.') + self._save_new_notebook('foo.ipynb') + self.assertEqual(os.listdir(self.out_dir), []) + + # Cache TTL expired, will check again for delocalization config. + now += timedelta(minutes=20) + self._save_new_notebook('foo.ipynb') + self.assertEqual(os.listdir(self.out_dir), ['foo.ipynb']) + +class TestWelderContentsManager(AsyncTestCase): + """WelderContentsManager tests""" + + def setUp(self): + super(TestWelderContentsManager, self).setUp() + self.manager = jupyter_delocalize.WelderContentsManager( + root_dir=tempfile.mkdtemp(), + delete_to_trash=False + ) + self.manager.new(model={'type': 'directory'}, path='dir') + + def tearDown(self): + shutil.rmtree(self.manager.root_dir) + super(TestWelderContentsManager, self).tearDown() + + def _save_new_notebook(self, path): + content = new_notebook() + self.manager.save({ + 'type': 'notebook', + 'content': content, + 'format': 'text' + }, path=path) + return content.dict() + + def _save_new_dir(self, path): + self.manager.save({ + 'type': 'directory', + 'content': '[]', + 'format': 'json' + }, path=path) + + @requests_mock.mock() + def test_save(self, mock_request): + mock_request.post(self.manager.welder_base_url + '/objects') + want = self._save_new_notebook('dir/foo.ipynb') + with open(self.manager.root_dir + '/dir/foo.ipynb', 'r') as got: + self.assertEqual(json.load(got), want) + + @requests_mock.mock() + def test_save_dir(self, mock_request): + self._save_new_dir('dir/foo') + self.assertTrue(os.path.isdir(self.manager.root_dir + '/dir/foo')) + + @requests_mock.mock() + def test_save_scratch_file(self, mock_request): + mock_request.post(self.manager.welder_base_url + '/objects', status_code=412, json={ + 'errorCode': 1 + }) + want = self._save_new_notebook('dir/foo.ipynb') + with open(self.manager.root_dir + '/dir/foo.ipynb', 'r') as got: + self.assertEqual(json.load(got), want) + + @requests_mock.mock() + def test_save_new_file_reverts_on_fail(self, mock_request): + mock_request.post(self.manager.welder_base_url + '/objects', status_code=412, json={ + 'errorCode': 4 + }) + try: + self._save_new_notebook('dir/foo.ipynb') + self.fail('expected error on save') + except IOError: + pass + self.assertFalse(os.path.isfile(self.manager.root_dir + '/dir/foo.ipynb')) + + @requests_mock.mock() + def test_save_reverts_on_fail(self, mock_request): + mock_request.post(self.manager.welder_base_url + '/objects') + content = new_notebook() + self.manager.save({ + 'type': 'notebook', + 'content': content, + 'format': 'text' + }, path='dir/foo.ipynb') + + mock_request.post(self.manager.welder_base_url + '/objects', status_code=412) + updated_content = copy.deepcopy(content) + updated_content['cells'] = [{ + 'cell_type': 'markdown', + 'metadata': {}, + 'source': ['XD'], + }] + try: + self.manager.save({ + 'type': 'notebook', + 'content': updated_content, + 'format': 'text' + }, path='dir/foo.ipynb') + self.fail('expected error on save') + except IOError: + pass + with open(self.manager.root_dir + '/dir/foo.ipynb', 'r') as got: + self.assertEqual(json.load(got)['cells'], content.dict()['cells']) + + def mock_edit_mode_meta(self, mock_request, edit_mode=True): + if not edit_mode: + mock_request.post(self.manager.welder_base_url + '/objects/metadata', status_code=412) + else: + mock_request.post(self.manager.welder_base_url + '/objects/metadata', json={ + 'syncMode': 'EDIT' + }) + + @requests_mock.mock() + def test_delete(self, mock_request): + self.mock_edit_mode_meta(mock_request) + mock_request.post(self.manager.welder_base_url + '/objects') + self._save_new_notebook('dir/foo.ipynb') + + self.manager.delete_file('dir/foo.ipynb') + self.assertFalse(os.path.isfile(self.manager.root_dir + '/dir/foo.ipynb')) + + @requests_mock.mock() + def test_delete_empty_dir(self, mock_request): + self.mock_edit_mode_meta(mock_request) + mock_request.post(self.manager.welder_base_url + '/objects') + self._save_new_dir('dir/foo') + + self.manager.delete_file('dir/foo') + self.assertFalse(os.path.isdir(self.manager.root_dir + '/dir/foo')) + + @requests_mock.mock() + def test_delete_dir_with_notebook(self, mock_request): + self.mock_edit_mode_meta(mock_request) + mock_request.post(self.manager.welder_base_url + '/objects') + self._save_new_dir('dir/foo') + self._save_new_notebook('dir/foo/nb.ipynb') + + try: + self.manager.delete_file('dir/foo') + self.fail('expected error on non-empty edit mode directory deletion') + except NotImplementedError: + pass + self.assertTrue(os.path.isdir(self.manager.root_dir + '/dir/foo')) + + @requests_mock.mock() + def test_delete_scratch_file(self, mock_request): + self.mock_edit_mode_meta(mock_request, edit_mode=False) + mock_request.post(self.manager.welder_base_url + '/objects') + self._save_new_notebook('dir/foo.ipynb') + + self.manager.delete_file('dir/foo.ipynb') + self.assertFalse(os.path.isfile(self.manager.root_dir + '/dir/foo.ipynb')) + + @requests_mock.mock() + def test_delete_scratch_dir(self, mock_request): + self.mock_edit_mode_meta(mock_request, edit_mode=False) + self._save_new_dir('dir/foo') + + self.manager.delete_file('dir/foo') + self.assertFalse(os.path.isdir(self.manager.root_dir + '/dir/foo')) + + @requests_mock.mock() + def test_delete_local_file_survives_welder_error(self, mock_request): + self.mock_edit_mode_meta(mock_request) + mock_request.post(self.manager.welder_base_url + '/objects') + self._save_new_notebook('dir/foo.ipynb') + + mock_request.post(self.manager.welder_base_url + '/objects', status_code=412, json={ + 'errorCode': 4 + }) + try: + self.manager.delete_file('dir/foo.ipynb') + self.fail('expected error on delete') + except IOError: + pass + self.assertTrue(os.path.isfile(self.manager.root_dir + '/dir/foo.ipynb')) + + @requests_mock.mock() + def test_rename(self, mock_request): + post_mock = mock_request.post(self.manager.welder_base_url + '/objects') + self.mock_edit_mode_meta(mock_request) + want = self._save_new_notebook('dir/foo.ipynb') + + # Creating the initial notebook above results in a Welder post. + posts_before_rename = post_mock.call_count + self.manager.rename('dir/foo.ipynb', 'dir/bar.ipynb') + self.assertFalse(os.path.isfile(self.manager.root_dir + '/dir/foo.ipynb')) + with open(self.manager.root_dir + '/dir/bar.ipynb', 'r') as got: + self.assertEqual(json.load(got), want) + + self.assertEqual(post_mock.call_count - posts_before_rename, 2) + + @requests_mock.mock() + def test_rename_empty_dir(self, mock_request): + self.mock_edit_mode_meta(mock_request) + mock_request.post(self.manager.welder_base_url + '/objects') + self._save_new_dir('dir/foo') + + self.manager.rename('dir/foo', 'dir/bar') + self.assertFalse(os.path.isdir(self.manager.root_dir + '/dir/foo')) + self.assertTrue(os.path.isdir(self.manager.root_dir + '/dir/bar')) + + @requests_mock.mock() + def test_rename_dir_with_notebook(self, mock_request): + post_mock = mock_request.post(self.manager.welder_base_url + '/objects') + self.mock_edit_mode_meta(mock_request) + self._save_new_dir('dir/foo') + self._save_new_notebook('dir/foo/nb.ipynb') + + try: + self.manager.rename('dir/foo', 'dir/bar') + self.fail('expected error on non-empty edit mode rename') + except: + pass + self.assertTrue(os.path.isdir(self.manager.root_dir + '/dir/foo')) + self.assertFalse(os.path.isdir(self.manager.root_dir + '/dir/bar')) + + @requests_mock.mock() + def test_rename_scratch_file(self, mock_request): + post_mock = mock_request.post(self.manager.welder_base_url + '/objects') + self.mock_edit_mode_meta(mock_request, edit_mode=False) + want = self._save_new_notebook('dir/foo.ipynb') + + # Creating the initial notebook above results in a Welder post. + posts_before_rename = post_mock.call_count + self.manager.rename('dir/foo.ipynb', 'dir/bar.ipynb') + self.assertFalse(os.path.isfile(self.manager.root_dir + '/dir/foo.ipynb')) + with open(self.manager.root_dir + '/dir/bar.ipynb', 'r') as got: + self.assertEqual(json.load(got), want) + + self.assertEqual(post_mock.call_count - posts_before_rename, 0) + + @requests_mock.mock() + def test_rename_scratch_dir(self, mock_request): + self.mock_edit_mode_meta(mock_request, edit_mode=False) + self._save_new_dir('dir/foo') + + self.manager.rename('dir/foo', 'dir/bar') + self.assertFalse(os.path.isdir(self.manager.root_dir + '/dir/foo')) + self.assertTrue(os.path.isdir(self.manager.root_dir + '/dir/bar')) + + def _delete_req_matcher(self, path): + def m(req): + return req.json()['action'] == 'delete' and req.json()['localPath'] == path + return m + + @requests_mock.mock() + def test_rename_cleanup_on_delete_fail(self, mock_request): + mock_request.post(self.manager.welder_base_url + '/objects') + mock_request.post(self.manager.welder_base_url + '/objects', additional_matcher=self._delete_req_matcher('dir/foo.ipynb'), status_code=500) + mock_request.post(self.manager.welder_base_url + '/objects', additional_matcher=self._delete_req_matcher('dir/bar.ipynb')) + self.mock_edit_mode_meta(mock_request) + want = self._save_new_notebook('dir/foo.ipynb') + + # Creating the initial notebook above results in a Welder post. + try: + self.manager.rename('dir/foo.ipynb', 'dir/bar.ipynb') + self.fail('expected rename exception') + except IOError: + pass + + self.assertFalse(os.path.isfile(self.manager.root_dir + '/dir/bar.ipynb')) + with open(self.manager.root_dir + '/dir/foo.ipynb', 'r') as got: + self.assertEqual(json.load(got), want) + +if __name__ == '__main__': + unittest.main() diff --git a/terra-base/custom/test/test-requirements.txt b/terra-base/custom/test/test-requirements.txt new file mode 100644 index 00000000..e7c6ae7e --- /dev/null +++ b/terra-base/custom/test/test-requirements.txt @@ -0,0 +1,3 @@ +jupyter==1.0.0 +nose==1.3.7 +requests-mock==1.6.0 diff --git a/terra-base/jupyter_notebook_config.py b/terra-base/jupyter_notebook_config.py new file mode 100755 index 00000000..9827df65 --- /dev/null +++ b/terra-base/jupyter_notebook_config.py @@ -0,0 +1,47 @@ +# adapted from https://github.com/jupyter/docker-stacks/blob/master/base-notebook/jupyter_notebook_config.py +# Note: this file also lives in the Leonardo repo here: +# https://github.com/DataBiosphere/leonardo/blob/develop/http/src/main/resources/jupyter/jupyter_notebook_config.py +# If you change this please keep the other version consistent as well. + +import os + +c = get_config() + +if os.environ.get('JUPYTER_DEBUG_LOGGING') == 'true': + c.Application.log_level = 'DEBUG' + +c.NotebookApp.ip = '0.0.0.0' +c.NotebookApp.port = 8000 +c.NotebookApp.open_browser = False + +c.NotebookApp.token = '' +c.NotebookApp.disable_check_xsrf = True #see https://github.com/nteract/hydrogen/issues/922 +c.NotebookApp.allow_origin = '*' + +c.NotebookApp.terminado_settings={'shell_command': ['bash']} + +if 'GOOGLE_PROJECT' in os.environ and 'CLUSTER_NAME' in os.environ: + fragment = '/' + os.environ['GOOGLE_PROJECT'] + '/' + os.environ['CLUSTER_NAME'] + '/' +else: + fragment = '/' + +c.NotebookApp.base_url = '/notebooks' + fragment + +# This is also specified in run-jupyter.sh +c.NotebookApp.nbserver_extensions = { + 'jupyter_localize_extension': True +} + +c.NotebookApp.kernel_manager_class = 'notebook.services.kernels.kernelmanager.AsyncMappingKernelManager' + +mgr_class = 'DelocalizingContentsManager' +if os.environ.get('WELDER_ENABLED') == 'true': + mgr_class = 'WelderContentsManager' +c.NotebookApp.contents_manager_class = 'jupyter_delocalize.' + mgr_class + +c.NotebookApp.tornado_settings = { + 'static_url_prefix':'/notebooks' + fragment + 'static/' +} + +# so that the default kernel can be overridden by the conda environment +c.KernelSpecManager.ensure_native_kernel = False diff --git a/terra-base/pyproject.toml b/terra-base/pyproject.toml new file mode 100644 index 00000000..1fb3d1d3 --- /dev/null +++ b/terra-base/pyproject.toml @@ -0,0 +1,29 @@ +[project] +name = "terra-base" +version = "1.0.0" +description = "Minimal Jupyter environment for Terra notebooks" +readme = "README.md" +requires-python = ">=3.10, <3.11" +dependencies = [ + "ipykernel>=6.29.5", + "jupyter-contrib-nbextensions==0.7.0", + "jupyter-core==5.3.1", + "jupyter-server==1.24.0", + "jupyter-server-proxy==4.0.0", + "jupyterlab==3.4.8", + "jupyterlab-server==2.23.0", + "nbclassic>=1.3.1", + "nbconvert>=7.16.6", + "nbstripout>=0.8.1", + "notebook==6.5.4", + "python-datauri>=3.0.2", + "requests>=2.29.0", + "tornado>=6.5.1", + "traitlets==5.9.0", +] + +build-constraint-dependencies =[ + "setuptools>=70.3.0", + "wheel>=0.45.1" +] + diff --git a/terra-base/scripts/extension/install_jupyter_contrib_nbextensions.sh b/terra-base/scripts/extension/install_jupyter_contrib_nbextensions.sh new file mode 100644 index 00000000..cab48dd2 --- /dev/null +++ b/terra-base/scripts/extension/install_jupyter_contrib_nbextensions.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -e +# installs jupyter_contrib_nbextensions https://github.com/ipython-contrib/jupyter_contrib_nbextensions +# also installs the jupyter_nbextensions_configurator https://github.com/Jupyter-contrib/jupyter_nbextensions_configurator +sudo -E -u jupyter /etc/jupyter/bin/jupyter nbextensions_configurator enable --sys-prefix +sudo -E -u jupyter /etc/jupyter/bin/jupyter contrib nbextension install --sys-prefix +sudo -E -u jupyter /etc/jupyter/bin/jupyter nbextension enable toc2/main --sys-prefix +sudo -E -u jupyter /etc/jupyter/bin/jupyter nbextension enable codefolding/main --sys-prefix +sudo -E -u jupyter /etc/jupyter/bin/jupyter nbextension enable collapsible_headings/main --sys-prefix + diff --git a/terra-base/scripts/extension/jupyter_install_combined_extension.sh b/terra-base/scripts/extension/jupyter_install_combined_extension.sh new file mode 100644 index 00000000..dd6d6bea --- /dev/null +++ b/terra-base/scripts/extension/jupyter_install_combined_extension.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +set -e + +if [ -n "$1" ]; then + JUPYTER_EXTENSION=$1 + JUPYTER_EXTENSION_NAME=`basename ${JUPYTER_EXTENSION%%.*}` + mkdir ${JUPYTER_HOME}/${JUPYTER_EXTENSION_NAME} + tar -xzf ${JUPYTER_EXTENSION} -C${JUPYTER_HOME}/${JUPYTER_EXTENSION_NAME} + pip3 install -e ${JUPYTER_HOME}/${JUPYTER_EXTENSION_NAME} + sudo -E -u jupyter /etc/jupyter/bin/jupyter serverextension enable --py ${JUPYTER_EXTENSION_NAME} --sys-prefix + sudo -E -u jupyter /etc/jupyter/bin/jupyter nbextension install --py ${JUPYTER_EXTENSION_NAME} --sys-prefix + sudo -E -u jupyter /etc/jupyter/bin/jupyter nbextension enable --py ${JUPYTER_EXTENSION_NAME} --sys-prefix +fi diff --git a/terra-base/scripts/extension/jupyter_install_lab_extension.sh b/terra-base/scripts/extension/jupyter_install_lab_extension.sh new file mode 100644 index 00000000..0c3c4eaf --- /dev/null +++ b/terra-base/scripts/extension/jupyter_install_lab_extension.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +set -e + + +if [ -n "$1" ]; then + JUPYTER_EXTENSION=$1 + if [[ ${JUPYTER_EXTENSION} == *'.js' ]]; then + # use jupyterlab extension template to create an extension using the specified JS file + # see https://github.com/jupyterlab/extension-cookiecutter-js + JUPYTER_EXTENSION_NAME=`basename ${JUPYTER_EXTENSION%%.*}` + cookiecutter --no-input https://github.com/jupyterlab/extension-cookiecutter-js extension_name=${JUPYTER_EXTENSION_NAME} + cp -f ${JUPYTER_EXTENSION} ${JUPYTER_EXTENSION_NAME}/lib/plugin.js + cd ${JUPYTER_EXTENSION_NAME} + jlpm + jupyter labextension install . + elif [[ ${JUPYTER_EXTENSION} == *'.ts' ]]; then + # same as above but in typescript, see https://github.com/jupyterlab/extension-cookiecutter-ts + JUPYTER_EXTENSION_NAME=`basename ${JUPYTER_EXTENSION%%.*}` + cookiecutter --no-input https://github.com/jupyterlab/extension-cookiecutter-ts extension_name=${JUPYTER_EXTENSION_NAME} + cp -f ${JUPYTER_EXTENSION} ${JUPYTER_EXTENSION_NAME}/src/index.ts + cd ${JUPYTER_EXTENSION_NAME} + jlpm + jupyter labextension install . + else + jupyter labextension install $JUPYTER_EXTENSION + fi +fi + diff --git a/terra-base/scripts/extension/jupyter_install_notebook_extension.sh b/terra-base/scripts/extension/jupyter_install_notebook_extension.sh new file mode 100644 index 00000000..6b5cdd25 --- /dev/null +++ b/terra-base/scripts/extension/jupyter_install_notebook_extension.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -e + +if [ -n "$1" ]; then + JUPYTER_EXTENSION=$1 + JUPYTER_EXTENSION_NAME=`basename ${JUPYTER_EXTENSION%%.*}` + mkdir -p ${JUPYTER_HOME}/${JUPYTER_EXTENSION_NAME} + if [[ ${JUPYTER_EXTENSION} == *'.tar.gz' ]]; then + tar -xzf ${JUPYTER_EXTENSION} -C${JUPYTER_HOME}/${JUPYTER_EXTENSION_NAME} + else + mv ${JUPYTER_EXTENSION} ${JUPYTER_HOME}/${JUPYTER_EXTENSION_NAME}/main.js + fi + sudo -E -u jupyter /etc/jupyter/bin/jupyter nbextension install ${JUPYTER_HOME}/${JUPYTER_EXTENSION_NAME}/ --sys-prefix + sudo -E -u jupyter /etc/jupyter/bin/jupyter nbextension enable ${JUPYTER_EXTENSION_NAME}/main --sys-prefix +fi diff --git a/terra-base/scripts/extension/jupyter_install_server_extension.sh b/terra-base/scripts/extension/jupyter_install_server_extension.sh new file mode 100644 index 00000000..8a4f0600 --- /dev/null +++ b/terra-base/scripts/extension/jupyter_install_server_extension.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +set -e + +if [ -n "$1" ]; then + JUPYTER_EXTENSION=$1 + JUPYTER_EXTENSION_NAME=`basename ${JUPYTER_EXTENSION%%.*}` + mkdir ${JUPYTER_HOME}/${JUPYTER_EXTENSION_NAME} + tar -xzf ${JUPYTER_EXTENSION} -C${JUPYTER_HOME}/${JUPYTER_EXTENSION_NAME} + pip3 install -e ${JUPYTER_HOME}/${JUPYTER_EXTENSION_NAME} + sudo -E -u jupyter /etc/jupyter/bin/jupyter serverextension enable --py ${JUPYTER_EXTENSION_NAME} --sys-prefix +fi diff --git a/terra-base/scripts/extension/jupyter_pip_install_combined_extension.sh b/terra-base/scripts/extension/jupyter_pip_install_combined_extension.sh new file mode 100644 index 00000000..b981a925 --- /dev/null +++ b/terra-base/scripts/extension/jupyter_pip_install_combined_extension.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +set -e + +if [ -n "$1" ]; then + JUPYTER_EXTENSION=$1 + uv pip install ${JUPYTER_EXTENSION} + sudo -E -u jupyter jupyter serverextension enable --py ${JUPYTER_EXTENSION} --sys-prefix + sudo -E -u jupyter jupyter nbextension install --py ${JUPYTER_EXTENSION} --sys-prefix + sudo -E -u jupyter jupyter nbextension enable --py ${JUPYTER_EXTENSION} --sys-prefix +fi diff --git a/terra-base/scripts/extension/jupyter_pip_install_notebook_extension.sh b/terra-base/scripts/extension/jupyter_pip_install_notebook_extension.sh new file mode 100644 index 00000000..96888a94 --- /dev/null +++ b/terra-base/scripts/extension/jupyter_pip_install_notebook_extension.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -e + +if [ -n "$1" ]; then + JUPYTER_EXTENSION=$1 + uv pip install ${JUPYTER_EXTENSION} + sudo -E -u jupyter /etc/jupyter/bin/jupyter nbextension install --py ${JUPYTER_EXTENSION} --sys-prefixr + sudo -E -u jupyter /etc/jupyter/bin/jupyter nbextension enable --py ${JUPYTER_EXTENSION} --sys-prefix +fi diff --git a/terra-base/scripts/extension/jupyter_pip_install_server_extension.sh b/terra-base/scripts/extension/jupyter_pip_install_server_extension.sh new file mode 100644 index 00000000..3a87bcc7 --- /dev/null +++ b/terra-base/scripts/extension/jupyter_pip_install_server_extension.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +set -e + +if [ -n "$1" ]; then + JUPYTER_EXTENSION=$1 + uv pip install ${JUPYTER_EXTENSION} + sudo -E -u jupyter /etc/jupyter/bin/jupyter serverextension enable --py ${JUPYTER_EXTENSION} --sys-prefix +fi diff --git a/terra-base/scripts/kernel/kernel_bootstrap.sh b/terra-base/scripts/kernel/kernel_bootstrap.sh new file mode 100644 index 00000000..d4811a94 --- /dev/null +++ b/terra-base/scripts/kernel/kernel_bootstrap.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +# This script runs at kernel startup time and sets environment variables for the +# workspace name and workspace bucket. +# +# Note: this script is highly dependent on a convention used by Terra and AllOfUs +# applications to place notebooks in the following directory structure: +# +# /home/jupyter///notebook.ipynb +# +# It exploits the fact that the CWD of a launching notebook is named after the workspace. +# If notebooks are ever launched from other directories, this script will break. + +PWD="$(pwd)" +# The workspace name is simply the CWD of the running notebook. +export WORKSPACE_NAME=`basename "$(dirname "$PWD")"` + +# Parse the .delocalize.json file (if it exists) in the workspace directory to obtain the workspace bucket. +DELOCALIZE_FILE="$PWD/.delocalize.json" +if [ -f "$DELOCALIZE_FILE" ]; then + export WORKSPACE_BUCKET="$(dirname "$(cat "$DELOCALIZE_FILE" | jq -r '.destination')")" +fi + +exec "$@" \ No newline at end of file diff --git a/terra-base/scripts/kernel/kernelspec.sh b/terra-base/scripts/kernel/kernelspec.sh new file mode 100644 index 00000000..b3816349 --- /dev/null +++ b/terra-base/scripts/kernel/kernelspec.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +TMP_KERNELSPEC_DIR=$1 +KERNELSPEC_HOME=$2 + +mkdir ${KERNELSPEC_HOME}/r + +# Replace the contents of the Python kernel scripts +sed -e 's/${PY_VERSION}/3/g' -e 's|${JUPYTER_HOME}|'${JUPYTER_HOME}'|g' ${TMP_KERNELSPEC_DIR}/python_kernelspec.tmpl > ${KERNELSPEC_HOME}/python3/kernel.json +# Enable R kernel +sed 's|${JUPYTER_HOME}|'${JUPYTER_HOME}'|g' ${TMP_KERNELSPEC_DIR}/r_kernelspec.tmpl > ${KERNELSPEC_HOME}/r/kernel.json \ No newline at end of file diff --git a/terra-base/scripts/kernel/python_kernelspec.tmpl b/terra-base/scripts/kernel/python_kernelspec.tmpl new file mode 100644 index 00000000..a2b0b66a --- /dev/null +++ b/terra-base/scripts/kernel/python_kernelspec.tmpl @@ -0,0 +1,12 @@ +{ + "language": "python", + "display_name": "Python ${PY_VERSION}", + "argv": [ + "${JUPYTER_HOME}/scripts/kernel/kernel_bootstrap.sh", + "python${PY_VERSION}", + "-m", + "ipykernel_launcher", + "-f", + "{connection_file}" + ] +} diff --git a/terra-base/scripts/kernel/r_kernelspec.tmpl b/terra-base/scripts/kernel/r_kernelspec.tmpl new file mode 100644 index 00000000..e88b8c94 --- /dev/null +++ b/terra-base/scripts/kernel/r_kernelspec.tmpl @@ -0,0 +1,13 @@ +{ + "language": "R", + "display_name": "R", + "argv": [ + "${JUPYTER_HOME}/scripts/kernel/kernel_bootstrap.sh", + "/usr/lib/R/bin/R", + "--slave", + "-e", + "IRkernel::main()", + "--args", + "{connection_file}" + ] +} \ No newline at end of file diff --git a/terra-base/scripts/run-jupyter.sh b/terra-base/scripts/run-jupyter.sh new file mode 100644 index 00000000..f1f40aaa --- /dev/null +++ b/terra-base/scripts/run-jupyter.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +set -e + +# Change the default umask to support R/W access to the shared volume with welder +umask 002 + +# TODO: change default to ${HOME}/notebooks once welder is rolled out to all clusters +NOTEBOOKS_DIR=${1:-${HOME}} + +# Forces python 3.10 +JUPYTER_BASE="/etc/jupyter/bin/python3.10 /etc/jupyter/bin/jupyter-notebook" +JUPYTER_CMD="$JUPYTER_BASE --NotebookApp.nbserver_extensions=\"{'jupyter_localize_extension':True}\" &> ${NOTEBOOKS_DIR}/jupyter.log" + +echo $JUPYTER_CMD + +eval $JUPYTER_CMD diff --git a/terra-base/uv.lock b/terra-base/uv.lock new file mode 100644 index 00000000..6bc3b0f3 --- /dev/null +++ b/terra-base/uv.lock @@ -0,0 +1,1427 @@ +version = 1 +revision = 2 +requires-python = "==3.10.*" + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.12.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "async-timeout" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/e7/d92a237d8802ca88483906c388f7c201bbe96cd80a165ffd0ac2f6a8d59f/aiohttp-3.12.15.tar.gz", hash = "sha256:4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2", size = 7823716, upload-time = "2025-07-29T05:52:32.215Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/dc/ef9394bde9080128ad401ac7ede185267ed637df03b51f05d14d1c99ad67/aiohttp-3.12.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b6fc902bff74d9b1879ad55f5404153e2b33a82e72a95c89cec5eb6cc9e92fbc", size = 703921, upload-time = "2025-07-29T05:49:43.584Z" }, + { url = "https://files.pythonhosted.org/packages/8f/42/63fccfc3a7ed97eb6e1a71722396f409c46b60a0552d8a56d7aad74e0df5/aiohttp-3.12.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:098e92835b8119b54c693f2f88a1dec690e20798ca5f5fe5f0520245253ee0af", size = 480288, upload-time = "2025-07-29T05:49:47.851Z" }, + { url = "https://files.pythonhosted.org/packages/9c/a2/7b8a020549f66ea2a68129db6960a762d2393248f1994499f8ba9728bbed/aiohttp-3.12.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:40b3fee496a47c3b4a39a731954c06f0bd9bd3e8258c059a4beb76ac23f8e421", size = 468063, upload-time = "2025-07-29T05:49:49.789Z" }, + { url = "https://files.pythonhosted.org/packages/8f/f5/d11e088da9176e2ad8220338ae0000ed5429a15f3c9dfd983f39105399cd/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ce13fcfb0bb2f259fb42106cdc63fa5515fb85b7e87177267d89a771a660b79", size = 1650122, upload-time = "2025-07-29T05:49:51.874Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6b/b60ce2757e2faed3d70ed45dafee48cee7bfb878785a9423f7e883f0639c/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3beb14f053222b391bf9cf92ae82e0171067cc9c8f52453a0f1ec7c37df12a77", size = 1624176, upload-time = "2025-07-29T05:49:53.805Z" }, + { url = "https://files.pythonhosted.org/packages/dd/de/8c9fde2072a1b72c4fadecf4f7d4be7a85b1d9a4ab333d8245694057b4c6/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c39e87afe48aa3e814cac5f535bc6199180a53e38d3f51c5e2530f5aa4ec58c", size = 1696583, upload-time = "2025-07-29T05:49:55.338Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ad/07f863ca3d895a1ad958a54006c6dafb4f9310f8c2fdb5f961b8529029d3/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f1b4ce5bc528a6ee38dbf5f39bbf11dd127048726323b72b8e85769319ffc4", size = 1738896, upload-time = "2025-07-29T05:49:57.045Z" }, + { url = "https://files.pythonhosted.org/packages/20/43/2bd482ebe2b126533e8755a49b128ec4e58f1a3af56879a3abdb7b42c54f/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1004e67962efabbaf3f03b11b4c43b834081c9e3f9b32b16a7d97d4708a9abe6", size = 1643561, upload-time = "2025-07-29T05:49:58.762Z" }, + { url = "https://files.pythonhosted.org/packages/23/40/2fa9f514c4cf4cbae8d7911927f81a1901838baf5e09a8b2c299de1acfe5/aiohttp-3.12.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8faa08fcc2e411f7ab91d1541d9d597d3a90e9004180edb2072238c085eac8c2", size = 1583685, upload-time = "2025-07-29T05:50:00.375Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c3/94dc7357bc421f4fb978ca72a201a6c604ee90148f1181790c129396ceeb/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fe086edf38b2222328cdf89af0dde2439ee173b8ad7cb659b4e4c6f385b2be3d", size = 1627533, upload-time = "2025-07-29T05:50:02.306Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3f/1f8911fe1844a07001e26593b5c255a685318943864b27b4e0267e840f95/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:79b26fe467219add81d5e47b4a4ba0f2394e8b7c7c3198ed36609f9ba161aecb", size = 1638319, upload-time = "2025-07-29T05:50:04.282Z" }, + { url = "https://files.pythonhosted.org/packages/4e/46/27bf57a99168c4e145ffee6b63d0458b9c66e58bb70687c23ad3d2f0bd17/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b761bac1192ef24e16706d761aefcb581438b34b13a2f069a6d343ec8fb693a5", size = 1613776, upload-time = "2025-07-29T05:50:05.863Z" }, + { url = "https://files.pythonhosted.org/packages/0f/7e/1d2d9061a574584bb4ad3dbdba0da90a27fdc795bc227def3a46186a8bc1/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e153e8adacfe2af562861b72f8bc47f8a5c08e010ac94eebbe33dc21d677cd5b", size = 1693359, upload-time = "2025-07-29T05:50:07.563Z" }, + { url = "https://files.pythonhosted.org/packages/08/98/bee429b52233c4a391980a5b3b196b060872a13eadd41c3a34be9b1469ed/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:fc49c4de44977aa8601a00edbf157e9a421f227aa7eb477d9e3df48343311065", size = 1716598, upload-time = "2025-07-29T05:50:09.33Z" }, + { url = "https://files.pythonhosted.org/packages/57/39/b0314c1ea774df3392751b686104a3938c63ece2b7ce0ba1ed7c0b4a934f/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2776c7ec89c54a47029940177e75c8c07c29c66f73464784971d6a81904ce9d1", size = 1644940, upload-time = "2025-07-29T05:50:11.334Z" }, + { url = "https://files.pythonhosted.org/packages/1b/83/3dacb8d3f8f512c8ca43e3fa8a68b20583bd25636ffa4e56ee841ffd79ae/aiohttp-3.12.15-cp310-cp310-win32.whl", hash = "sha256:2c7d81a277fa78b2203ab626ced1487420e8c11a8e373707ab72d189fcdad20a", size = 429239, upload-time = "2025-07-29T05:50:12.803Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f9/470b5daba04d558c9673ca2034f28d067f3202a40e17804425f0c331c89f/aiohttp-3.12.15-cp310-cp310-win_amd64.whl", hash = "sha256:83603f881e11f0f710f8e2327817c82e79431ec976448839f3cd05d7afe8f830", size = 452297, upload-time = "2025-07-29T05:50:14.266Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "anyio" +version = "3.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup" }, + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/99/2dfd53fd55ce9838e6ff2d4dac20ce58263798bd1a0dbe18b3a9af3fcfce/anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780", size = 142927, upload-time = "2023-07-05T16:45:02.294Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/24/44299477fe7dcc9cb58d0a57d5a7588d6af2ff403fdd2d47a246c91a3246/anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5", size = 80896, upload-time = "2023-07-05T16:44:59.805Z" }, +] + +[[package]] +name = "appnope" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170, upload-time = "2024-02-06T09:43:11.258Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" }, +] + +[[package]] +name = "argon2-cffi" +version = "23.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argon2-cffi-bindings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/31/fa/57ec2c6d16ecd2ba0cf15f3c7d1c3c2e7b5fcb83555ff56d7ab10888ec8f/argon2_cffi-23.1.0.tar.gz", hash = "sha256:879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08", size = 42798, upload-time = "2023-08-15T14:13:12.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/6a/e8a041599e78b6b3752da48000b14c8d1e8a04ded09c88c714ba047f34f5/argon2_cffi-23.1.0-py3-none-any.whl", hash = "sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea", size = 15124, upload-time = "2023-08-15T14:13:10.752Z" }, +] + +[[package]] +name = "argon2-cffi-bindings" +version = "21.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/e9/184b8ccce6683b0aa2fbb7ba5683ea4b9c5763f1356347f1312c32e3c66e/argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3", size = 1779911, upload-time = "2021-12-01T08:52:55.68Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/13/838ce2620025e9666aa8f686431f67a29052241692a3dd1ae9d3692a89d3/argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367", size = 29658, upload-time = "2021-12-01T09:09:17.016Z" }, + { url = "https://files.pythonhosted.org/packages/b3/02/f7f7bb6b6af6031edb11037639c697b912e1dea2db94d436e681aea2f495/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d", size = 80583, upload-time = "2021-12-01T09:09:19.546Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f7/378254e6dd7ae6f31fe40c8649eea7d4832a42243acaf0f1fff9083b2bed/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae", size = 86168, upload-time = "2021-12-01T09:09:21.445Z" }, + { url = "https://files.pythonhosted.org/packages/74/f6/4a34a37a98311ed73bb80efe422fed95f2ac25a4cacc5ae1d7ae6a144505/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c", size = 82709, upload-time = "2021-12-01T09:09:18.182Z" }, + { url = "https://files.pythonhosted.org/packages/74/2b/73d767bfdaab25484f7e7901379d5f8793cccbb86c6e0cbc4c1b96f63896/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86", size = 83613, upload-time = "2021-12-01T09:09:22.741Z" }, + { url = "https://files.pythonhosted.org/packages/4f/fd/37f86deef67ff57c76f137a67181949c2d408077e2e3dd70c6c42912c9bf/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f", size = 84583, upload-time = "2021-12-01T09:09:24.177Z" }, + { url = "https://files.pythonhosted.org/packages/6f/52/5a60085a3dae8fded8327a4f564223029f5f54b0cb0455a31131b5363a01/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e", size = 88475, upload-time = "2021-12-01T09:09:26.673Z" }, + { url = "https://files.pythonhosted.org/packages/8b/95/143cd64feb24a15fa4b189a3e1e7efbaeeb00f39a51e99b26fc62fbacabd/argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082", size = 27698, upload-time = "2021-12-01T09:09:27.87Z" }, + { url = "https://files.pythonhosted.org/packages/37/2c/e34e47c7dee97ba6f01a6203e0383e15b60fb85d78ac9a15cd066f6fe28b/argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f", size = 30817, upload-time = "2021-12-01T09:09:30.267Z" }, + { url = "https://files.pythonhosted.org/packages/5a/e4/bf8034d25edaa495da3c8a3405627d2e35758e44ff6eaa7948092646fdcc/argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93", size = 53104, upload-time = "2021-12-01T09:09:31.335Z" }, +] + +[[package]] +name = "asttokens" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/e7/82da0a03e7ba5141f05cce0d302e6eed121ae055e0456ca228bf693984bc/asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7", size = 61978, upload-time = "2024-11-30T04:30:14.439Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918, upload-time = "2024-11-30T04:30:10.946Z" }, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, +] + +[[package]] +name = "babel" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067, upload-time = "2025-04-15T17:05:13.836Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285, upload-time = "2025-04-15T17:05:12.221Z" }, +] + +[[package]] +name = "bleach" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/e6/d5f220ca638f6a25557a611860482cb6e54b2d97f0332966b1b005742e1f/bleach-6.0.0.tar.gz", hash = "sha256:1a1a85c1595e07d8db14c5f09f09e6433502c51c595970edc090551f0db99414", size = 201298, upload-time = "2023-01-23T16:40:18.494Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/e2/dfcab68c9b2e7800c8f06b85c76e5f978d05b195a958daa9b1dda54a1db6/bleach-6.0.0-py3-none-any.whl", hash = "sha256:33c16e3353dbd13028ab4799a0f89a83f113405c766e9c122df8a06f5b85b3f4", size = 162493, upload-time = "2023-01-23T16:40:15.874Z" }, +] + +[package.optional-dependencies] +css = [ + { name = "tinycss2" }, +] + +[[package]] +name = "cached-property" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/4b/3d870836119dbe9a5e3c9a61af8cc1a8b69d75aea564572e385882d5aefb/cached_property-2.0.1.tar.gz", hash = "sha256:484d617105e3ee0e4f1f58725e72a8ef9e93deee462222dbd51cd91230897641", size = 10574, upload-time = "2024-10-25T15:43:55.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/0e/7d8225aab3bc1a0f5811f8e1b557aa034ac04bdf641925b30d3caf586b28/cached_property-2.0.1-py3-none-any.whl", hash = "sha256:f617d70ab1100b7bcf6e42228f9ddcb78c676ffa167278d9f730d1c2fba69ccb", size = 7428, upload-time = "2024-10-25T15:43:54.711Z" }, +] + +[[package]] +name = "certifi" +version = "2025.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191, upload-time = "2024-09-04T20:43:30.027Z" }, + { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592, upload-time = "2024-09-04T20:43:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024, upload-time = "2024-09-04T20:43:34.186Z" }, + { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188, upload-time = "2024-09-04T20:43:36.286Z" }, + { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571, upload-time = "2024-09-04T20:43:38.586Z" }, + { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687, upload-time = "2024-09-04T20:43:40.084Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211, upload-time = "2024-09-04T20:43:41.526Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325, upload-time = "2024-09-04T20:43:43.117Z" }, + { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784, upload-time = "2024-09-04T20:43:45.256Z" }, + { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564, upload-time = "2024-09-04T20:43:46.779Z" }, + { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804, upload-time = "2024-09-04T20:43:48.186Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299, upload-time = "2024-09-04T20:43:49.812Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818, upload-time = "2025-05-02T08:31:46.725Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649, upload-time = "2025-05-02T08:31:48.889Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045, upload-time = "2025-05-02T08:31:50.757Z" }, + { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356, upload-time = "2025-05-02T08:31:52.634Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471, upload-time = "2025-05-02T08:31:56.207Z" }, + { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317, upload-time = "2025-05-02T08:31:57.613Z" }, + { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368, upload-time = "2025-05-02T08:31:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491, upload-time = "2025-05-02T08:32:01.219Z" }, + { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695, upload-time = "2025-05-02T08:32:03.045Z" }, + { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849, upload-time = "2025-05-02T08:32:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091, upload-time = "2025-05-02T08:32:06.719Z" }, + { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445, upload-time = "2025-05-02T08:32:08.66Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782, upload-time = "2025-05-02T08:32:10.46Z" }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "comm" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/a8/fb783cb0abe2b5fded9f55e5703015cdf1c9c85b3669087c538dd15a6a86/comm-0.2.2.tar.gz", hash = "sha256:3fd7a84065306e07bea1773df6eb8282de51ba82f77c72f9c85716ab11fe980e", size = 6210, upload-time = "2024-03-12T16:53:41.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/75/49e5bfe642f71f272236b5b2d2691cf915a7283cc0ceda56357b61daa538/comm-0.2.2-py3-none-any.whl", hash = "sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3", size = 7180, upload-time = "2024-03-12T16:53:39.226Z" }, +] + +[[package]] +name = "debugpy" +version = "1.8.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/75/087fe07d40f490a78782ff3b0a30e3968936854105487decdb33446d4b0e/debugpy-1.8.14.tar.gz", hash = "sha256:7cd287184318416850aa8b60ac90105837bb1e59531898c07569d197d2ed5322", size = 1641444, upload-time = "2025-04-10T19:46:10.981Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/df/156df75a41aaebd97cee9d3870fe68f8001b6c1c4ca023e221cfce69bece/debugpy-1.8.14-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:93fee753097e85623cab1c0e6a68c76308cd9f13ffdf44127e6fab4fbf024339", size = 2076510, upload-time = "2025-04-10T19:46:13.315Z" }, + { url = "https://files.pythonhosted.org/packages/69/cd/4fc391607bca0996db5f3658762106e3d2427beaef9bfd363fd370a3c054/debugpy-1.8.14-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d937d93ae4fa51cdc94d3e865f535f185d5f9748efb41d0d49e33bf3365bd79", size = 3559614, upload-time = "2025-04-10T19:46:14.647Z" }, + { url = "https://files.pythonhosted.org/packages/1a/42/4e6d2b9d63e002db79edfd0cb5656f1c403958915e0e73ab3e9220012eec/debugpy-1.8.14-cp310-cp310-win32.whl", hash = "sha256:c442f20577b38cc7a9aafecffe1094f78f07fb8423c3dddb384e6b8f49fd2987", size = 5208588, upload-time = "2025-04-10T19:46:16.233Z" }, + { url = "https://files.pythonhosted.org/packages/97/b1/cc9e4e5faadc9d00df1a64a3c2d5c5f4b9df28196c39ada06361c5141f89/debugpy-1.8.14-cp310-cp310-win_amd64.whl", hash = "sha256:f117dedda6d969c5c9483e23f573b38f4e39412845c7bc487b6f2648df30fe84", size = 5241043, upload-time = "2025-04-10T19:46:17.768Z" }, + { url = "https://files.pythonhosted.org/packages/97/1a/481f33c37ee3ac8040d3d51fc4c4e4e7e61cb08b8bc8971d6032acc2279f/debugpy-1.8.14-py2.py3-none-any.whl", hash = "sha256:5cd9a579d553b6cb9759a7908a41988ee6280b961f24f63336835d9418216a20", size = 5256230, upload-time = "2025-04-10T19:46:54.077Z" }, +] + +[[package]] +name = "decorator" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, +] + +[[package]] +name = "executing" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/50/a9d80c47ff289c611ff12e63f7c5d13942c65d68125160cefd768c73e6e4/executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755", size = 978693, upload-time = "2025-01-22T15:41:29.403Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", size = 26702, upload-time = "2025-01-22T15:41:25.929Z" }, +] + +[[package]] +name = "fastjsonschema" +version = "2.21.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/50/4b769ce1ac4071a1ef6d86b1a3fb56cdc3a37615e8c5519e1af96cdac366/fastjsonschema-2.21.1.tar.gz", hash = "sha256:794d4f0a58f848961ba16af7b9c85a3e88cd360df008c59aac6fc5ae9323b5d4", size = 373939, upload-time = "2024-12-02T10:55:15.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/2b/0817a2b257fe88725c25589d89aec060581aabf668707a8d03b2e9e0cb2a/fastjsonschema-2.21.1-py3-none-any.whl", hash = "sha256:c9e5b7e908310918cf494a434eeb31384dd84a98b57a30bcb1f535015b554667", size = 23924, upload-time = "2024-12-02T10:55:07.599Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/b1/b64018016eeb087db503b038296fd782586432b9c077fc5c7839e9cb6ef6/frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f", size = 45078, upload-time = "2025-06-09T23:02:35.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/36/0da0a49409f6b47cc2d060dc8c9040b897b5902a8a4e37d9bc1deb11f680/frozenlist-1.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cc4df77d638aa2ed703b878dd093725b72a824c3c546c076e8fdf276f78ee84a", size = 81304, upload-time = "2025-06-09T22:59:46.226Z" }, + { url = "https://files.pythonhosted.org/packages/77/f0/77c11d13d39513b298e267b22eb6cb559c103d56f155aa9a49097221f0b6/frozenlist-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:716a9973a2cc963160394f701964fe25012600f3d311f60c790400b00e568b61", size = 47735, upload-time = "2025-06-09T22:59:48.133Z" }, + { url = "https://files.pythonhosted.org/packages/37/12/9d07fa18971a44150593de56b2f2947c46604819976784bcf6ea0d5db43b/frozenlist-1.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0fd1bad056a3600047fb9462cff4c5322cebc59ebf5d0a3725e0ee78955001d", size = 46775, upload-time = "2025-06-09T22:59:49.564Z" }, + { url = "https://files.pythonhosted.org/packages/70/34/f73539227e06288fcd1f8a76853e755b2b48bca6747e99e283111c18bcd4/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3789ebc19cb811163e70fe2bd354cea097254ce6e707ae42e56f45e31e96cb8e", size = 224644, upload-time = "2025-06-09T22:59:51.35Z" }, + { url = "https://files.pythonhosted.org/packages/fb/68/c1d9c2f4a6e438e14613bad0f2973567586610cc22dcb1e1241da71de9d3/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af369aa35ee34f132fcfad5be45fbfcde0e3a5f6a1ec0712857f286b7d20cca9", size = 222125, upload-time = "2025-06-09T22:59:52.884Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d0/98e8f9a515228d708344d7c6986752be3e3192d1795f748c24bcf154ad99/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac64b6478722eeb7a3313d494f8342ef3478dff539d17002f849101b212ef97c", size = 233455, upload-time = "2025-06-09T22:59:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/79/df/8a11bcec5600557f40338407d3e5bea80376ed1c01a6c0910fcfdc4b8993/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f89f65d85774f1797239693cef07ad4c97fdd0639544bad9ac4b869782eb1981", size = 227339, upload-time = "2025-06-09T22:59:56.187Z" }, + { url = "https://files.pythonhosted.org/packages/50/82/41cb97d9c9a5ff94438c63cc343eb7980dac4187eb625a51bdfdb7707314/frozenlist-1.7.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1073557c941395fdfcfac13eb2456cb8aad89f9de27bae29fabca8e563b12615", size = 212969, upload-time = "2025-06-09T22:59:57.604Z" }, + { url = "https://files.pythonhosted.org/packages/13/47/f9179ee5ee4f55629e4f28c660b3fdf2775c8bfde8f9c53f2de2d93f52a9/frozenlist-1.7.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed8d2fa095aae4bdc7fdd80351009a48d286635edffee66bf865e37a9125c50", size = 222862, upload-time = "2025-06-09T22:59:59.498Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/df81e41ec6b953902c8b7e3a83bee48b195cb0e5ec2eabae5d8330c78038/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:24c34bea555fe42d9f928ba0a740c553088500377448febecaa82cc3e88aa1fa", size = 222492, upload-time = "2025-06-09T23:00:01.026Z" }, + { url = "https://files.pythonhosted.org/packages/84/17/30d6ea87fa95a9408245a948604b82c1a4b8b3e153cea596421a2aef2754/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:69cac419ac6a6baad202c85aaf467b65ac860ac2e7f2ac1686dc40dbb52f6577", size = 238250, upload-time = "2025-06-09T23:00:03.401Z" }, + { url = "https://files.pythonhosted.org/packages/8f/00/ecbeb51669e3c3df76cf2ddd66ae3e48345ec213a55e3887d216eb4fbab3/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:960d67d0611f4c87da7e2ae2eacf7ea81a5be967861e0c63cf205215afbfac59", size = 218720, upload-time = "2025-06-09T23:00:05.282Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c0/c224ce0e0eb31cc57f67742071bb470ba8246623c1823a7530be0e76164c/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:41be2964bd4b15bf575e5daee5a5ce7ed3115320fb3c2b71fca05582ffa4dc9e", size = 232585, upload-time = "2025-06-09T23:00:07.962Z" }, + { url = "https://files.pythonhosted.org/packages/55/3c/34cb694abf532f31f365106deebdeac9e45c19304d83cf7d51ebbb4ca4d1/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:46d84d49e00c9429238a7ce02dc0be8f6d7cd0cd405abd1bebdc991bf27c15bd", size = 234248, upload-time = "2025-06-09T23:00:09.428Z" }, + { url = "https://files.pythonhosted.org/packages/98/c0/2052d8b6cecda2e70bd81299e3512fa332abb6dcd2969b9c80dfcdddbf75/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:15900082e886edb37480335d9d518cec978afc69ccbc30bd18610b7c1b22a718", size = 221621, upload-time = "2025-06-09T23:00:11.32Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bf/7dcebae315436903b1d98ffb791a09d674c88480c158aa171958a3ac07f0/frozenlist-1.7.0-cp310-cp310-win32.whl", hash = "sha256:400ddd24ab4e55014bba442d917203c73b2846391dd42ca5e38ff52bb18c3c5e", size = 39578, upload-time = "2025-06-09T23:00:13.526Z" }, + { url = "https://files.pythonhosted.org/packages/8f/5f/f69818f017fa9a3d24d1ae39763e29b7f60a59e46d5f91b9c6b21622f4cd/frozenlist-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:6eb93efb8101ef39d32d50bce242c84bcbddb4f7e9febfa7b524532a239b4464", size = 43830, upload-time = "2025-06-09T23:00:14.98Z" }, + { url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106, upload-time = "2025-06-09T23:02:34.204Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "ipykernel" +version = "6.29.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appnope", marker = "sys_platform == 'darwin'" }, + { name = "comm" }, + { name = "debugpy" }, + { name = "ipython" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "matplotlib-inline" }, + { name = "nest-asyncio" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/5c/67594cb0c7055dc50814b21731c22a601101ea3b1b50a9a1b090e11f5d0f/ipykernel-6.29.5.tar.gz", hash = "sha256:f093a22c4a40f8828f8e330a9c297cb93dcab13bd9678ded6de8e5cf81c56215", size = 163367, upload-time = "2024-07-01T14:07:22.543Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/5c/368ae6c01c7628438358e6d337c19b05425727fbb221d2a3c4303c372f42/ipykernel-6.29.5-py3-none-any.whl", hash = "sha256:afdb66ba5aa354b09b91379bac28ae4afebbb30e8b39510c9690afb7a10421b5", size = 117173, upload-time = "2024-07-01T14:07:19.603Z" }, +] + +[[package]] +name = "ipython" +version = "8.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "decorator" }, + { name = "exceptiongroup" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect", marker = "sys_platform != 'win32'" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/aa/6b5bd15d4914394a4f6bd5a4d88dee2d5ddd1b346b8b60fd9e735223a8ea/ipython-8.21.0.tar.gz", hash = "sha256:48fbc236fbe0e138b88773fa0437751f14c3645fb483f1d4c5dee58b37e5ce73", size = 5490331, upload-time = "2024-01-31T10:35:33.969Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e7/07dc8b6541affd4de15f0e8fc855f238cb93d04c4f8490757226d12cdb5a/ipython-8.21.0-py3-none-any.whl", hash = "sha256:1050a3ab8473488d7eee163796b02e511d0735cf43a04ba2a8348bd0f2eaf8a5", size = 810017, upload-time = "2024-01-31T10:35:28.909Z" }, +] + +[[package]] +name = "ipython-genutils" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/69/fbeffffc05236398ebfcfb512b6d2511c622871dca1746361006da310399/ipython_genutils-0.2.0.tar.gz", hash = "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8", size = 22208, upload-time = "2017-03-13T22:12:26.393Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/bc/9bd3b5c2b4774d5f33b2d544f1460be9df7df2fe42f352135381c347c69a/ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8", size = 26343, upload-time = "2017-03-13T22:12:25.412Z" }, +] + +[[package]] +name = "jedi" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "json5" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/be/c6c745ec4c4539b25a278b70e29793f10382947df0d9efba2fa09120895d/json5-0.12.0.tar.gz", hash = "sha256:0b4b6ff56801a1c7dc817b0241bca4ce474a0e6a163bfef3fc594d3fd263ff3a", size = 51907, upload-time = "2025-04-03T16:33:13.201Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/9f/3500910d5a98549e3098807493851eeef2b89cdd3032227558a104dfe926/json5-0.12.0-py3-none-any.whl", hash = "sha256:6d37aa6c08b0609f16e1ec5ff94697e2cbbfbad5ac112afa05794da9ab7810db", size = 36079, upload-time = "2025-04-03T16:33:11.927Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/d3/1cf5326b923a53515d8f3a2cd442e6d7e94fcc444716e879ea70a0ce3177/jsonschema-4.24.0.tar.gz", hash = "sha256:0b4e8069eb12aedfa881333004bccaec24ecef5a8a6a4b6df142b2cc9599d196", size = 353480, upload-time = "2025-05-26T18:48:10.459Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/3d/023389198f69c722d039351050738d6755376c8fd343e91dc493ea485905/jsonschema-4.24.0-py3-none-any.whl", hash = "sha256:a462455f19f5faf404a7902952b6f0e3ce868f3ee09a359b05eca6673bd8412d", size = 88709, upload-time = "2025-05-26T18:48:08.417Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513, upload-time = "2025-04-23T12:34:07.418Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437, upload-time = "2025-04-23T12:34:05.422Z" }, +] + +[[package]] +name = "jupyter-client" +version = "8.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-core" }, + { name = "python-dateutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/22/bf9f12fdaeae18019a468b68952a60fe6dbab5d67cd2a103cac7659b41ca/jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419", size = 342019, upload-time = "2024-09-17T10:44:17.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/85/b0394e0b6fcccd2c1eeefc230978a6f8cb0c5df1e4cd3e7625735a0d7d1e/jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f", size = 106105, upload-time = "2024-09-17T10:44:15.218Z" }, +] + +[[package]] +name = "jupyter-contrib-core" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-core" }, + { name = "notebook" }, + { name = "setuptools" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/94/0d37e5b49ea1c8bf204c46f9b0257c1f3319a4ab88acbd401da2cab25e55/jupyter_contrib_core-0.4.2.tar.gz", hash = "sha256:1887212f3ca9d4487d624c0705c20dfdf03d5a0b9ea2557d3aaeeb4c38bdcabb", size = 17490, upload-time = "2022-11-15T16:21:53.357Z" } + +[[package]] +name = "jupyter-contrib-nbextensions" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ipython-genutils" }, + { name = "jupyter-contrib-core" }, + { name = "jupyter-core" }, + { name = "jupyter-highlight-selected-word" }, + { name = "jupyter-nbextensions-configurator" }, + { name = "lxml" }, + { name = "nbconvert" }, + { name = "notebook" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/91/78cc4362611dbde2b0cd068204aaf1b8899d0459c50d8ff9daca8c069791/jupyter_contrib_nbextensions-0.7.0.tar.gz", hash = "sha256:06e33f005885eb92f89cbe82711e921278201298d08ab0d886d1ba09e8c3e9ca", size = 23462252, upload-time = "2022-11-15T17:31:27.754Z" } + +[[package]] +name = "jupyter-core" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "platformdirs" }, + { name = "pywin32", marker = "platform_python_implementation != 'PyPy' and sys_platform == 'win32'" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/53/f27bd74ceaa672a1ce17b4b2bee93c0742ca00cb9f540ec4fa60cf7319b5/jupyter_core-5.3.1.tar.gz", hash = "sha256:5ba5c7938a7f97a6b0481463f7ff0dbac7c15ba48cf46fa4035ca6e838aa1aba", size = 84448, upload-time = "2023-06-14T15:16:33.02Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/e0/3f9061c5e99a03612510f892647b15a91f910c5275b7b77c6c72edae1494/jupyter_core-5.3.1-py3-none-any.whl", hash = "sha256:ae9036db959a71ec1cac33081eeb040a79e681f08ab68b0883e9a676c7a90dce", size = 93670, upload-time = "2023-06-14T15:16:31.042Z" }, +] + +[[package]] +name = "jupyter-highlight-selected-word" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/a5/3dfeb7c8643ef502e82969fdebb201b63b33ded15a7761b27299bacebc3a/jupyter_highlight_selected_word-0.2.0.tar.gz", hash = "sha256:9fa740424859a807950ca08d2bfd28a35154cd32dd6d50ac4e0950022adc0e7b", size = 12592, upload-time = "2018-04-07T13:56:22.498Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/d7/19ab7cfd60bf268d2abbacc52d4295a40f52d74dfc0d938e4761ee5e598b/jupyter_highlight_selected_word-0.2.0-py2.py3-none-any.whl", hash = "sha256:9545dfa9cb057eebe3a5795604dcd3a5294ea18637e553f61a0b67c1b5903c58", size = 11699, upload-time = "2018-04-07T13:56:20.715Z" }, +] + +[[package]] +name = "jupyter-nbextensions-configurator" +version = "0.6.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-contrib-core" }, + { name = "jupyter-core" }, + { name = "jupyter-server" }, + { name = "notebook" }, + { name = "pyyaml" }, + { name = "tornado" }, + { name = "traitlets" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/fe/cffb14a4fbb43cf276aa3047e42c3f9ecfda851ba3c466295401f6b1e085/jupyter_nbextensions_configurator-0.6.4-py2.py3-none-any.whl", hash = "sha256:fe7a7b0805b5926449692fb077e0e659bab8b27563bc68cba26854532fdf99c7", size = 466890, upload-time = "2024-06-05T16:08:37.236Z" }, +] + +[[package]] +name = "jupyter-server" +version = "1.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "argon2-cffi" }, + { name = "jinja2" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "nbconvert" }, + { name = "nbformat" }, + { name = "packaging" }, + { name = "prometheus-client" }, + { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "pyzmq" }, + { name = "send2trash" }, + { name = "terminado" }, + { name = "tornado" }, + { name = "traitlets" }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/0c/ce06c97c52707bc0fed9461ed5624f1a5ee76dc772d7da2c0699395653af/jupyter_server-1.24.0.tar.gz", hash = "sha256:23368e8e214baf82b313d4c5a0d828ca73015e1a192ce3829bd74e62fab8d046", size = 456590, upload-time = "2023-04-13T19:53:20.991Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/ce/142bcb35ffe215d8880e968689ab733bd7976a6c20dae24b6782cce2219a/jupyter_server-1.24.0-py3-none-any.whl", hash = "sha256:c88ddbe862966ea1aea8c3ccb89a5903abd8fbcfe5cd14090ef549d403332c37", size = 347516, upload-time = "2023-04-13T19:53:18.153Z" }, +] + +[[package]] +name = "jupyter-server-proxy" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "jupyter-server" }, + { name = "simpervisor" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/b6/c9e6b1e6bd84e43811e533a54f20e44537461f0b689198ff53374b58ab21/jupyter_server_proxy-4.0.0.tar.gz", hash = "sha256:f5dc12dd204baca71b013df3522c14403692a2d37cb7adcd77851dbab71533b5", size = 130416, upload-time = "2023-04-20T14:59:56.48Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/40/5a8c2128f5efebe4eff29e0d3d92dac4d8f760f2e5bb584f5800e51fd230/jupyter_server_proxy-4.0.0-py3-none-any.whl", hash = "sha256:8075afce3465a5e987e43ec837c307f9b9ac7398ebcff497abf1f51303d23470", size = 32731, upload-time = "2023-04-20T14:59:53.408Z" }, +] + +[[package]] +name = "jupyterlab" +version = "3.4.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ipython" }, + { name = "jinja2" }, + { name = "jupyter-core" }, + { name = "jupyter-server" }, + { name = "jupyterlab-server" }, + { name = "nbclassic" }, + { name = "notebook" }, + { name = "packaging" }, + { name = "tomli" }, + { name = "tornado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/13/bad942536fdec9dce4d5c32fdb6bb54633800bdf4eb43f677fe0cbe4009a/jupyterlab-3.4.8.tar.gz", hash = "sha256:1fafb8b657005d91603f3c3adfd6d9e8eaf33fdc601537fef09283332efe67cb", size = 17104727, upload-time = "2022-10-04T12:23:41.126Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/67/be3e254f846d5a143edc385bdfd61ee366be70a3223808f30f0b6b3d8f62/jupyterlab-3.4.8-py3-none-any.whl", hash = "sha256:4626a0434c76a3a22f11c4efaa1d031d2586367f72cfdbdbff6b08b6ef0060f7", size = 8782663, upload-time = "2022-10-04T12:23:35.398Z" }, +] + +[[package]] +name = "jupyterlab-pygments" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/51/9187be60d989df97f5f0aba133fa54e7300f17616e065d1ada7d7646b6d6/jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d", size = 512900, upload-time = "2023-11-23T09:26:37.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780", size = 15884, upload-time = "2023-11-23T09:26:34.325Z" }, +] + +[[package]] +name = "jupyterlab-server" +version = "2.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "jinja2" }, + { name = "json5" }, + { name = "jsonschema" }, + { name = "jupyter-server" }, + { name = "packaging" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/65/f7ba44dc74f611b1d00fb37ed1ad0069edeb353a0f0cc7214e3014575b85/jupyterlab_server-2.23.0.tar.gz", hash = "sha256:83c01aa4ad9451cd61b383e634d939ff713850f4640c0056b2cdb2b6211a74c7", size = 71812, upload-time = "2023-06-13T08:46:04.901Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/14/8f1c4b9b80db855d48a762e248efd41649d102841b6bfadbd26b8c25e054/jupyterlab_server-2.23.0-py3-none-any.whl", hash = "sha256:a5ea2c839336a8ba7c38c8e7b2f24cedf919f0d439f4d2e606d9322013a95788", size = 57266, upload-time = "2023-06-13T08:46:02.298Z" }, +] + +[[package]] +name = "lxml" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/ed/60eb6fa2923602fba988d9ca7c5cdbd7cf25faa795162ed538b527a35411/lxml-6.0.0.tar.gz", hash = "sha256:032e65120339d44cdc3efc326c9f660f5f7205f3a535c1fdbf898b29ea01fb72", size = 4096938, upload-time = "2025-06-26T16:28:19.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/e9/9c3ca02fbbb7585116c2e274b354a2d92b5c70561687dd733ec7b2018490/lxml-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:35bc626eec405f745199200ccb5c6b36f202675d204aa29bb52e27ba2b71dea8", size = 8399057, upload-time = "2025-06-26T16:25:02.169Z" }, + { url = "https://files.pythonhosted.org/packages/86/25/10a6e9001191854bf283515020f3633b1b1f96fd1b39aa30bf8fff7aa666/lxml-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:246b40f8a4aec341cbbf52617cad8ab7c888d944bfe12a6abd2b1f6cfb6f6082", size = 4569676, upload-time = "2025-06-26T16:25:05.431Z" }, + { url = "https://files.pythonhosted.org/packages/f5/a5/378033415ff61d9175c81de23e7ad20a3ffb614df4ffc2ffc86bc6746ffd/lxml-6.0.0-cp310-cp310-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:2793a627e95d119e9f1e19720730472f5543a6d84c50ea33313ce328d870f2dd", size = 5291361, upload-time = "2025-06-26T16:25:07.901Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a6/19c87c4f3b9362b08dc5452a3c3bce528130ac9105fc8fff97ce895ce62e/lxml-6.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:46b9ed911f36bfeb6338e0b482e7fe7c27d362c52fde29f221fddbc9ee2227e7", size = 5008290, upload-time = "2025-06-28T18:47:13.196Z" }, + { url = "https://files.pythonhosted.org/packages/09/d1/e9b7ad4b4164d359c4d87ed8c49cb69b443225cb495777e75be0478da5d5/lxml-6.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b4790b558bee331a933e08883c423f65bbcd07e278f91b2272489e31ab1e2b4", size = 5163192, upload-time = "2025-06-28T18:47:17.279Z" }, + { url = "https://files.pythonhosted.org/packages/56/d6/b3eba234dc1584744b0b374a7f6c26ceee5dc2147369a7e7526e25a72332/lxml-6.0.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2030956cf4886b10be9a0285c6802e078ec2391e1dd7ff3eb509c2c95a69b76", size = 5076973, upload-time = "2025-06-26T16:25:10.936Z" }, + { url = "https://files.pythonhosted.org/packages/8e/47/897142dd9385dcc1925acec0c4afe14cc16d310ce02c41fcd9010ac5d15d/lxml-6.0.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d23854ecf381ab1facc8f353dcd9adeddef3652268ee75297c1164c987c11dc", size = 5297795, upload-time = "2025-06-26T16:25:14.282Z" }, + { url = "https://files.pythonhosted.org/packages/fb/db/551ad84515c6f415cea70193a0ff11d70210174dc0563219f4ce711655c6/lxml-6.0.0-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:43fe5af2d590bf4691531b1d9a2495d7aab2090547eaacd224a3afec95706d76", size = 4776547, upload-time = "2025-06-26T16:25:17.123Z" }, + { url = "https://files.pythonhosted.org/packages/e0/14/c4a77ab4f89aaf35037a03c472f1ccc54147191888626079bd05babd6808/lxml-6.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74e748012f8c19b47f7d6321ac929a9a94ee92ef12bc4298c47e8b7219b26541", size = 5124904, upload-time = "2025-06-26T16:25:19.485Z" }, + { url = "https://files.pythonhosted.org/packages/70/b4/12ae6a51b8da106adec6a2e9c60f532350a24ce954622367f39269e509b1/lxml-6.0.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:43cfbb7db02b30ad3926e8fceaef260ba2fb7df787e38fa2df890c1ca7966c3b", size = 4805804, upload-time = "2025-06-26T16:25:21.949Z" }, + { url = "https://files.pythonhosted.org/packages/a9/b6/2e82d34d49f6219cdcb6e3e03837ca5fb8b7f86c2f35106fb8610ac7f5b8/lxml-6.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:34190a1ec4f1e84af256495436b2d196529c3f2094f0af80202947567fdbf2e7", size = 5323477, upload-time = "2025-06-26T16:25:24.475Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e6/b83ddc903b05cd08a5723fefd528eee84b0edd07bdf87f6c53a1fda841fd/lxml-6.0.0-cp310-cp310-win32.whl", hash = "sha256:5967fe415b1920a3877a4195e9a2b779249630ee49ece22021c690320ff07452", size = 3613840, upload-time = "2025-06-26T16:25:27.345Z" }, + { url = "https://files.pythonhosted.org/packages/40/af/874fb368dd0c663c030acb92612341005e52e281a102b72a4c96f42942e1/lxml-6.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:f3389924581d9a770c6caa4df4e74b606180869043b9073e2cec324bad6e306e", size = 3993584, upload-time = "2025-06-26T16:25:29.391Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f4/d296bc22c17d5607653008f6dd7b46afdfda12efd31021705b507df652bb/lxml-6.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:522fe7abb41309e9543b0d9b8b434f2b630c5fdaf6482bee642b34c8c70079c8", size = 3681400, upload-time = "2025-06-26T16:25:31.421Z" }, + { url = "https://files.pythonhosted.org/packages/66/e1/2c22a3cff9e16e1d717014a1e6ec2bf671bf56ea8716bb64466fcf820247/lxml-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:dbdd7679a6f4f08152818043dbb39491d1af3332128b3752c3ec5cebc0011a72", size = 3898804, upload-time = "2025-06-26T16:27:59.751Z" }, + { url = "https://files.pythonhosted.org/packages/2b/3a/d68cbcb4393a2a0a867528741fafb7ce92dac5c9f4a1680df98e5e53e8f5/lxml-6.0.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:40442e2a4456e9910875ac12951476d36c0870dcb38a68719f8c4686609897c4", size = 4216406, upload-time = "2025-06-28T18:47:45.518Z" }, + { url = "https://files.pythonhosted.org/packages/15/8f/d9bfb13dff715ee3b2a1ec2f4a021347ea3caf9aba93dea0cfe54c01969b/lxml-6.0.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db0efd6bae1c4730b9c863fc4f5f3c0fa3e8f05cae2c44ae141cb9dfc7d091dc", size = 4326455, upload-time = "2025-06-28T18:47:48.411Z" }, + { url = "https://files.pythonhosted.org/packages/01/8b/fde194529ee8a27e6f5966d7eef05fa16f0567e4a8e8abc3b855ef6b3400/lxml-6.0.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ab542c91f5a47aaa58abdd8ea84b498e8e49fe4b883d67800017757a3eb78e8", size = 4268788, upload-time = "2025-06-26T16:28:02.776Z" }, + { url = "https://files.pythonhosted.org/packages/99/a8/3b8e2581b4f8370fc9e8dc343af4abdfadd9b9229970fc71e67bd31c7df1/lxml-6.0.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:013090383863b72c62a702d07678b658fa2567aa58d373d963cca245b017e065", size = 4411394, upload-time = "2025-06-26T16:28:05.179Z" }, + { url = "https://files.pythonhosted.org/packages/e7/a5/899a4719e02ff4383f3f96e5d1878f882f734377f10dfb69e73b5f223e44/lxml-6.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c86df1c9af35d903d2b52d22ea3e66db8058d21dc0f59842ca5deb0595921141", size = 3517946, upload-time = "2025-06-26T16:28:07.665Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, +] + +[[package]] +name = "matplotlib-inline" +version = "0.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/5b/a36a337438a14116b16480db471ad061c36c3694df7c2084a0da7ba538b7/matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", size = 8159, upload-time = "2024-04-15T13:44:44.803Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899, upload-time = "2024-04-15T13:44:43.265Z" }, +] + +[[package]] +name = "mistune" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/79/bda47f7dd7c3c55770478d6d02c9960c430b0cf1773b72366ff89126ea31/mistune-3.1.3.tar.gz", hash = "sha256:a7035c21782b2becb6be62f8f25d3df81ccb4d6fa477a6525b15af06539f02a0", size = 94347, upload-time = "2025-03-19T14:27:24.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/4d/23c4e4f09da849e127e9f123241946c23c1e30f45a88366879e064211815/mistune-3.1.3-py3-none-any.whl", hash = "sha256:1a32314113cff28aa6432e99e522677c8587fd83e3d51c29b82a52409c842bd9", size = 53410, upload-time = "2025-03-19T14:27:23.451Z" }, +] + +[[package]] +name = "multidict" +version = "6.6.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/7f/0652e6ed47ab288e3756ea9c0df8b14950781184d4bd7883f4d87dd41245/multidict-6.6.4.tar.gz", hash = "sha256:d2d4e4787672911b48350df02ed3fa3fffdc2f2e8ca06dd6afdf34189b76a9dd", size = 101843, upload-time = "2025-08-11T12:08:48.217Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/6b/86f353088c1358e76fd30b0146947fddecee812703b604ee901e85cd2a80/multidict-6.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b8aa6f0bd8125ddd04a6593437bad6a7e70f300ff4180a531654aa2ab3f6d58f", size = 77054, upload-time = "2025-08-11T12:06:02.99Z" }, + { url = "https://files.pythonhosted.org/packages/19/5d/c01dc3d3788bb877bd7f5753ea6eb23c1beeca8044902a8f5bfb54430f63/multidict-6.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b9e5853bbd7264baca42ffc53391b490d65fe62849bf2c690fa3f6273dbcd0cb", size = 44914, upload-time = "2025-08-11T12:06:05.264Z" }, + { url = "https://files.pythonhosted.org/packages/46/44/964dae19ea42f7d3e166474d8205f14bb811020e28bc423d46123ddda763/multidict-6.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0af5f9dee472371e36d6ae38bde009bd8ce65ac7335f55dcc240379d7bed1495", size = 44601, upload-time = "2025-08-11T12:06:06.627Z" }, + { url = "https://files.pythonhosted.org/packages/31/20/0616348a1dfb36cb2ab33fc9521de1f27235a397bf3f59338e583afadd17/multidict-6.6.4-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:d24f351e4d759f5054b641c81e8291e5d122af0fca5c72454ff77f7cbe492de8", size = 224821, upload-time = "2025-08-11T12:06:08.06Z" }, + { url = "https://files.pythonhosted.org/packages/14/26/5d8923c69c110ff51861af05bd27ca6783011b96725d59ccae6d9daeb627/multidict-6.6.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db6a3810eec08280a172a6cd541ff4a5f6a97b161d93ec94e6c4018917deb6b7", size = 242608, upload-time = "2025-08-11T12:06:09.697Z" }, + { url = "https://files.pythonhosted.org/packages/5c/cc/e2ad3ba9459aa34fa65cf1f82a5c4a820a2ce615aacfb5143b8817f76504/multidict-6.6.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a1b20a9d56b2d81e2ff52ecc0670d583eaabaa55f402e8d16dd062373dbbe796", size = 222324, upload-time = "2025-08-11T12:06:10.905Z" }, + { url = "https://files.pythonhosted.org/packages/19/db/4ed0f65701afbc2cb0c140d2d02928bb0fe38dd044af76e58ad7c54fd21f/multidict-6.6.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8c9854df0eaa610a23494c32a6f44a3a550fb398b6b51a56e8c6b9b3689578db", size = 253234, upload-time = "2025-08-11T12:06:12.658Z" }, + { url = "https://files.pythonhosted.org/packages/94/c1/5160c9813269e39ae14b73debb907bfaaa1beee1762da8c4fb95df4764ed/multidict-6.6.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4bb7627fd7a968f41905a4d6343b0d63244a0623f006e9ed989fa2b78f4438a0", size = 251613, upload-time = "2025-08-11T12:06:13.97Z" }, + { url = "https://files.pythonhosted.org/packages/05/a9/48d1bd111fc2f8fb98b2ed7f9a115c55a9355358432a19f53c0b74d8425d/multidict-6.6.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caebafea30ed049c57c673d0b36238b1748683be2593965614d7b0e99125c877", size = 241649, upload-time = "2025-08-11T12:06:15.204Z" }, + { url = "https://files.pythonhosted.org/packages/85/2a/f7d743df0019408768af8a70d2037546a2be7b81fbb65f040d76caafd4c5/multidict-6.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ad887a8250eb47d3ab083d2f98db7f48098d13d42eb7a3b67d8a5c795f224ace", size = 239238, upload-time = "2025-08-11T12:06:16.467Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b8/4f4bb13323c2d647323f7919201493cf48ebe7ded971717bfb0f1a79b6bf/multidict-6.6.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:ed8358ae7d94ffb7c397cecb62cbac9578a83ecefc1eba27b9090ee910e2efb6", size = 233517, upload-time = "2025-08-11T12:06:18.107Z" }, + { url = "https://files.pythonhosted.org/packages/33/29/4293c26029ebfbba4f574febd2ed01b6f619cfa0d2e344217d53eef34192/multidict-6.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ecab51ad2462197a4c000b6d5701fc8585b80eecb90583635d7e327b7b6923eb", size = 243122, upload-time = "2025-08-11T12:06:19.361Z" }, + { url = "https://files.pythonhosted.org/packages/20/60/a1c53628168aa22447bfde3a8730096ac28086704a0d8c590f3b63388d0c/multidict-6.6.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c5c97aa666cf70e667dfa5af945424ba1329af5dd988a437efeb3a09430389fb", size = 248992, upload-time = "2025-08-11T12:06:20.661Z" }, + { url = "https://files.pythonhosted.org/packages/a3/3b/55443a0c372f33cae5d9ec37a6a973802884fa0ab3586659b197cf8cc5e9/multidict-6.6.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:9a950b7cf54099c1209f455ac5970b1ea81410f2af60ed9eb3c3f14f0bfcf987", size = 243708, upload-time = "2025-08-11T12:06:21.891Z" }, + { url = "https://files.pythonhosted.org/packages/7c/60/a18c6900086769312560b2626b18e8cca22d9e85b1186ba77f4755b11266/multidict-6.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:163c7ea522ea9365a8a57832dea7618e6cbdc3cd75f8c627663587459a4e328f", size = 237498, upload-time = "2025-08-11T12:06:23.206Z" }, + { url = "https://files.pythonhosted.org/packages/11/3d/8bdd8bcaff2951ce2affccca107a404925a2beafedd5aef0b5e4a71120a6/multidict-6.6.4-cp310-cp310-win32.whl", hash = "sha256:17d2cbbfa6ff20821396b25890f155f40c986f9cfbce5667759696d83504954f", size = 41415, upload-time = "2025-08-11T12:06:24.77Z" }, + { url = "https://files.pythonhosted.org/packages/c0/53/cab1ad80356a4cd1b685a254b680167059b433b573e53872fab245e9fc95/multidict-6.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:ce9a40fbe52e57e7edf20113a4eaddfacac0561a0879734e636aa6d4bb5e3fb0", size = 46046, upload-time = "2025-08-11T12:06:25.893Z" }, + { url = "https://files.pythonhosted.org/packages/cf/9a/874212b6f5c1c2d870d0a7adc5bb4cfe9b0624fa15cdf5cf757c0f5087ae/multidict-6.6.4-cp310-cp310-win_arm64.whl", hash = "sha256:01d0959807a451fe9fdd4da3e139cb5b77f7328baf2140feeaf233e1d777b729", size = 43147, upload-time = "2025-08-11T12:06:27.534Z" }, + { url = "https://files.pythonhosted.org/packages/fd/69/b547032297c7e63ba2af494edba695d781af8a0c6e89e4d06cf848b21d80/multidict-6.6.4-py3-none-any.whl", hash = "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c", size = 12313, upload-time = "2025-08-11T12:08:46.891Z" }, +] + +[[package]] +name = "nbclassic" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ipykernel" }, + { name = "ipython-genutils" }, + { name = "nest-asyncio" }, + { name = "notebook-shim" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/ae/dea5e0f8f5c519abe52706d23f9dc1a87b4badb9f98beadda16f896f994f/nbclassic-1.3.1.tar.gz", hash = "sha256:4c52da8fc88f9f73ef512cc305091d5ce726bdca19f44ed697cb5ba12dcaad3c", size = 81488343, upload-time = "2025-05-06T16:02:05.945Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/70/6c5dd85072e7f82272a6dfab3698b5cb3db29949a6b16f268569d27a57a3/nbclassic-1.3.1-py3-none-any.whl", hash = "sha256:96da3b4d7f877b1285e0adc956ea2ea9ea9f70a4ba7b7c03d558f6c9799118fa", size = 26187709, upload-time = "2025-05-06T16:01:54.185Z" }, +] + +[[package]] +name = "nbclient" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "nbformat" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/66/7ffd18d58eae90d5721f9f39212327695b749e23ad44b3881744eaf4d9e8/nbclient-0.10.2.tar.gz", hash = "sha256:90b7fc6b810630db87a6d0c2250b1f0ab4cf4d3c27a299b0cde78a4ed3fd9193", size = 62424, upload-time = "2024-12-19T10:32:27.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/6d/e7fa07f03a4a7b221d94b4d586edb754a9b0dc3c9e2c93353e9fa4e0d117/nbclient-0.10.2-py3-none-any.whl", hash = "sha256:4ffee11e788b4a27fabeb7955547e4318a5298f34342a4bfd01f2e1faaeadc3d", size = 25434, upload-time = "2024-12-19T10:32:24.139Z" }, +] + +[[package]] +name = "nbconvert" +version = "7.16.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "bleach", extra = ["css"] }, + { name = "defusedxml" }, + { name = "jinja2" }, + { name = "jupyter-core" }, + { name = "jupyterlab-pygments" }, + { name = "markupsafe" }, + { name = "mistune" }, + { name = "nbclient" }, + { name = "nbformat" }, + { name = "packaging" }, + { name = "pandocfilters" }, + { name = "pygments" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/59/f28e15fc47ffb73af68a8d9b47367a8630d76e97ae85ad18271b9db96fdf/nbconvert-7.16.6.tar.gz", hash = "sha256:576a7e37c6480da7b8465eefa66c17844243816ce1ccc372633c6b71c3c0f582", size = 857715, upload-time = "2025-01-28T09:29:14.724Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/9a/cd673b2f773a12c992f41309ef81b99da1690426bd2f96957a7ade0d3ed7/nbconvert-7.16.6-py3-none-any.whl", hash = "sha256:1375a7b67e0c2883678c48e506dc320febb57685e5ee67faa51b18a90f3a712b", size = 258525, upload-time = "2025-01-28T09:29:12.551Z" }, +] + +[[package]] +name = "nbformat" +version = "5.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastjsonschema" }, + { name = "jsonschema" }, + { name = "jupyter-core" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749, upload-time = "2024-04-04T11:20:37.371Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454, upload-time = "2024-04-04T11:20:34.895Z" }, +] + +[[package]] +name = "nbstripout" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nbformat" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/6e/05d7e0e35598bd0d423167295f978005912a2dcd137c88ebf36e34047dc7/nbstripout-0.8.1.tar.gz", hash = "sha256:eaac8b6b4e729e8dfe1e5df2c0f8ba44abc5a17a65448f0480141f80be230bb1", size = 26399, upload-time = "2024-11-17T10:38:33.275Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/91/93b459c456b0e4389b2b3ddb3b82cd401d022691334a0f06e92c2046e780/nbstripout-0.8.1-py2.py3-none-any.whl", hash = "sha256:79a8c8da488d98c54c112fa87185045f0271a97d84f1d46918d6a3ee561b30e7", size = 16329, upload-time = "2024-11-17T10:38:31.803Z" }, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, +] + +[[package]] +name = "notebook" +version = "6.5.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argon2-cffi" }, + { name = "ipykernel" }, + { name = "ipython-genutils" }, + { name = "jinja2" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "nbclassic" }, + { name = "nbconvert" }, + { name = "nbformat" }, + { name = "nest-asyncio" }, + { name = "prometheus-client" }, + { name = "pyzmq" }, + { name = "send2trash" }, + { name = "terminado" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/1e/b555b6e33c962a605e2e85b6014f609d3e1c6a5ff48f7c2480376b430d96/notebook-6.5.4.tar.gz", hash = "sha256:517209568bd47261e2def27a140e97d49070602eea0d226a696f42a7f16c9a4e", size = 5785832, upload-time = "2023-04-06T15:08:15.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/21/0e7683e7c4d51b8f6cc5df9bbd33fb2d1e114b9e5dcddeef96ebd8e86348/notebook-6.5.4-py3-none-any.whl", hash = "sha256:dd17e78aefe64c768737b32bf171c1c766666a21cc79a44d37a1700771cab56f", size = 529822, upload-time = "2023-04-06T15:08:11.457Z" }, +] + +[[package]] +name = "notebook-shim" +version = "0.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-server" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/d2/92fa3243712b9a3e8bafaf60aac366da1cada3639ca767ff4b5b3654ec28/notebook_shim-0.2.4.tar.gz", hash = "sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb", size = 13167, upload-time = "2024-02-14T23:35:18.353Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef", size = 13307, upload-time = "2024-02-14T23:35:16.286Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pandocfilters" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/6f/3dd4940bbe001c06a65f88e36bad298bc7a0de5036115639926b0c5c0458/pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e", size = 8454, upload-time = "2024-01-18T20:08:13.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc", size = 8663, upload-time = "2024-01-18T20:08:11.28Z" }, +] + +[[package]] +name = "parso" +version = "0.8.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/94/68e2e17afaa9169cf6412ab0f28623903be73d1b32e208d9e8e541bb086d/parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d", size = 400609, upload-time = "2024-04-05T09:43:55.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650, upload-time = "2024-04-05T09:43:53.299Z" }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, +] + +[[package]] +name = "prometheus-client" +version = "0.22.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/5a/3fa1fa7e91a203759aaf316be394f70f2ef598d589b9785a8611b6094c00/prometheus_client-0.22.0.tar.gz", hash = "sha256:18da1d2241ac2d10c8d2110f13eedcd5c7c0c8af18c926e8731f04fc10cd575c", size = 74443, upload-time = "2025-05-16T20:50:18.333Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/c7/cee159ba3d7192e84a4c166ec1752f44a5fa859ac0eeda2d73a1da65ab47/prometheus_client-0.22.0-py3-none-any.whl", hash = "sha256:c8951bbe64e62b96cd8e8f5d917279d1b9b91ab766793f33d4dce6c228558713", size = 62658, upload-time = "2025-05-16T20:50:16.978Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.51" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940, upload-time = "2025-04-15T09:18:47.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810, upload-time = "2025-04-15T09:18:44.753Z" }, +] + +[[package]] +name = "propcache" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/16/43264e4a779dd8588c21a70f0709665ee8f611211bdd2c87d952cfa7c776/propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168", size = 44139, upload-time = "2025-06-09T22:56:06.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/14/510deed325e262afeb8b360043c5d7c960da7d3ecd6d6f9496c9c56dc7f4/propcache-0.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:22d9962a358aedbb7a2e36187ff273adeaab9743373a272976d2e348d08c7770", size = 73178, upload-time = "2025-06-09T22:53:40.126Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4e/ad52a7925ff01c1325653a730c7ec3175a23f948f08626a534133427dcff/propcache-0.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0d0fda578d1dc3f77b6b5a5dce3b9ad69a8250a891760a548df850a5e8da87f3", size = 43133, upload-time = "2025-06-09T22:53:41.965Z" }, + { url = "https://files.pythonhosted.org/packages/63/7c/e9399ba5da7780871db4eac178e9c2e204c23dd3e7d32df202092a1ed400/propcache-0.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3def3da3ac3ce41562d85db655d18ebac740cb3fa4367f11a52b3da9d03a5cc3", size = 43039, upload-time = "2025-06-09T22:53:43.268Z" }, + { url = "https://files.pythonhosted.org/packages/22/e1/58da211eb8fdc6fc854002387d38f415a6ca5f5c67c1315b204a5d3e9d7a/propcache-0.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bec58347a5a6cebf239daba9bda37dffec5b8d2ce004d9fe4edef3d2815137e", size = 201903, upload-time = "2025-06-09T22:53:44.872Z" }, + { url = "https://files.pythonhosted.org/packages/c4/0a/550ea0f52aac455cb90111c8bab995208443e46d925e51e2f6ebdf869525/propcache-0.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55ffda449a507e9fbd4aca1a7d9aa6753b07d6166140e5a18d2ac9bc49eac220", size = 213362, upload-time = "2025-06-09T22:53:46.707Z" }, + { url = "https://files.pythonhosted.org/packages/5a/af/9893b7d878deda9bb69fcf54600b247fba7317761b7db11fede6e0f28bd0/propcache-0.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64a67fb39229a8a8491dd42f864e5e263155e729c2e7ff723d6e25f596b1e8cb", size = 210525, upload-time = "2025-06-09T22:53:48.547Z" }, + { url = "https://files.pythonhosted.org/packages/7c/bb/38fd08b278ca85cde36d848091ad2b45954bc5f15cce494bb300b9285831/propcache-0.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da1cf97b92b51253d5b68cf5a2b9e0dafca095e36b7f2da335e27dc6172a614", size = 198283, upload-time = "2025-06-09T22:53:50.067Z" }, + { url = "https://files.pythonhosted.org/packages/78/8c/9fe55bd01d362bafb413dfe508c48753111a1e269737fa143ba85693592c/propcache-0.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5f559e127134b07425134b4065be45b166183fdcb433cb6c24c8e4149056ad50", size = 191872, upload-time = "2025-06-09T22:53:51.438Z" }, + { url = "https://files.pythonhosted.org/packages/54/14/4701c33852937a22584e08abb531d654c8bcf7948a8f87ad0a4822394147/propcache-0.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aff2e4e06435d61f11a428360a932138d0ec288b0a31dd9bd78d200bd4a2b339", size = 199452, upload-time = "2025-06-09T22:53:53.229Z" }, + { url = "https://files.pythonhosted.org/packages/16/44/447f2253d859602095356007657ee535e0093215ea0b3d1d6a41d16e5201/propcache-0.3.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4927842833830942a5d0a56e6f4839bc484785b8e1ce8d287359794818633ba0", size = 191567, upload-time = "2025-06-09T22:53:54.541Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b3/e4756258749bb2d3b46defcff606a2f47410bab82be5824a67e84015b267/propcache-0.3.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6107ddd08b02654a30fb8ad7a132021759d750a82578b94cd55ee2772b6ebea2", size = 193015, upload-time = "2025-06-09T22:53:56.44Z" }, + { url = "https://files.pythonhosted.org/packages/1e/df/e6d3c7574233164b6330b9fd697beeac402afd367280e6dc377bb99b43d9/propcache-0.3.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:70bd8b9cd6b519e12859c99f3fc9a93f375ebd22a50296c3a295028bea73b9e7", size = 204660, upload-time = "2025-06-09T22:53:57.839Z" }, + { url = "https://files.pythonhosted.org/packages/b2/53/e4d31dd5170b4a0e2e6b730f2385a96410633b4833dc25fe5dffd1f73294/propcache-0.3.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2183111651d710d3097338dd1893fcf09c9f54e27ff1a8795495a16a469cc90b", size = 206105, upload-time = "2025-06-09T22:53:59.638Z" }, + { url = "https://files.pythonhosted.org/packages/7f/fe/74d54cf9fbe2a20ff786e5f7afcfde446588f0cf15fb2daacfbc267b866c/propcache-0.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fb075ad271405dcad8e2a7ffc9a750a3bf70e533bd86e89f0603e607b93aa64c", size = 196980, upload-time = "2025-06-09T22:54:01.071Z" }, + { url = "https://files.pythonhosted.org/packages/22/ec/c469c9d59dada8a7679625e0440b544fe72e99311a4679c279562051f6fc/propcache-0.3.2-cp310-cp310-win32.whl", hash = "sha256:404d70768080d3d3bdb41d0771037da19d8340d50b08e104ca0e7f9ce55fce70", size = 37679, upload-time = "2025-06-09T22:54:03.003Z" }, + { url = "https://files.pythonhosted.org/packages/38/35/07a471371ac89d418f8d0b699c75ea6dca2041fbda360823de21f6a9ce0a/propcache-0.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:7435d766f978b4ede777002e6b3b6641dd229cd1da8d3d3106a45770365f9ad9", size = 41459, upload-time = "2025-06-09T22:54:04.134Z" }, + { url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663, upload-time = "2025-06-09T22:56:04.484Z" }, +] + +[[package]] +name = "psutil" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload-time = "2025-02-13T21:54:07.946Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload-time = "2025-02-13T21:54:12.36Z" }, + { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload-time = "2025-02-13T21:54:16.07Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload-time = "2025-02-13T21:54:18.662Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload-time = "2025-02-13T21:54:21.811Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload-time = "2025-02-13T21:54:24.68Z" }, + { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload-time = "2025-02-13T21:54:34.31Z" }, + { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, +] + +[[package]] +name = "python-datauri" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cached-property" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/3b/8a9a2ec12424a8617678d663fa70de43d917d3590416d3a2b9c7fc065d5b/python_datauri-3.0.2.tar.gz", hash = "sha256:d77c37f1f734fc035de424e643464990b2b840e9b8c7c1817c11fca19b71eeb7", size = 9746, upload-time = "2025-01-03T16:22:56.143Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/b6/3332df034d7f322506f2267517b051cd3605e129ecc7f9d46a6fbd540279/python_datauri-3.0.2-py2.py3-none-any.whl", hash = "sha256:b365690a1d7d1b7777009eb11a86bd069db4f194e50f4f871a47302f0587c144", size = 5803, upload-time = "2025-01-03T16:22:53.899Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pywin32" +version = "310" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/da/a5f38fffbba2fb99aa4aa905480ac4b8e83ca486659ac8c95bce47fb5276/pywin32-310-cp310-cp310-win32.whl", hash = "sha256:6dd97011efc8bf51d6793a82292419eba2c71cf8e7250cfac03bba284454abc1", size = 8848240, upload-time = "2025-03-17T00:55:46.783Z" }, + { url = "https://files.pythonhosted.org/packages/aa/fe/d873a773324fa565619ba555a82c9dabd677301720f3660a731a5d07e49a/pywin32-310-cp310-cp310-win_amd64.whl", hash = "sha256:c3e78706e4229b915a0821941a84e7ef420bf2b77e08c9dae3c76fd03fd2ae3d", size = 9601854, upload-time = "2025-03-17T00:55:48.783Z" }, + { url = "https://files.pythonhosted.org/packages/3c/84/1a8e3d7a15490d28a5d816efa229ecb4999cdc51a7c30dd8914f669093b8/pywin32-310-cp310-cp310-win_arm64.whl", hash = "sha256:33babed0cf0c92a6f94cc6cc13546ab24ee13e3e800e61ed87609ab91e4c8213", size = 8522963, upload-time = "2025-03-17T00:55:50.969Z" }, +] + +[[package]] +name = "pywinpty" +version = "2.0.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/7c/917f9c4681bb8d34bfbe0b79d36bbcd902651aeab48790df3d30ba0202fb/pywinpty-2.0.15.tar.gz", hash = "sha256:312cf39153a8736c617d45ce8b6ad6cd2107de121df91c455b10ce6bba7a39b2", size = 29017, upload-time = "2025-02-03T21:53:23.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/b7/855db919ae526d2628f3f2e6c281c4cdff7a9a8af51bb84659a9f07b1861/pywinpty-2.0.15-cp310-cp310-win_amd64.whl", hash = "sha256:8e7f5de756a615a38b96cd86fa3cd65f901ce54ce147a3179c45907fa11b4c4e", size = 1405161, upload-time = "2025-02-03T21:56:25.008Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, +] + +[[package]] +name = "pyzmq" +version = "26.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/11/b9213d25230ac18a71b39b3723494e57adebe36e066397b961657b3b41c1/pyzmq-26.4.0.tar.gz", hash = "sha256:4bd13f85f80962f91a651a7356fe0472791a5f7a92f227822b5acf44795c626d", size = 278293, upload-time = "2025-04-04T12:05:44.049Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/b8/af1d814ffc3ff9730f9a970cbf216b6f078e5d251a25ef5201d7bc32a37c/pyzmq-26.4.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:0329bdf83e170ac133f44a233fc651f6ed66ef8e66693b5af7d54f45d1ef5918", size = 1339238, upload-time = "2025-04-04T12:03:07.022Z" }, + { url = "https://files.pythonhosted.org/packages/ee/e4/5aafed4886c264f2ea6064601ad39c5fc4e9b6539c6ebe598a859832eeee/pyzmq-26.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:398a825d2dea96227cf6460ce0a174cf7657d6f6827807d4d1ae9d0f9ae64315", size = 672848, upload-time = "2025-04-04T12:03:08.591Z" }, + { url = "https://files.pythonhosted.org/packages/79/39/026bf49c721cb42f1ef3ae0ee3d348212a7621d2adb739ba97599b6e4d50/pyzmq-26.4.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d52d62edc96787f5c1dfa6c6ccff9b581cfae5a70d94ec4c8da157656c73b5b", size = 911299, upload-time = "2025-04-04T12:03:10Z" }, + { url = "https://files.pythonhosted.org/packages/03/23/b41f936a9403b8f92325c823c0f264c6102a0687a99c820f1aaeb99c1def/pyzmq-26.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1410c3a3705db68d11eb2424d75894d41cff2f64d948ffe245dd97a9debfebf4", size = 867920, upload-time = "2025-04-04T12:03:11.311Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3e/2de5928cdadc2105e7c8f890cc5f404136b41ce5b6eae5902167f1d5641c/pyzmq-26.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:7dacb06a9c83b007cc01e8e5277f94c95c453c5851aac5e83efe93e72226353f", size = 862514, upload-time = "2025-04-04T12:03:13.013Z" }, + { url = "https://files.pythonhosted.org/packages/ce/57/109569514dd32e05a61d4382bc88980c95bfd2f02e58fea47ec0ccd96de1/pyzmq-26.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6bab961c8c9b3a4dc94d26e9b2cdf84de9918931d01d6ff38c721a83ab3c0ef5", size = 1204494, upload-time = "2025-04-04T12:03:14.795Z" }, + { url = "https://files.pythonhosted.org/packages/aa/02/dc51068ff2ca70350d1151833643a598625feac7b632372d229ceb4de3e1/pyzmq-26.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7a5c09413b924d96af2aa8b57e76b9b0058284d60e2fc3730ce0f979031d162a", size = 1514525, upload-time = "2025-04-04T12:03:16.246Z" }, + { url = "https://files.pythonhosted.org/packages/48/2a/a7d81873fff0645eb60afaec2b7c78a85a377af8f1d911aff045d8955bc7/pyzmq-26.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7d489ac234d38e57f458fdbd12a996bfe990ac028feaf6f3c1e81ff766513d3b", size = 1414659, upload-time = "2025-04-04T12:03:17.652Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ea/813af9c42ae21845c1ccfe495bd29c067622a621e85d7cda6bc437de8101/pyzmq-26.4.0-cp310-cp310-win32.whl", hash = "sha256:dea1c8db78fb1b4b7dc9f8e213d0af3fc8ecd2c51a1d5a3ca1cde1bda034a980", size = 580348, upload-time = "2025-04-04T12:03:19.384Z" }, + { url = "https://files.pythonhosted.org/packages/20/68/318666a89a565252c81d3fed7f3b4c54bd80fd55c6095988dfa2cd04a62b/pyzmq-26.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:fa59e1f5a224b5e04dc6c101d7186058efa68288c2d714aa12d27603ae93318b", size = 643838, upload-time = "2025-04-04T12:03:20.795Z" }, + { url = "https://files.pythonhosted.org/packages/91/f8/fb1a15b5f4ecd3e588bfde40c17d32ed84b735195b5c7d1d7ce88301a16f/pyzmq-26.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:a651fe2f447672f4a815e22e74630b6b1ec3a1ab670c95e5e5e28dcd4e69bbb5", size = 559565, upload-time = "2025-04-04T12:03:22.676Z" }, + { url = "https://files.pythonhosted.org/packages/47/03/96004704a84095f493be8d2b476641f5c967b269390173f85488a53c1c13/pyzmq-26.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:98d948288ce893a2edc5ec3c438fe8de2daa5bbbd6e2e865ec5f966e237084ba", size = 834408, upload-time = "2025-04-04T12:05:04.569Z" }, + { url = "https://files.pythonhosted.org/packages/e4/7f/68d8f3034a20505db7551cb2260248be28ca66d537a1ac9a257913d778e4/pyzmq-26.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9f34f5c9e0203ece706a1003f1492a56c06c0632d86cb77bcfe77b56aacf27b", size = 569580, upload-time = "2025-04-04T12:05:06.283Z" }, + { url = "https://files.pythonhosted.org/packages/9b/a6/2b0d6801ec33f2b2a19dd8d02e0a1e8701000fec72926e6787363567d30c/pyzmq-26.4.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80c9b48aef586ff8b698359ce22f9508937c799cc1d2c9c2f7c95996f2300c94", size = 798250, upload-time = "2025-04-04T12:05:07.88Z" }, + { url = "https://files.pythonhosted.org/packages/96/2a/0322b3437de977dcac8a755d6d7ce6ec5238de78e2e2d9353730b297cf12/pyzmq-26.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3f2a5b74009fd50b53b26f65daff23e9853e79aa86e0aa08a53a7628d92d44a", size = 756758, upload-time = "2025-04-04T12:05:09.483Z" }, + { url = "https://files.pythonhosted.org/packages/c2/33/43704f066369416d65549ccee366cc19153911bec0154da7c6b41fca7e78/pyzmq-26.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:61c5f93d7622d84cb3092d7f6398ffc77654c346545313a3737e266fc11a3beb", size = 555371, upload-time = "2025-04-04T12:05:11.062Z" }, +] + +[[package]] +name = "referencing" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, +] + +[[package]] +name = "requests" +version = "2.29.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/d2/70fc708727b62d55bc24e43cc85f073039023212d482553d853c44e57bdb/requests-2.29.0.tar.gz", hash = "sha256:f2e34a75f4749019bb0e3effb66683630e4ffeaf75819fb51bebef1bf5aef059", size = 108279, upload-time = "2023-04-26T15:24:34.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/e1/2aa539876d9ed0ddc95882451deb57cfd7aa8dbf0b8dbce68e045549ba56/requests-2.29.0-py3-none-any.whl", hash = "sha256:e8f3c9be120d3333921d213eef078af392fba3933ab7ed2d1cba3b56f2568c3b", size = 62499, upload-time = "2023-04-26T15:24:31.555Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/a6/60184b7fc00dd3ca80ac635dd5b8577d444c57e8e8742cecabfacb829921/rpds_py-0.25.1.tar.gz", hash = "sha256:8960b6dac09b62dac26e75d7e2c4a22efb835d827a7278c34f72b2b84fa160e3", size = 27304, upload-time = "2025-05-21T12:46:12.502Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/09/e1158988e50905b7f8306487a576b52d32aa9a87f79f7ab24ee8db8b6c05/rpds_py-0.25.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:f4ad628b5174d5315761b67f212774a32f5bad5e61396d38108bd801c0a8f5d9", size = 373140, upload-time = "2025-05-21T12:42:38.834Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4b/a284321fb3c45c02fc74187171504702b2934bfe16abab89713eedfe672e/rpds_py-0.25.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8c742af695f7525e559c16f1562cf2323db0e3f0fbdcabdf6865b095256b2d40", size = 358860, upload-time = "2025-05-21T12:42:41.394Z" }, + { url = "https://files.pythonhosted.org/packages/4e/46/8ac9811150c75edeae9fc6fa0e70376c19bc80f8e1f7716981433905912b/rpds_py-0.25.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:605ffe7769e24b1800b4d024d24034405d9404f0bc2f55b6db3362cd34145a6f", size = 386179, upload-time = "2025-05-21T12:42:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ec/87eb42d83e859bce91dcf763eb9f2ab117142a49c9c3d17285440edb5b69/rpds_py-0.25.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ccc6f3ddef93243538be76f8e47045b4aad7a66a212cd3a0f23e34469473d36b", size = 400282, upload-time = "2025-05-21T12:42:44.92Z" }, + { url = "https://files.pythonhosted.org/packages/68/c8/2a38e0707d7919c8c78e1d582ab15cf1255b380bcb086ca265b73ed6db23/rpds_py-0.25.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f70316f760174ca04492b5ab01be631a8ae30cadab1d1081035136ba12738cfa", size = 521824, upload-time = "2025-05-21T12:42:46.856Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2c/6a92790243569784dde84d144bfd12bd45102f4a1c897d76375076d730ab/rpds_py-0.25.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1dafef8df605fdb46edcc0bf1573dea0d6d7b01ba87f85cd04dc855b2b4479e", size = 411644, upload-time = "2025-05-21T12:42:48.838Z" }, + { url = "https://files.pythonhosted.org/packages/eb/76/66b523ffc84cf47db56efe13ae7cf368dee2bacdec9d89b9baca5e2e6301/rpds_py-0.25.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0701942049095741a8aeb298a31b203e735d1c61f4423511d2b1a41dcd8a16da", size = 386955, upload-time = "2025-05-21T12:42:50.835Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b9/a362d7522feaa24dc2b79847c6175daa1c642817f4a19dcd5c91d3e2c316/rpds_py-0.25.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e87798852ae0b37c88babb7f7bbbb3e3fecc562a1c340195b44c7e24d403e380", size = 421039, upload-time = "2025-05-21T12:42:52.348Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c4/b5b6f70b4d719b6584716889fd3413102acf9729540ee76708d56a76fa97/rpds_py-0.25.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3bcce0edc1488906c2d4c75c94c70a0417e83920dd4c88fec1078c94843a6ce9", size = 563290, upload-time = "2025-05-21T12:42:54.404Z" }, + { url = "https://files.pythonhosted.org/packages/87/a3/2e6e816615c12a8f8662c9d8583a12eb54c52557521ef218cbe3095a8afa/rpds_py-0.25.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e2f6a2347d3440ae789505693a02836383426249d5293541cd712e07e7aecf54", size = 592089, upload-time = "2025-05-21T12:42:55.976Z" }, + { url = "https://files.pythonhosted.org/packages/c0/08/9b8e1050e36ce266135994e2c7ec06e1841f1c64da739daeb8afe9cb77a4/rpds_py-0.25.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4fd52d3455a0aa997734f3835cbc4c9f32571345143960e7d7ebfe7b5fbfa3b2", size = 558400, upload-time = "2025-05-21T12:42:58.032Z" }, + { url = "https://files.pythonhosted.org/packages/f2/df/b40b8215560b8584baccd839ff5c1056f3c57120d79ac41bd26df196da7e/rpds_py-0.25.1-cp310-cp310-win32.whl", hash = "sha256:3f0b1798cae2bbbc9b9db44ee068c556d4737911ad53a4e5093d09d04b3bbc24", size = 219741, upload-time = "2025-05-21T12:42:59.479Z" }, + { url = "https://files.pythonhosted.org/packages/10/99/e4c58be18cf5d8b40b8acb4122bc895486230b08f978831b16a3916bd24d/rpds_py-0.25.1-cp310-cp310-win_amd64.whl", hash = "sha256:3ebd879ab996537fc510a2be58c59915b5dd63bccb06d1ef514fee787e05984a", size = 231553, upload-time = "2025-05-21T12:43:01.425Z" }, + { url = "https://files.pythonhosted.org/packages/78/ff/566ce53529b12b4f10c0a348d316bd766970b7060b4fd50f888be3b3b281/rpds_py-0.25.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b24bf3cd93d5b6ecfbedec73b15f143596c88ee249fa98cefa9a9dc9d92c6f28", size = 373931, upload-time = "2025-05-21T12:45:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/83/5d/deba18503f7c7878e26aa696e97f051175788e19d5336b3b0e76d3ef9256/rpds_py-0.25.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:0eb90e94f43e5085623932b68840b6f379f26db7b5c2e6bcef3179bd83c9330f", size = 359074, upload-time = "2025-05-21T12:45:06.714Z" }, + { url = "https://files.pythonhosted.org/packages/0d/74/313415c5627644eb114df49c56a27edba4d40cfd7c92bd90212b3604ca84/rpds_py-0.25.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d50e4864498a9ab639d6d8854b25e80642bd362ff104312d9770b05d66e5fb13", size = 387255, upload-time = "2025-05-21T12:45:08.669Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c8/c723298ed6338963d94e05c0f12793acc9b91d04ed7c4ba7508e534b7385/rpds_py-0.25.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c9409b47ba0650544b0bb3c188243b83654dfe55dcc173a86832314e1a6a35d", size = 400714, upload-time = "2025-05-21T12:45:10.39Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/51f1f6aa653c2e110ed482ef2ae94140d56c910378752a1b483af11019ee/rpds_py-0.25.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:796ad874c89127c91970652a4ee8b00d56368b7e00d3477f4415fe78164c8000", size = 523105, upload-time = "2025-05-21T12:45:12.273Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a4/7873d15c088ad3bff36910b29ceb0f178e4b3232c2adbe9198de68a41e63/rpds_py-0.25.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:85608eb70a659bf4c1142b2781083d4b7c0c4e2c90eff11856a9754e965b2540", size = 411499, upload-time = "2025-05-21T12:45:13.95Z" }, + { url = "https://files.pythonhosted.org/packages/90/f3/0ce1437befe1410766d11d08239333ac1b2d940f8a64234ce48a7714669c/rpds_py-0.25.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4feb9211d15d9160bc85fa72fed46432cdc143eb9cf6d5ca377335a921ac37b", size = 387918, upload-time = "2025-05-21T12:45:15.649Z" }, + { url = "https://files.pythonhosted.org/packages/94/d4/5551247988b2a3566afb8a9dba3f1d4a3eea47793fd83000276c1a6c726e/rpds_py-0.25.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ccfa689b9246c48947d31dd9d8b16d89a0ecc8e0e26ea5253068efb6c542b76e", size = 421705, upload-time = "2025-05-21T12:45:17.788Z" }, + { url = "https://files.pythonhosted.org/packages/b0/25/5960f28f847bf736cc7ee3c545a7e1d2f3b5edaf82c96fb616c2f5ed52d0/rpds_py-0.25.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:3c5b317ecbd8226887994852e85de562f7177add602514d4ac40f87de3ae45a8", size = 564489, upload-time = "2025-05-21T12:45:19.466Z" }, + { url = "https://files.pythonhosted.org/packages/02/66/1c99884a0d44e8c2904d3c4ec302f995292d5dde892c3bf7685ac1930146/rpds_py-0.25.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:454601988aab2c6e8fd49e7634c65476b2b919647626208e376afcd22019eeb8", size = 592557, upload-time = "2025-05-21T12:45:21.362Z" }, + { url = "https://files.pythonhosted.org/packages/55/ae/4aeac84ebeffeac14abb05b3bb1d2f728d00adb55d3fb7b51c9fa772e760/rpds_py-0.25.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:1c0c434a53714358532d13539272db75a5ed9df75a4a090a753ac7173ec14e11", size = 558691, upload-time = "2025-05-21T12:45:23.084Z" }, + { url = "https://files.pythonhosted.org/packages/41/b3/728a08ff6f5e06fe3bb9af2e770e9d5fd20141af45cff8dfc62da4b2d0b3/rpds_py-0.25.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f73ce1512e04fbe2bc97836e89830d6b4314c171587a99688082d090f934d20a", size = 231651, upload-time = "2025-05-21T12:45:24.72Z" }, +] + +[[package]] +name = "send2trash" +version = "1.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/3a/aec9b02217bb79b87bbc1a21bc6abc51e3d5dcf65c30487ac96c0908c722/Send2Trash-1.8.3.tar.gz", hash = "sha256:b18e7a3966d99871aefeb00cfbcfdced55ce4871194810fc71f4aa484b953abf", size = 17394, upload-time = "2024-04-07T00:01:09.267Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/b0/4562db6223154aa4e22f939003cb92514c79f3d4dccca3444253fd17f902/Send2Trash-1.8.3-py3-none-any.whl", hash = "sha256:0c31227e0bd08961c7665474a3d1ef7193929fedda4233843689baa056be46c9", size = 18072, upload-time = "2024-04-07T00:01:07.438Z" }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, +] + +[[package]] +name = "simpervisor" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dd/fc/9182a4049036c5de29f84a16c5a33304ffc4dbb06d76d569ded8ad527574/simpervisor-1.0.0.tar.gz", hash = "sha256:7eb87ca86d5e276976f5bb0290975a05d452c6a7b7f58062daea7d8369c823c1", size = 14637, upload-time = "2023-05-18T14:01:27.069Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/65/be223a02df814a3dbd84d8a0c446d21d4860a4f23ec4d81aabea34e7e994/simpervisor-1.0.0-py3-none-any.whl", hash = "sha256:3e313318264559beea3f475ead202bc1cd58a2f1288363abb5657d306c5b8388", size = 8342, upload-time = "2023-05-18T14:01:25.92Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418, upload-time = "2025-04-20T18:50:08.518Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677, upload-time = "2025-04-20T18:50:07.196Z" }, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, +] + +[[package]] +name = "terminado" +version = "0.18.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess", marker = "os_name != 'nt'" }, + { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "tornado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/11/965c6fd8e5cc254f1fe142d547387da17a8ebfd75a3455f637c663fb38a0/terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e", size = 32701, upload-time = "2024-03-12T14:34:39.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl", hash = "sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0", size = 14154, upload-time = "2024-03-12T14:34:36.569Z" }, +] + +[[package]] +name = "terra-base" +version = "1.0.0" +source = { virtual = "." } +dependencies = [ + { name = "ipykernel" }, + { name = "jupyter-contrib-nbextensions" }, + { name = "jupyter-core" }, + { name = "jupyter-server" }, + { name = "jupyter-server-proxy" }, + { name = "jupyterlab" }, + { name = "jupyterlab-server" }, + { name = "nbclassic" }, + { name = "nbconvert" }, + { name = "nbstripout" }, + { name = "notebook" }, + { name = "python-datauri" }, + { name = "requests" }, + { name = "tornado" }, + { name = "traitlets" }, +] + +[package.metadata] +requires-dist = [ + { name = "ipykernel", specifier = ">=6.29.5" }, + { name = "jupyter-contrib-nbextensions", specifier = "==0.7.0" }, + { name = "jupyter-core", specifier = "==5.3.1" }, + { name = "jupyter-server", specifier = "==1.24.0" }, + { name = "jupyter-server-proxy", specifier = "==4.0.0" }, + { name = "jupyterlab", specifier = "==3.4.8" }, + { name = "jupyterlab-server", specifier = "==2.23.0" }, + { name = "nbclassic", specifier = ">=1.3.1" }, + { name = "nbconvert", specifier = ">=7.16.6" }, + { name = "nbstripout", specifier = ">=0.8.1" }, + { name = "notebook", specifier = "==6.5.4" }, + { name = "python-datauri", specifier = ">=3.0.2" }, + { name = "requests", specifier = ">=2.29.0" }, + { name = "tornado", specifier = ">=6.5.1" }, + { name = "traitlets", specifier = "==5.9.0" }, +] + +[[package]] +name = "tinycss2" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/5a/576828164b5486f319c4323915b915a8af3fa4a654bbb6f8fc8e87b5cb17/tinycss2-1.1.1.tar.gz", hash = "sha256:b2e44dd8883c360c35dd0d1b5aad0b610e5156c2cb3b33434634e539ead9d8bf", size = 65703, upload-time = "2021-11-22T17:59:14.235Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/7b/5dba39bf0572f1f28e2844f08f74a73482a381de1d1feac3bbc6b808051e/tinycss2-1.1.1-py3-none-any.whl", hash = "sha256:fe794ceaadfe3cf3e686b22155d0da5780dd0e273471a51846d0a02bc204fec8", size = 21883, upload-time = "2021-11-22T17:59:08.603Z" }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, +] + +[[package]] +name = "tornado" +version = "6.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/89/c72771c81d25d53fe33e3dca61c233b665b2780f21820ba6fd2c6793c12b/tornado-6.5.1.tar.gz", hash = "sha256:84ceece391e8eb9b2b95578db65e920d2a61070260594819589609ba9bc6308c", size = 509934, upload-time = "2025-05-22T18:15:38.788Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/89/f4532dee6843c9e0ebc4e28d4be04c67f54f60813e4bf73d595fe7567452/tornado-6.5.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d50065ba7fd11d3bd41bcad0825227cc9a95154bad83239357094c36708001f7", size = 441948, upload-time = "2025-05-22T18:15:20.862Z" }, + { url = "https://files.pythonhosted.org/packages/15/9a/557406b62cffa395d18772e0cdcf03bed2fff03b374677348eef9f6a3792/tornado-6.5.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9e9ca370f717997cb85606d074b0e5b247282cf5e2e1611568b8821afe0342d6", size = 440112, upload-time = "2025-05-22T18:15:22.591Z" }, + { url = "https://files.pythonhosted.org/packages/55/82/7721b7319013a3cf881f4dffa4f60ceff07b31b394e459984e7a36dc99ec/tornado-6.5.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b77e9dfa7ed69754a54c89d82ef746398be82f749df69c4d3abe75c4d1ff4888", size = 443672, upload-time = "2025-05-22T18:15:24.027Z" }, + { url = "https://files.pythonhosted.org/packages/7d/42/d11c4376e7d101171b94e03cef0cbce43e823ed6567ceda571f54cf6e3ce/tornado-6.5.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:253b76040ee3bab8bcf7ba9feb136436a3787208717a1fb9f2c16b744fba7331", size = 443019, upload-time = "2025-05-22T18:15:25.735Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f7/0c48ba992d875521ac761e6e04b0a1750f8150ae42ea26df1852d6a98942/tornado-6.5.1-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:308473f4cc5a76227157cdf904de33ac268af770b2c5f05ca6c1161d82fdd95e", size = 443252, upload-time = "2025-05-22T18:15:27.499Z" }, + { url = "https://files.pythonhosted.org/packages/89/46/d8d7413d11987e316df4ad42e16023cd62666a3c0dfa1518ffa30b8df06c/tornado-6.5.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:caec6314ce8a81cf69bd89909f4b633b9f523834dc1a352021775d45e51d9401", size = 443930, upload-time = "2025-05-22T18:15:29.299Z" }, + { url = "https://files.pythonhosted.org/packages/78/b2/f8049221c96a06df89bed68260e8ca94beca5ea532ffc63b1175ad31f9cc/tornado-6.5.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:13ce6e3396c24e2808774741331638ee6c2f50b114b97a55c5b442df65fd9692", size = 443351, upload-time = "2025-05-22T18:15:31.038Z" }, + { url = "https://files.pythonhosted.org/packages/76/ff/6a0079e65b326cc222a54720a748e04a4db246870c4da54ece4577bfa702/tornado-6.5.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5cae6145f4cdf5ab24744526cc0f55a17d76f02c98f4cff9daa08ae9a217448a", size = 443328, upload-time = "2025-05-22T18:15:32.426Z" }, + { url = "https://files.pythonhosted.org/packages/49/18/e3f902a1d21f14035b5bc6246a8c0f51e0eef562ace3a2cea403c1fb7021/tornado-6.5.1-cp39-abi3-win32.whl", hash = "sha256:e0a36e1bc684dca10b1aa75a31df8bdfed656831489bc1e6a6ebed05dc1ec365", size = 444396, upload-time = "2025-05-22T18:15:34.205Z" }, + { url = "https://files.pythonhosted.org/packages/7b/09/6526e32bf1049ee7de3bebba81572673b19a2a8541f795d887e92af1a8bc/tornado-6.5.1-cp39-abi3-win_amd64.whl", hash = "sha256:908e7d64567cecd4c2b458075589a775063453aeb1d2a1853eedb806922f568b", size = 444840, upload-time = "2025-05-22T18:15:36.1Z" }, + { url = "https://files.pythonhosted.org/packages/55/a7/535c44c7bea4578e48281d83c615219f3ab19e6abc67625ef637c73987be/tornado-6.5.1-cp39-abi3-win_arm64.whl", hash = "sha256:02420a0eb7bf617257b9935e2b754d1b63897525d8a289c9d65690d580b4dcf7", size = 443596, upload-time = "2025-05-22T18:15:37.433Z" }, +] + +[[package]] +name = "traitlets" +version = "5.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/39/c3/205e88f02959712b62008502952707313640369144a7fded4cbc61f48321/traitlets-5.9.0.tar.gz", hash = "sha256:f6cde21a9c68cf756af02035f72d5a723bf607e862e7be33ece505abf4a3bad9", size = 150207, upload-time = "2023-01-30T14:15:59.636Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/75/c28e9ef7abec2b7e9ff35aea3e0be6c1aceaf7873c26c95ae1f0d594de71/traitlets-5.9.0-py3-none-any.whl", hash = "sha256:9e6ec080259b9a5940c797d58b613b5e31441c2257b87c2e795c5228ae80d2d8", size = 117376, upload-time = "2023-01-30T14:15:57.273Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, +] + +[[package]] +name = "urllib3" +version = "1.26.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c8/93/65e479b023bbc46dab3e092bda6b0005424ea3217d711964ccdede3f9b1b/urllib3-1.26.19.tar.gz", hash = "sha256:3e3d753a8618b86d7de333b4223005f68720bcd6a7d2bcb9fbd2229ec7c1e429", size = 306068, upload-time = "2024-06-17T14:53:34.424Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/6a/99eaaeae8becaa17a29aeb334a18e5d582d873b6f084c11f02581b8d7f7f/urllib3-1.26.19-py2.py3-none-any.whl", hash = "sha256:37a0344459b199fce0e80b0d3569837ec6b6937435c5244e7fd73fa6006830f3", size = 143933, upload-time = "2024-06-17T14:53:31.589Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.2.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301, upload-time = "2024-01-06T02:10:57.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" }, +] + +[[package]] +name = "webencodings" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, +] + +[[package]] +name = "websocket-client" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/30/fba0d96b4b5fbf5948ed3f4681f7da2f9f64512e1d303f94b4cc174c24a5/websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da", size = 54648, upload-time = "2024-04-23T22:16:16.976Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826, upload-time = "2024-04-23T22:16:14.422Z" }, +] + +[[package]] +name = "yarl" +version = "1.20.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/fb/efaa23fa4e45537b827620f04cf8f3cd658b76642205162e072703a5b963/yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac", size = 186428, upload-time = "2025-06-10T00:46:09.923Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/65/7fed0d774abf47487c64be14e9223749468922817b5e8792b8a64792a1bb/yarl-1.20.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6032e6da6abd41e4acda34d75a816012717000fa6839f37124a47fcefc49bec4", size = 132910, upload-time = "2025-06-10T00:42:31.108Z" }, + { url = "https://files.pythonhosted.org/packages/8a/7b/988f55a52da99df9e56dc733b8e4e5a6ae2090081dc2754fc8fd34e60aa0/yarl-1.20.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2c7b34d804b8cf9b214f05015c4fee2ebe7ed05cf581e7192c06555c71f4446a", size = 90644, upload-time = "2025-06-10T00:42:33.851Z" }, + { url = "https://files.pythonhosted.org/packages/f7/de/30d98f03e95d30c7e3cc093759982d038c8833ec2451001d45ef4854edc1/yarl-1.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0c869f2651cc77465f6cd01d938d91a11d9ea5d798738c1dc077f3de0b5e5fed", size = 89322, upload-time = "2025-06-10T00:42:35.688Z" }, + { url = "https://files.pythonhosted.org/packages/e0/7a/f2f314f5ebfe9200724b0b748de2186b927acb334cf964fd312eb86fc286/yarl-1.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62915e6688eb4d180d93840cda4110995ad50c459bf931b8b3775b37c264af1e", size = 323786, upload-time = "2025-06-10T00:42:37.817Z" }, + { url = "https://files.pythonhosted.org/packages/15/3f/718d26f189db96d993d14b984ce91de52e76309d0fd1d4296f34039856aa/yarl-1.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:41ebd28167bc6af8abb97fec1a399f412eec5fd61a3ccbe2305a18b84fb4ca73", size = 319627, upload-time = "2025-06-10T00:42:39.937Z" }, + { url = "https://files.pythonhosted.org/packages/a5/76/8fcfbf5fa2369157b9898962a4a7d96764b287b085b5b3d9ffae69cdefd1/yarl-1.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21242b4288a6d56f04ea193adde174b7e347ac46ce6bc84989ff7c1b1ecea84e", size = 339149, upload-time = "2025-06-10T00:42:42.627Z" }, + { url = "https://files.pythonhosted.org/packages/3c/95/d7fc301cc4661785967acc04f54a4a42d5124905e27db27bb578aac49b5c/yarl-1.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bea21cdae6c7eb02ba02a475f37463abfe0a01f5d7200121b03e605d6a0439f8", size = 333327, upload-time = "2025-06-10T00:42:44.842Z" }, + { url = "https://files.pythonhosted.org/packages/65/94/e21269718349582eee81efc5c1c08ee71c816bfc1585b77d0ec3f58089eb/yarl-1.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f8a891e4a22a89f5dde7862994485e19db246b70bb288d3ce73a34422e55b23", size = 326054, upload-time = "2025-06-10T00:42:47.149Z" }, + { url = "https://files.pythonhosted.org/packages/32/ae/8616d1f07853704523519f6131d21f092e567c5af93de7e3e94b38d7f065/yarl-1.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd803820d44c8853a109a34e3660e5a61beae12970da479cf44aa2954019bf70", size = 315035, upload-time = "2025-06-10T00:42:48.852Z" }, + { url = "https://files.pythonhosted.org/packages/48/aa/0ace06280861ef055855333707db5e49c6e3a08840a7ce62682259d0a6c0/yarl-1.20.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b982fa7f74c80d5c0c7b5b38f908971e513380a10fecea528091405f519b9ebb", size = 338962, upload-time = "2025-06-10T00:42:51.024Z" }, + { url = "https://files.pythonhosted.org/packages/20/52/1e9d0e6916f45a8fb50e6844f01cb34692455f1acd548606cbda8134cd1e/yarl-1.20.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:33f29ecfe0330c570d997bcf1afd304377f2e48f61447f37e846a6058a4d33b2", size = 335399, upload-time = "2025-06-10T00:42:53.007Z" }, + { url = "https://files.pythonhosted.org/packages/f2/65/60452df742952c630e82f394cd409de10610481d9043aa14c61bf846b7b1/yarl-1.20.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:835ab2cfc74d5eb4a6a528c57f05688099da41cf4957cf08cad38647e4a83b30", size = 338649, upload-time = "2025-06-10T00:42:54.964Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f5/6cd4ff38dcde57a70f23719a838665ee17079640c77087404c3d34da6727/yarl-1.20.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:46b5e0ccf1943a9a6e766b2c2b8c732c55b34e28be57d8daa2b3c1d1d4009309", size = 358563, upload-time = "2025-06-10T00:42:57.28Z" }, + { url = "https://files.pythonhosted.org/packages/d1/90/c42eefd79d0d8222cb3227bdd51b640c0c1d0aa33fe4cc86c36eccba77d3/yarl-1.20.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:df47c55f7d74127d1b11251fe6397d84afdde0d53b90bedb46a23c0e534f9d24", size = 357609, upload-time = "2025-06-10T00:42:59.055Z" }, + { url = "https://files.pythonhosted.org/packages/03/c8/cea6b232cb4617514232e0f8a718153a95b5d82b5290711b201545825532/yarl-1.20.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76d12524d05841276b0e22573f28d5fbcb67589836772ae9244d90dd7d66aa13", size = 350224, upload-time = "2025-06-10T00:43:01.248Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a3/eaa0ab9712f1f3d01faf43cf6f1f7210ce4ea4a7e9b28b489a2261ca8db9/yarl-1.20.1-cp310-cp310-win32.whl", hash = "sha256:6c4fbf6b02d70e512d7ade4b1f998f237137f1417ab07ec06358ea04f69134f8", size = 81753, upload-time = "2025-06-10T00:43:03.486Z" }, + { url = "https://files.pythonhosted.org/packages/8f/34/e4abde70a9256465fe31c88ed02c3f8502b7b5dead693a4f350a06413f28/yarl-1.20.1-cp310-cp310-win_amd64.whl", hash = "sha256:aef6c4d69554d44b7f9d923245f8ad9a707d971e6209d51279196d8e8fe1ae16", size = 86817, upload-time = "2025-06-10T00:43:05.231Z" }, + { url = "https://files.pythonhosted.org/packages/b4/2d/2345fce04cfd4bee161bf1e7d9cdc702e3e16109021035dbb24db654a622/yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77", size = 46542, upload-time = "2025-06-10T00:46:07.521Z" }, +] diff --git a/terra-jupyter-bioconductor/tests/smoke_test.ipynb b/terra-jupyter-bioconductor/tests/smoke_test.ipynb index d054e27f..8c8e016f 100644 --- a/terra-jupyter-bioconductor/tests/smoke_test.ipynb +++ b/terra-jupyter-bioconductor/tests/smoke_test.ipynb @@ -3,7 +3,7 @@ { "attachments": {}, "cell_type": "markdown", - "id": "3d33c4ec", + "id": "0", "metadata": {}, "source": [ "Test Bioconductor image" @@ -12,7 +12,7 @@ { "cell_type": "code", "execution_count": null, - "id": "52ca0aa1", + "id": "1", "metadata": { "vscode": { "languageId": "r" @@ -26,7 +26,7 @@ { "cell_type": "code", "execution_count": null, - "id": "martial-fitness", + "id": "2", "metadata": { "vscode": { "languageId": "r" @@ -40,7 +40,7 @@ { "cell_type": "code", "execution_count": null, - "id": "blessed-burden", + "id": "3", "metadata": { "vscode": { "languageId": "r" @@ -54,7 +54,7 @@ { "cell_type": "code", "execution_count": null, - "id": "7cde5e74", + "id": "4", "metadata": { "vscode": { "languageId": "r" @@ -68,7 +68,7 @@ { "cell_type": "code", "execution_count": null, - "id": "fc54067c", + "id": "5", "metadata": { "vscode": { "languageId": "r" @@ -82,7 +82,7 @@ { "cell_type": "code", "execution_count": null, - "id": "699ca08a", + "id": "6", "metadata": { "vscode": { "languageId": "r" @@ -96,7 +96,7 @@ { "cell_type": "code", "execution_count": null, - "id": "91f1d7ad", + "id": "7", "metadata": { "vscode": { "languageId": "r" @@ -110,7 +110,7 @@ { "cell_type": "code", "execution_count": null, - "id": "b941edc1", + "id": "8", "metadata": { "vscode": { "languageId": "r" @@ -124,7 +124,7 @@ { "cell_type": "code", "execution_count": null, - "id": "3adf1f30", + "id": "9", "metadata": { "vscode": { "languageId": "r" @@ -139,7 +139,7 @@ { "cell_type": "code", "execution_count": null, - "id": "bibliographic-intent", + "id": "10", "metadata": { "vscode": { "languageId": "r" @@ -153,7 +153,7 @@ { "cell_type": "code", "execution_count": null, - "id": "3663e1de", + "id": "11", "metadata": { "vscode": { "languageId": "r" @@ -167,7 +167,7 @@ { "cell_type": "code", "execution_count": null, - "id": "7c2881aa", + "id": "12", "metadata": { "vscode": { "languageId": "r" @@ -181,7 +181,7 @@ { "cell_type": "code", "execution_count": null, - "id": "15a5ba29", + "id": "13", "metadata": { "vscode": { "languageId": "r" @@ -195,7 +195,7 @@ { "cell_type": "code", "execution_count": null, - "id": "45051b29", + "id": "14", "metadata": { "vscode": { "languageId": "r" @@ -209,7 +209,7 @@ { "cell_type": "code", "execution_count": null, - "id": "09a7477d", + "id": "15", "metadata": { "vscode": { "languageId": "r" @@ -223,7 +223,7 @@ { "cell_type": "code", "execution_count": null, - "id": "c33a766b", + "id": "16", "metadata": { "vscode": { "languageId": "r" @@ -239,7 +239,7 @@ { "cell_type": "code", "execution_count": null, - "id": "c90bd714", + "id": "17", "metadata": { "vscode": { "languageId": "r" @@ -253,7 +253,7 @@ { "cell_type": "code", "execution_count": null, - "id": "0ba20610", + "id": "18", "metadata": { "vscode": { "languageId": "r" @@ -267,7 +267,7 @@ { "cell_type": "code", "execution_count": null, - "id": "8cd7e307", + "id": "19", "metadata": { "vscode": { "languageId": "r" @@ -281,7 +281,7 @@ { "cell_type": "code", "execution_count": null, - "id": "fbb3f21b", + "id": "20", "metadata": { "vscode": { "languageId": "r" @@ -296,7 +296,7 @@ { "cell_type": "code", "execution_count": null, - "id": "919ab35c", + "id": "21", "metadata": { "vscode": { "languageId": "r" @@ -311,7 +311,7 @@ { "cell_type": "code", "execution_count": null, - "id": "a7e397b2", + "id": "22", "metadata": { "vscode": { "languageId": "r" @@ -326,7 +326,7 @@ { "cell_type": "code", "execution_count": null, - "id": "c3d238c6", + "id": "23", "metadata": { "vscode": { "languageId": "r" @@ -340,7 +340,7 @@ { "cell_type": "code", "execution_count": null, - "id": "4dad1ef3", + "id": "24", "metadata": { "vscode": { "languageId": "r" @@ -354,7 +354,7 @@ { "cell_type": "code", "execution_count": null, - "id": "4749b448", + "id": "25", "metadata": { "vscode": { "languageId": "r" @@ -368,7 +368,7 @@ { "cell_type": "code", "execution_count": null, - "id": "fdb05268", + "id": "26", "metadata": { "vscode": { "languageId": "r" @@ -382,7 +382,7 @@ { "cell_type": "code", "execution_count": null, - "id": "aa8c5778", + "id": "27", "metadata": { "vscode": { "languageId": "r" @@ -396,7 +396,7 @@ { "cell_type": "code", "execution_count": null, - "id": "592faa65", + "id": "28", "metadata": { "vscode": { "languageId": "r" @@ -410,7 +410,7 @@ { "cell_type": "code", "execution_count": null, - "id": "e87ffece", + "id": "29", "metadata": { "vscode": { "languageId": "r" @@ -424,7 +424,7 @@ { "cell_type": "code", "execution_count": null, - "id": "f3e13a29", + "id": "30", "metadata": { "vscode": { "languageId": "r" @@ -438,7 +438,7 @@ { "cell_type": "code", "execution_count": null, - "id": "72a7c4d4", + "id": "31", "metadata": { "vscode": { "languageId": "r" @@ -452,7 +452,7 @@ { "cell_type": "code", "execution_count": null, - "id": "invisible-romantic", + "id": "32", "metadata": { "vscode": { "languageId": "r" @@ -466,7 +466,7 @@ { "cell_type": "code", "execution_count": null, - "id": "a3c864c5", + "id": "33", "metadata": { "vscode": { "languageId": "r" @@ -481,7 +481,7 @@ { "cell_type": "code", "execution_count": null, - "id": "6a4b7b40", + "id": "34", "metadata": { "vscode": { "languageId": "r" @@ -495,9 +495,8 @@ { "cell_type": "code", "execution_count": null, - "id": "younger-greenhouse", + "id": "35", "metadata": { - "scrolled": true, "vscode": { "languageId": "r" } @@ -510,7 +509,7 @@ { "cell_type": "code", "execution_count": null, - "id": "approximate-cutting", + "id": "36", "metadata": { "vscode": { "languageId": "r" @@ -524,7 +523,7 @@ { "cell_type": "code", "execution_count": null, - "id": "special-fishing", + "id": "37", "metadata": { "vscode": { "languageId": "r" @@ -538,7 +537,7 @@ { "cell_type": "code", "execution_count": null, - "id": "conscious-hacker", + "id": "38", "metadata": { "vscode": { "languageId": "r" @@ -552,7 +551,7 @@ { "cell_type": "code", "execution_count": null, - "id": "6f3c92ba", + "id": "39", "metadata": { "vscode": { "languageId": "r"