From 688c8b9bb666d973420cf9a8b5a45dccd219afbe Mon Sep 17 00:00:00 2001 From: fl3sc0b Date: Tue, 10 May 2022 16:04:41 +0200 Subject: [PATCH] add python-3-pyspark-databricks-dbx --- .../.devcontainer/Dockerfile | 61 ++ .../.devcontainer/base.Dockerfile | 51 + .../.devcontainer/devcontainer.json | 46 + .../.devcontainer/library-scripts/README.md | 5 + .../library-scripts/common-debian.sh | 454 +++++++++ .../.devcontainer/library-scripts/meta.env | 1 + .../library-scripts/node-debian.sh | 169 +++ .../library-scripts/python-debian.sh | 354 +++++++ .../.gitignore | 1 + .../.npmignore | 7 + .../.vscode/launch.json | 15 + .../.vscode/tasks.json | 71 ++ .../python-3-pyspark-databricks-dbx/README.md | 198 ++++ .../README/img/code-analysis.png | Bin 0 -> 121653 bytes .../README/img/docum.png | Bin 0 -> 35857 bytes .../README/img/remote-windows-toolbox.png | Bin 0 -> 4037 bytes .../README/img/results.png | Bin 0 -> 119134 bytes .../definition-manifest.json | 70 ++ .../history/0.201.4.md | 473 +++++++++ .../history/0.201.5.md | 473 +++++++++ .../history/0.201.6.md | 381 +++++++ .../history/0.201.7.md | 381 +++++++ .../history/0.201.8.md | 381 +++++++ .../history/0.201.9.md | 377 +++++++ .../history/0.202.0.md | 773 ++++++++++++++ .../history/0.202.1.md | 868 ++++++++++++++++ .../history/0.203.0.md | 963 ++++++++++++++++++ .../history/0.203.1.md | 963 ++++++++++++++++++ .../history/0.203.2.md | 963 ++++++++++++++++++ .../history/0.203.3.md | 963 ++++++++++++++++++ .../history/0.203.5.md | 963 ++++++++++++++++++ .../history/dev.md | 963 ++++++++++++++++++ .../scripts/new-dbx-project.sh | 24 + .../scripts/new-local-pyspark-project.sh | 9 + .../scripts/pyspark-template.py | 66 ++ .../scripts/run-dbx-project.sh | 7 + .../test-project/hello.py | 6 + 37 files changed, 11500 insertions(+) create mode 100644 containers/python-3-pyspark-databricks-dbx/.devcontainer/Dockerfile create mode 100644 containers/python-3-pyspark-databricks-dbx/.devcontainer/base.Dockerfile create mode 100644 containers/python-3-pyspark-databricks-dbx/.devcontainer/devcontainer.json create mode 100644 containers/python-3-pyspark-databricks-dbx/.devcontainer/library-scripts/README.md create mode 100644 containers/python-3-pyspark-databricks-dbx/.devcontainer/library-scripts/common-debian.sh create mode 100644 containers/python-3-pyspark-databricks-dbx/.devcontainer/library-scripts/meta.env create mode 100644 containers/python-3-pyspark-databricks-dbx/.devcontainer/library-scripts/node-debian.sh create mode 100644 containers/python-3-pyspark-databricks-dbx/.devcontainer/library-scripts/python-debian.sh create mode 100644 containers/python-3-pyspark-databricks-dbx/.gitignore create mode 100644 containers/python-3-pyspark-databricks-dbx/.npmignore create mode 100644 containers/python-3-pyspark-databricks-dbx/.vscode/launch.json create mode 100644 containers/python-3-pyspark-databricks-dbx/.vscode/tasks.json create mode 100644 containers/python-3-pyspark-databricks-dbx/README.md create mode 100644 containers/python-3-pyspark-databricks-dbx/README/img/code-analysis.png create mode 100644 containers/python-3-pyspark-databricks-dbx/README/img/docum.png create mode 100644 containers/python-3-pyspark-databricks-dbx/README/img/remote-windows-toolbox.png create mode 100644 containers/python-3-pyspark-databricks-dbx/README/img/results.png create mode 100644 containers/python-3-pyspark-databricks-dbx/definition-manifest.json create mode 100644 containers/python-3-pyspark-databricks-dbx/history/0.201.4.md create mode 100644 containers/python-3-pyspark-databricks-dbx/history/0.201.5.md create mode 100644 containers/python-3-pyspark-databricks-dbx/history/0.201.6.md create mode 100644 containers/python-3-pyspark-databricks-dbx/history/0.201.7.md create mode 100644 containers/python-3-pyspark-databricks-dbx/history/0.201.8.md create mode 100644 containers/python-3-pyspark-databricks-dbx/history/0.201.9.md create mode 100644 containers/python-3-pyspark-databricks-dbx/history/0.202.0.md create mode 100644 containers/python-3-pyspark-databricks-dbx/history/0.202.1.md create mode 100644 containers/python-3-pyspark-databricks-dbx/history/0.203.0.md create mode 100644 containers/python-3-pyspark-databricks-dbx/history/0.203.1.md create mode 100644 containers/python-3-pyspark-databricks-dbx/history/0.203.2.md create mode 100644 containers/python-3-pyspark-databricks-dbx/history/0.203.3.md create mode 100644 containers/python-3-pyspark-databricks-dbx/history/0.203.5.md create mode 100644 containers/python-3-pyspark-databricks-dbx/history/dev.md create mode 100644 containers/python-3-pyspark-databricks-dbx/scripts/new-dbx-project.sh create mode 100644 containers/python-3-pyspark-databricks-dbx/scripts/new-local-pyspark-project.sh create mode 100644 containers/python-3-pyspark-databricks-dbx/scripts/pyspark-template.py create mode 100644 containers/python-3-pyspark-databricks-dbx/scripts/run-dbx-project.sh create mode 100644 containers/python-3-pyspark-databricks-dbx/test-project/hello.py diff --git a/containers/python-3-pyspark-databricks-dbx/.devcontainer/Dockerfile b/containers/python-3-pyspark-databricks-dbx/.devcontainer/Dockerfile new file mode 100644 index 0000000000..36dc503975 --- /dev/null +++ b/containers/python-3-pyspark-databricks-dbx/.devcontainer/Dockerfile @@ -0,0 +1,61 @@ +# [Choice] Python version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.10, 3.9, 3.8, 3.7, 3.6, 3-bullseye, 3.10-bullseye, 3.9-bullseye, 3.8-bullseye, 3.7-bullseye, 3.6-bullseye, 3-buster, 3.10-buster, 3.9-buster, 3.8-buster, 3.7-buster, 3.6-buster +ARG VARIANT=3-bullseye +FROM mcr.microsoft.com/vscode/devcontainers/python:${VARIANT} + +# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 +ARG NODE_VERSION="none" +RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi + +ARG spark_version="3.1.2" +ARG hadoop_version="3.2" +ARG spark_checksum="2385CB772F21B014CE2ABD6B8F5E815721580D6E8BC42A26D70BBCDDA8D303D886A6F12B36D40F6971B5547B70FAE62B5A96146F0421CB93D4E51491308EF5D5" +ARG openjdk_version=11 + +ENV APACHE_SPARK_VERSION="${spark_version}" \ + HADOOP_VERSION="${hadoop_version}" + +RUN sudo apt-get update --yes && sudo apt-get install --yes --no-install-recommends \ + "openjdk-${openjdk_version}-jre-headless" \ + ca-certificates-java && \ + sudo apt-get clean && sudo rm -rf /var/lib/apt/lists/* + +# Spark installation +WORKDIR /tmp +RUN sudo wget -q "https://archive.apache.org/dist/spark/spark-${APACHE_SPARK_VERSION}/spark-${APACHE_SPARK_VERSION}-bin-hadoop${HADOOP_VERSION}.tgz" && \ + echo "${spark_checksum} *spark-${APACHE_SPARK_VERSION}-bin-hadoop${HADOOP_VERSION}.tgz" | sha512sum -c - && \ + sudo tar xzf "spark-${APACHE_SPARK_VERSION}-bin-hadoop${HADOOP_VERSION}.tgz" -C /usr/local --owner root --group root --no-same-owner && \ + sudo rm "spark-${APACHE_SPARK_VERSION}-bin-hadoop${HADOOP_VERSION}.tgz" + +WORKDIR /usr/local + +# Configure Spark +ENV SPARK_HOME=/usr/local/spark +ENV SPARK_OPTS="--driver-java-options=-Xms1024M --driver-java-options=-Xmx4096M --driver-java-options=-Dlog4j.logLevel=info" \ + PATH="${PATH}:${SPARK_HOME}/bin" + +RUN sudo ln -s "spark-${APACHE_SPARK_VERSION}-bin-hadoop${HADOOP_VERSION}" spark && \ + # Add a link in the before_notebook hook in order to source automatically PYTHONPATH + sudo mkdir -p /usr/local/bin/before-notebook.d && \ + sudo ln -s "${SPARK_HOME}/sbin/spark-config.sh" /usr/local/bin/before-notebook.d/spark-config.sh + +# Fix Spark installation for Java 11 and Apache Arrow library +# see: https://github.com/apache/spark/pull/27356, https://spark.apache.org/docs/latest/#downloading +RUN sudo cp -p "${SPARK_HOME}/conf/spark-defaults.conf.template" "${SPARK_HOME}/conf/spark-defaults.conf" && \ + echo 'spark.driver.extraJavaOptions -Dio.netty.tryReflectionSetAccessible=true' >> "${SPARK_HOME}/conf/spark-defaults.conf" && \ + echo 'spark.executor.extraJavaOptions -Dio.netty.tryReflectionSetAccessible=true' >> "${SPARK_HOME}/conf/spark-defaults.conf" + +RUN sudo pip install pyspark + +RUN sudo pip install dbx + +# [Optional] If your pip requirements rarely change, uncomment this section to add them to the image. +# COPY requirements.txt /tmp/pip-tmp/ +# RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \ +# && rm -rf /tmp/pip-tmp + +# [Optional] Uncomment this section to install additional OS packages. +# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +# && apt-get -y install --no-install-recommends + +# [Optional] Uncomment this line to install global node packages. +# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 diff --git a/containers/python-3-pyspark-databricks-dbx/.devcontainer/base.Dockerfile b/containers/python-3-pyspark-databricks-dbx/.devcontainer/base.Dockerfile new file mode 100644 index 0000000000..9ee1e4289f --- /dev/null +++ b/containers/python-3-pyspark-databricks-dbx/.devcontainer/base.Dockerfile @@ -0,0 +1,51 @@ +# [Choice] Python version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.10, 3.9, 3.8, 3.7, 3.6, 3-bullseye, 3.10-bullseye, 3.9-bullseye, 3.8-bullseye, 3.7-bullseye, 3.6-bullseye, 3-buster, 3.10-buster, 3.9-buster, 3.8-buster, 3.7-buster, 3.6-buster +ARG VARIANT=3-bullseye +FROM python:${VARIANT} + +# Copy library scripts to execute +COPY .devcontainer/library-scripts/*.sh .devcontainer/library-scripts/*.env /tmp/library-scripts/ + +# [Option] Install zsh +ARG INSTALL_ZSH="true" +# [Option] Upgrade OS packages to their latest versions +ARG UPGRADE_PACKAGES="true" +# Install needed packages and setup non-root user. Use a separate RUN statement to add your own dependencies. +ARG USERNAME=vscode +ARG USER_UID=1000 +ARG USER_GID=$USER_UID +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ + # Remove imagemagick due to https://security-tracker.debian.org/tracker/CVE-2019-10131 + && apt-get purge -y imagemagick imagemagick-6-common \ + # Install common packages, non-root user + && bash /tmp/library-scripts/common-debian.sh "${INSTALL_ZSH}" "${USERNAME}" "${USER_UID}" "${USER_GID}" "${UPGRADE_PACKAGES}" "true" "true" \ + && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* + +# Setup default python tools in a venv via pipx to avoid conflicts +ENV PIPX_HOME=/usr/local/py-utils \ + PIPX_BIN_DIR=/usr/local/py-utils/bin +ENV PATH=${PATH}:${PIPX_BIN_DIR} +RUN bash /tmp/library-scripts/python-debian.sh "none" "/usr/local" "${PIPX_HOME}" "${USERNAME}" \ + && apt-get clean -y && rm -rf /var/lib/apt/lists/* + +# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 +ARG NODE_VERSION="none" +ENV NVM_DIR=/usr/local/share/nvm +ENV NVM_SYMLINK_CURRENT=true \ + PATH=${NVM_DIR}/current/bin:${PATH} +RUN bash /tmp/library-scripts/node-debian.sh "${NVM_DIR}" "${NODE_VERSION}" "${USERNAME}" \ + && apt-get clean -y && rm -rf /var/lib/apt/lists/* + +# Remove library scripts for final image +RUN rm -rf /tmp/library-scripts + +# [Optional] If your pip requirements rarely change, uncomment this section to add them to the image. +# COPY requirements.txt /tmp/pip-tmp/ +# RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \ +# && rm -rf /tmp/pip-tmp + +# [Optional] Uncomment this section to install additional OS packages. +# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +# && apt-get -y install --no-install-recommends + +# [Optional] Uncomment this line to install global node packages. +# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 diff --git a/containers/python-3-pyspark-databricks-dbx/.devcontainer/devcontainer.json b/containers/python-3-pyspark-databricks-dbx/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..65964fadbe --- /dev/null +++ b/containers/python-3-pyspark-databricks-dbx/.devcontainer/devcontainer.json @@ -0,0 +1,46 @@ +{ + "name": "Python 3", + "build": { + "dockerfile": "Dockerfile", + "context": "..", + "args": { + // Update 'VARIANT' to pick a Python version: 3, 3.10, 3.9, 3.8, 3.7, 3.6 + // Append -bullseye or -buster to pin to an OS version. + // Use -bullseye variants on local on arm64/Apple Silicon. + "VARIANT": "3.10-bullseye", + // Options + "NODE_VERSION": "lts/*" + } + }, + + // Set *default* container specific settings.json values on container create. + "settings": { + "python.defaultInterpreterPath": "/usr/local/bin/python", + "python.linting.enabled": true, + "python.linting.pylintEnabled": true, + "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8", + "python.formatting.blackPath": "/usr/local/py-utils/bin/black", + "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf", + "python.linting.banditPath": "/usr/local/py-utils/bin/bandit", + "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8", + "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy", + "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle", + "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle", + "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint" + }, + + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "ms-python.python", + "ms-python.vscode-pylance" + ], + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "pip3 install --user -r requirements.txt", + + // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode" +} diff --git a/containers/python-3-pyspark-databricks-dbx/.devcontainer/library-scripts/README.md b/containers/python-3-pyspark-databricks-dbx/.devcontainer/library-scripts/README.md new file mode 100644 index 0000000000..72e2dbbaa9 --- /dev/null +++ b/containers/python-3-pyspark-databricks-dbx/.devcontainer/library-scripts/README.md @@ -0,0 +1,5 @@ +# Warning: Folder contents may be replaced + +The contents of this folder will be automatically replaced with a file of the same name in the [vscode-dev-containers](https://github.com/microsoft/vscode-dev-containers) repository's [script-library folder](https://github.com/microsoft/vscode-dev-containers/tree/main/script-library) whenever the repository is packaged. + +To retain your edits, move the file to a different location. You may also delete the files if they are not needed. \ No newline at end of file diff --git a/containers/python-3-pyspark-databricks-dbx/.devcontainer/library-scripts/common-debian.sh b/containers/python-3-pyspark-databricks-dbx/.devcontainer/library-scripts/common-debian.sh new file mode 100644 index 0000000000..af4facc8f6 --- /dev/null +++ b/containers/python-3-pyspark-databricks-dbx/.devcontainer/library-scripts/common-debian.sh @@ -0,0 +1,454 @@ +#!/usr/bin/env bash +#------------------------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. +#------------------------------------------------------------------------------------------------------------- +# +# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/common.md +# Maintainer: The VS Code and Codespaces Teams +# +# Syntax: ./common-debian.sh [install zsh flag] [username] [user UID] [user GID] [upgrade packages flag] [install Oh My Zsh! flag] [Add non-free packages] + +set -e + +INSTALL_ZSH=${1:-"true"} +USERNAME=${2:-"automatic"} +USER_UID=${3:-"automatic"} +USER_GID=${4:-"automatic"} +UPGRADE_PACKAGES=${5:-"true"} +INSTALL_OH_MYS=${6:-"true"} +ADD_NON_FREE_PACKAGES=${7:-"false"} +SCRIPT_DIR="$(cd $(dirname "${BASH_SOURCE[0]}") && pwd)" +MARKER_FILE="/usr/local/etc/vscode-dev-containers/common" + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +# Ensure that login shells get the correct path if the user updated the PATH using ENV. +rm -f /etc/profile.d/00-restore-env.sh +echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh +chmod +x /etc/profile.d/00-restore-env.sh + +# If in automatic mode, determine if a user already exists, if not use vscode +if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then + USERNAME="" + POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") + for CURRENT_USER in ${POSSIBLE_USERS[@]}; do + if id -u ${CURRENT_USER} > /dev/null 2>&1; then + USERNAME=${CURRENT_USER} + break + fi + done + if [ "${USERNAME}" = "" ]; then + USERNAME=vscode + fi +elif [ "${USERNAME}" = "none" ]; then + USERNAME=root + USER_UID=0 + USER_GID=0 +fi + +# Load markers to see which steps have already run +if [ -f "${MARKER_FILE}" ]; then + echo "Marker file found:" + cat "${MARKER_FILE}" + source "${MARKER_FILE}" +fi + +# Ensure apt is in non-interactive to avoid prompts +export DEBIAN_FRONTEND=noninteractive + +# Function to call apt-get if needed +apt_get_update_if_needed() +{ + if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update + else + echo "Skipping apt-get update." + fi +} + +# Run install apt-utils to avoid debconf warning then verify presence of other common developer tools and dependencies +if [ "${PACKAGES_ALREADY_INSTALLED}" != "true" ]; then + + package_list="apt-utils \ + openssh-client \ + gnupg2 \ + dirmngr \ + iproute2 \ + procps \ + lsof \ + htop \ + net-tools \ + psmisc \ + curl \ + wget \ + rsync \ + ca-certificates \ + unzip \ + zip \ + nano \ + vim-tiny \ + less \ + jq \ + lsb-release \ + apt-transport-https \ + dialog \ + libc6 \ + libgcc1 \ + libkrb5-3 \ + libgssapi-krb5-2 \ + libicu[0-9][0-9] \ + liblttng-ust0 \ + libstdc++6 \ + zlib1g \ + locales \ + sudo \ + ncdu \ + man-db \ + strace \ + manpages \ + manpages-dev \ + init-system-helpers" + + # Needed for adding manpages-posix and manpages-posix-dev which are non-free packages in Debian + if [ "${ADD_NON_FREE_PACKAGES}" = "true" ]; then + # Bring in variables from /etc/os-release like VERSION_CODENAME + . /etc/os-release + sed -i -E "s/deb http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME} main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME} main contrib non-free/" /etc/apt/sources.list + sed -i -E "s/deb-src http:\/\/(deb|httredir)\.debian\.org\/debian ${VERSION_CODENAME} main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME} main contrib non-free/" /etc/apt/sources.list + sed -i -E "s/deb http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME}-updates main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME}-updates main contrib non-free/" /etc/apt/sources.list + sed -i -E "s/deb-src http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME}-updates main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME}-updates main contrib non-free/" /etc/apt/sources.list + sed -i "s/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main contrib non-free/" /etc/apt/sources.list + sed -i "s/deb-src http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main contrib non-free/" /etc/apt/sources.list + sed -i "s/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main contrib non-free/" /etc/apt/sources.list + sed -i "s/deb-src http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main contrib non-free/" /etc/apt/sources.list + # Handle bullseye location for security https://www.debian.org/releases/bullseye/amd64/release-notes/ch-information.en.html + sed -i "s/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main contrib non-free/" /etc/apt/sources.list + sed -i "s/deb-src http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main contrib non-free/" /etc/apt/sources.list + echo "Running apt-get update..." + apt-get update + package_list="${package_list} manpages-posix manpages-posix-dev" + else + apt_get_update_if_needed + fi + + # Install libssl1.1 if available + if [[ ! -z $(apt-cache --names-only search ^libssl1.1$) ]]; then + package_list="${package_list} libssl1.1" + fi + + # Install appropriate version of libssl1.0.x if available + libssl_package=$(dpkg-query -f '${db:Status-Abbrev}\t${binary:Package}\n' -W 'libssl1\.0\.?' 2>&1 || echo '') + if [ "$(echo "$LIlibssl_packageBSSL" | grep -o 'libssl1\.0\.[0-9]:' | uniq | sort | wc -l)" -eq 0 ]; then + if [[ ! -z $(apt-cache --names-only search ^libssl1.0.2$) ]]; then + # Debian 9 + package_list="${package_list} libssl1.0.2" + elif [[ ! -z $(apt-cache --names-only search ^libssl1.0.0$) ]]; then + # Ubuntu 18.04, 16.04, earlier + package_list="${package_list} libssl1.0.0" + fi + fi + + echo "Packages to verify are installed: ${package_list}" + apt-get -y install --no-install-recommends ${package_list} 2> >( grep -v 'debconf: delaying package configuration, since apt-utils is not installed' >&2 ) + + # Install git if not already installed (may be more recent than distro version) + if ! type git > /dev/null 2>&1; then + apt-get -y install --no-install-recommends git + fi + + PACKAGES_ALREADY_INSTALLED="true" +fi + +# Get to latest versions of all packages +if [ "${UPGRADE_PACKAGES}" = "true" ]; then + apt_get_update_if_needed + apt-get -y upgrade --no-install-recommends + apt-get autoremove -y +fi + +# Ensure at least the en_US.UTF-8 UTF-8 locale is available. +# Common need for both applications and things like the agnoster ZSH theme. +if [ "${LOCALE_ALREADY_SET}" != "true" ] && ! grep -o -E '^\s*en_US.UTF-8\s+UTF-8' /etc/locale.gen > /dev/null; then + echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen + locale-gen + LOCALE_ALREADY_SET="true" +fi + +# Create or update a non-root user to match UID/GID. +group_name="${USERNAME}" +if id -u ${USERNAME} > /dev/null 2>&1; then + # User exists, update if needed + if [ "${USER_GID}" != "automatic" ] && [ "$USER_GID" != "$(id -g $USERNAME)" ]; then + group_name="$(id -gn $USERNAME)" + groupmod --gid $USER_GID ${group_name} + usermod --gid $USER_GID $USERNAME + fi + if [ "${USER_UID}" != "automatic" ] && [ "$USER_UID" != "$(id -u $USERNAME)" ]; then + usermod --uid $USER_UID $USERNAME + fi +else + # Create user + if [ "${USER_GID}" = "automatic" ]; then + groupadd $USERNAME + else + groupadd --gid $USER_GID $USERNAME + fi + if [ "${USER_UID}" = "automatic" ]; then + useradd -s /bin/bash --gid $USERNAME -m $USERNAME + else + useradd -s /bin/bash --uid $USER_UID --gid $USERNAME -m $USERNAME + fi +fi + +# Add add sudo support for non-root user +if [ "${USERNAME}" != "root" ] && [ "${EXISTING_NON_ROOT_USER}" != "${USERNAME}" ]; then + echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME + chmod 0440 /etc/sudoers.d/$USERNAME + EXISTING_NON_ROOT_USER="${USERNAME}" +fi + +# ** Shell customization section ** +if [ "${USERNAME}" = "root" ]; then + user_rc_path="/root" +else + user_rc_path="/home/${USERNAME}" +fi + +# Restore user .bashrc defaults from skeleton file if it doesn't exist or is empty +if [ ! -f "${user_rc_path}/.bashrc" ] || [ ! -s "${user_rc_path}/.bashrc" ] ; then + cp /etc/skel/.bashrc "${user_rc_path}/.bashrc" +fi + +# Restore user .profile defaults from skeleton file if it doesn't exist or is empty +if [ ! -f "${user_rc_path}/.profile" ] || [ ! -s "${user_rc_path}/.profile" ] ; then + cp /etc/skel/.profile "${user_rc_path}/.profile" +fi + +# .bashrc/.zshrc snippet +rc_snippet="$(cat << 'EOF' + +if [ -z "${USER}" ]; then export USER=$(whoami); fi +if [[ "${PATH}" != *"$HOME/.local/bin"* ]]; then export PATH="${PATH}:$HOME/.local/bin"; fi + +# Display optional first run image specific notice if configured and terminal is interactive +if [ -t 1 ] && [[ "${TERM_PROGRAM}" = "vscode" || "${TERM_PROGRAM}" = "codespaces" ]] && [ ! -f "$HOME/.config/vscode-dev-containers/first-run-notice-already-displayed" ]; then + if [ -f "/usr/local/etc/vscode-dev-containers/first-run-notice.txt" ]; then + cat "/usr/local/etc/vscode-dev-containers/first-run-notice.txt" + elif [ -f "/workspaces/.codespaces/shared/first-run-notice.txt" ]; then + cat "/workspaces/.codespaces/shared/first-run-notice.txt" + fi + mkdir -p "$HOME/.config/vscode-dev-containers" + # Mark first run notice as displayed after 10s to avoid problems with fast terminal refreshes hiding it + ((sleep 10s; touch "$HOME/.config/vscode-dev-containers/first-run-notice-already-displayed") &) +fi + +# Set the default git editor if not already set +if [ -z "$(git config --get core.editor)" ] && [ -z "${GIT_EDITOR}" ]; then + if [ "${TERM_PROGRAM}" = "vscode" ]; then + if [[ -n $(command -v code-insiders) && -z $(command -v code) ]]; then + export GIT_EDITOR="code-insiders --wait" + else + export GIT_EDITOR="code --wait" + fi + fi +fi + +EOF +)" + +# code shim, it fallbacks to code-insiders if code is not available +cat << 'EOF' > /usr/local/bin/code +#!/bin/sh + +get_in_path_except_current() { + which -a "$1" | grep -A1 "$0" | grep -v "$0" +} + +code="$(get_in_path_except_current code)" + +if [ -n "$code" ]; then + exec "$code" "$@" +elif [ "$(command -v code-insiders)" ]; then + exec code-insiders "$@" +else + echo "code or code-insiders is not installed" >&2 + exit 127 +fi +EOF +chmod +x /usr/local/bin/code + +# systemctl shim - tells people to use 'service' if systemd is not running +cat << 'EOF' > /usr/local/bin/systemctl +#!/bin/sh +set -e +if [ -d "/run/systemd/system" ]; then + exec /bin/systemctl/systemctl "$@" +else + echo '\n"systemd" is not running in this container due to its overhead.\nUse the "service" command to start services instead. e.g.: \n\nservice --status-all' +fi +EOF +chmod +x /usr/local/bin/systemctl + +# Codespaces bash and OMZ themes - partly inspired by https://github.com/ohmyzsh/ohmyzsh/blob/master/themes/robbyrussell.zsh-theme +codespaces_bash="$(cat \ +<<'EOF' + +# Codespaces bash prompt theme +__bash_prompt() { + local userpart='`export XIT=$? \ + && [ ! -z "${GITHUB_USER}" ] && echo -n "\[\033[0;32m\]@${GITHUB_USER} " || echo -n "\[\033[0;32m\]\u " \ + && [ "$XIT" -ne "0" ] && echo -n "\[\033[1;31m\]➜" || echo -n "\[\033[0m\]➜"`' + local gitbranch='`\ + if [ "$(git config --get codespaces-theme.hide-status 2>/dev/null)" != 1 ]; then \ + export BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || git rev-parse --short HEAD 2>/dev/null); \ + if [ "${BRANCH}" != "" ]; then \ + echo -n "\[\033[0;36m\](\[\033[1;31m\]${BRANCH}" \ + && if git ls-files --error-unmatch -m --directory --no-empty-directory -o --exclude-standard ":/*" > /dev/null 2>&1; then \ + echo -n " \[\033[1;33m\]✗"; \ + fi \ + && echo -n "\[\033[0;36m\]) "; \ + fi; \ + fi`' + local lightblue='\[\033[1;34m\]' + local removecolor='\[\033[0m\]' + PS1="${userpart} ${lightblue}\w ${gitbranch}${removecolor}\$ " + unset -f __bash_prompt +} +__bash_prompt + +EOF +)" + +codespaces_zsh="$(cat \ +<<'EOF' +# Codespaces zsh prompt theme +__zsh_prompt() { + local prompt_username + if [ ! -z "${GITHUB_USER}" ]; then + prompt_username="@${GITHUB_USER}" + else + prompt_username="%n" + fi + PROMPT="%{$fg[green]%}${prompt_username} %(?:%{$reset_color%}➜ :%{$fg_bold[red]%}➜ )" # User/exit code arrow + PROMPT+='%{$fg_bold[blue]%}%(5~|%-1~/…/%3~|%4~)%{$reset_color%} ' # cwd + PROMPT+='$([ "$(git config --get codespaces-theme.hide-status 2>/dev/null)" != 1 ] && git_prompt_info)' # Git status + PROMPT+='%{$fg[white]%}$ %{$reset_color%}' + unset -f __zsh_prompt +} +ZSH_THEME_GIT_PROMPT_PREFIX="%{$fg_bold[cyan]%}(%{$fg_bold[red]%}" +ZSH_THEME_GIT_PROMPT_SUFFIX="%{$reset_color%} " +ZSH_THEME_GIT_PROMPT_DIRTY=" %{$fg_bold[yellow]%}✗%{$fg_bold[cyan]%})" +ZSH_THEME_GIT_PROMPT_CLEAN="%{$fg_bold[cyan]%})" +__zsh_prompt + +EOF +)" + +# Add RC snippet and custom bash prompt +if [ "${RC_SNIPPET_ALREADY_ADDED}" != "true" ]; then + echo "${rc_snippet}" >> /etc/bash.bashrc + echo "${codespaces_bash}" >> "${user_rc_path}/.bashrc" + echo 'export PROMPT_DIRTRIM=4' >> "${user_rc_path}/.bashrc" + if [ "${USERNAME}" != "root" ]; then + echo "${codespaces_bash}" >> "/root/.bashrc" + echo 'export PROMPT_DIRTRIM=4' >> "/root/.bashrc" + fi + chown ${USERNAME}:${group_name} "${user_rc_path}/.bashrc" + RC_SNIPPET_ALREADY_ADDED="true" +fi + +# Optionally install and configure zsh and Oh My Zsh! +if [ "${INSTALL_ZSH}" = "true" ]; then + if ! type zsh > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get install -y zsh + fi + if [ "${ZSH_ALREADY_INSTALLED}" != "true" ]; then + echo "${rc_snippet}" >> /etc/zsh/zshrc + ZSH_ALREADY_INSTALLED="true" + fi + + # Adapted, simplified inline Oh My Zsh! install steps that adds, defaults to a codespaces theme. + # See https://github.com/ohmyzsh/ohmyzsh/blob/master/tools/install.sh for official script. + oh_my_install_dir="${user_rc_path}/.oh-my-zsh" + if [ ! -d "${oh_my_install_dir}" ] && [ "${INSTALL_OH_MYS}" = "true" ]; then + template_path="${oh_my_install_dir}/templates/zshrc.zsh-template" + user_rc_file="${user_rc_path}/.zshrc" + umask g-w,o-w + mkdir -p ${oh_my_install_dir} + git clone --depth=1 \ + -c core.eol=lf \ + -c core.autocrlf=false \ + -c fsck.zeroPaddedFilemode=ignore \ + -c fetch.fsck.zeroPaddedFilemode=ignore \ + -c receive.fsck.zeroPaddedFilemode=ignore \ + "https://github.com/ohmyzsh/ohmyzsh" "${oh_my_install_dir}" 2>&1 + echo -e "$(cat "${template_path}")\nDISABLE_AUTO_UPDATE=true\nDISABLE_UPDATE_PROMPT=true" > ${user_rc_file} + sed -i -e 's/ZSH_THEME=.*/ZSH_THEME="codespaces"/g' ${user_rc_file} + + mkdir -p ${oh_my_install_dir}/custom/themes + echo "${codespaces_zsh}" > "${oh_my_install_dir}/custom/themes/codespaces.zsh-theme" + # Shrink git while still enabling updates + cd "${oh_my_install_dir}" + git repack -a -d -f --depth=1 --window=1 + # Copy to non-root user if one is specified + if [ "${USERNAME}" != "root" ]; then + cp -rf "${user_rc_file}" "${oh_my_install_dir}" /root + chown -R ${USERNAME}:${group_name} "${user_rc_path}" + fi + fi +fi + +# Persist image metadata info, script if meta.env found in same directory +meta_info_script="$(cat << 'EOF' +#!/bin/sh +. /usr/local/etc/vscode-dev-containers/meta.env + +# Minimal output +if [ "$1" = "version" ] || [ "$1" = "image-version" ]; then + echo "${VERSION}" + exit 0 +elif [ "$1" = "release" ]; then + echo "${GIT_REPOSITORY_RELEASE}" + exit 0 +elif [ "$1" = "content" ] || [ "$1" = "content-url" ] || [ "$1" = "contents" ] || [ "$1" = "contents-url" ]; then + echo "${CONTENTS_URL}" + exit 0 +fi + +#Full output +echo +echo "Development container image information" +echo +if [ ! -z "${VERSION}" ]; then echo "- Image version: ${VERSION}"; fi +if [ ! -z "${DEFINITION_ID}" ]; then echo "- Definition ID: ${DEFINITION_ID}"; fi +if [ ! -z "${VARIANT}" ]; then echo "- Variant: ${VARIANT}"; fi +if [ ! -z "${GIT_REPOSITORY}" ]; then echo "- Source code repository: ${GIT_REPOSITORY}"; fi +if [ ! -z "${GIT_REPOSITORY_RELEASE}" ]; then echo "- Source code release/branch: ${GIT_REPOSITORY_RELEASE}"; fi +if [ ! -z "${BUILD_TIMESTAMP}" ]; then echo "- Timestamp: ${BUILD_TIMESTAMP}"; fi +if [ ! -z "${CONTENTS_URL}" ]; then echo && echo "More info: ${CONTENTS_URL}"; fi +echo +EOF +)" +if [ -f "${SCRIPT_DIR}/meta.env" ]; then + mkdir -p /usr/local/etc/vscode-dev-containers/ + cp -f "${SCRIPT_DIR}/meta.env" /usr/local/etc/vscode-dev-containers/meta.env + echo "${meta_info_script}" > /usr/local/bin/devcontainer-info + chmod +x /usr/local/bin/devcontainer-info +fi + +# Write marker file +mkdir -p "$(dirname "${MARKER_FILE}")" +echo -e "\ + PACKAGES_ALREADY_INSTALLED=${PACKAGES_ALREADY_INSTALLED}\n\ + LOCALE_ALREADY_SET=${LOCALE_ALREADY_SET}\n\ + EXISTING_NON_ROOT_USER=${EXISTING_NON_ROOT_USER}\n\ + RC_SNIPPET_ALREADY_ADDED=${RC_SNIPPET_ALREADY_ADDED}\n\ + ZSH_ALREADY_INSTALLED=${ZSH_ALREADY_INSTALLED}" > "${MARKER_FILE}" + +echo "Done!" diff --git a/containers/python-3-pyspark-databricks-dbx/.devcontainer/library-scripts/meta.env b/containers/python-3-pyspark-databricks-dbx/.devcontainer/library-scripts/meta.env new file mode 100644 index 0000000000..9e5433682e --- /dev/null +++ b/containers/python-3-pyspark-databricks-dbx/.devcontainer/library-scripts/meta.env @@ -0,0 +1 @@ +VERSION='dev' diff --git a/containers/python-3-pyspark-databricks-dbx/.devcontainer/library-scripts/node-debian.sh b/containers/python-3-pyspark-databricks-dbx/.devcontainer/library-scripts/node-debian.sh new file mode 100644 index 0000000000..c3551689c9 --- /dev/null +++ b/containers/python-3-pyspark-databricks-dbx/.devcontainer/library-scripts/node-debian.sh @@ -0,0 +1,169 @@ +#!/bin/bash +#------------------------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. +#------------------------------------------------------------------------------------------------------------- +# +# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/node.md +# Maintainer: The VS Code and Codespaces Teams +# +# Syntax: ./node-debian.sh [directory to install nvm] [node version to install (use "none" to skip)] [non-root user] [Update rc files flag] [install node-gyp deps] + +export NVM_DIR=${1:-"/usr/local/share/nvm"} +export NODE_VERSION=${2:-"lts"} +USERNAME=${3:-"automatic"} +UPDATE_RC=${4:-"true"} +INSTALL_TOOLS_FOR_NODE_GYP="${5:-true}" +export NVM_VERSION="0.38.0" + +set -e + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +# Ensure that login shells get the correct path if the user updated the PATH using ENV. +rm -f /etc/profile.d/00-restore-env.sh +echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh +chmod +x /etc/profile.d/00-restore-env.sh + +# Determine the appropriate non-root user +if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then + USERNAME="" + POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") + for CURRENT_USER in ${POSSIBLE_USERS[@]}; do + if id -u ${CURRENT_USER} > /dev/null 2>&1; then + USERNAME=${CURRENT_USER} + break + fi + done + if [ "${USERNAME}" = "" ]; then + USERNAME=root + fi +elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then + USERNAME=root +fi + +updaterc() { + if [ "${UPDATE_RC}" = "true" ]; then + echo "Updating /etc/bash.bashrc and /etc/zsh/zshrc..." + if [[ "$(cat /etc/bash.bashrc)" != *"$1"* ]]; then + echo -e "$1" >> /etc/bash.bashrc + fi + if [ -f "/etc/zsh/zshrc" ] && [[ "$(cat /etc/zsh/zshrc)" != *"$1"* ]]; then + echo -e "$1" >> /etc/zsh/zshrc + fi + fi +} + +# Function to run apt-get if needed +apt_get_update_if_needed() +{ + if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update + else + echo "Skipping apt-get update." + fi +} + +# Checks if packages are installed and installs them if not +check_packages() { + if ! dpkg -s "$@" > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends "$@" + fi +} + +# Ensure apt is in non-interactive to avoid prompts +export DEBIAN_FRONTEND=noninteractive + +# Install dependencies +check_packages apt-transport-https curl ca-certificates tar gnupg2 dirmngr + +# Install yarn +if type yarn > /dev/null 2>&1; then + echo "Yarn already installed." +else + # Import key safely (new method rather than deprecated apt-key approach) and install + curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor > /usr/share/keyrings/yarn-archive-keyring.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/yarn-archive-keyring.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list + apt-get update + apt-get -y install --no-install-recommends yarn +fi + +# Adjust node version if required +if [ "${NODE_VERSION}" = "none" ]; then + export NODE_VERSION= +elif [ "${NODE_VERSION}" = "lts" ]; then + export NODE_VERSION="lts/*" +fi + +# Create a symlink to the installed version for use in Dockerfile PATH statements +export NVM_SYMLINK_CURRENT=true + +# Install the specified node version if NVM directory already exists, then exit +if [ -d "${NVM_DIR}" ]; then + echo "NVM already installed." + if [ "${NODE_VERSION}" != "" ]; then + su ${USERNAME} -c ". $NVM_DIR/nvm.sh && nvm install ${NODE_VERSION} && nvm clear-cache" + fi + exit 0 +fi + +# Create nvm group, nvm dir, and set sticky bit +if ! cat /etc/group | grep -e "^nvm:" > /dev/null 2>&1; then + groupadd -r nvm +fi +umask 0002 +usermod -a -G nvm ${USERNAME} +mkdir -p ${NVM_DIR} +chown :nvm ${NVM_DIR} +chmod g+s ${NVM_DIR} +su ${USERNAME} -c "$(cat << EOF + set -e + umask 0002 + # Do not update profile - we'll do this manually + export PROFILE=/dev/null + curl -so- https://raw.githubusercontent.com/nvm-sh/nvm/v${NVM_VERSION}/install.sh | bash + source ${NVM_DIR}/nvm.sh + if [ "${NODE_VERSION}" != "" ]; then + nvm alias default ${NODE_VERSION} + fi + nvm clear-cache +EOF +)" 2>&1 +# Update rc files +if [ "${UPDATE_RC}" = "true" ]; then +updaterc "$(cat < /dev/null 2>&1; then + to_install="${to_install} make" + fi + if ! type gcc > /dev/null 2>&1; then + to_install="${to_install} gcc" + fi + if ! type g++ > /dev/null 2>&1; then + to_install="${to_install} g++" + fi + if ! type python3 > /dev/null 2>&1; then + to_install="${to_install} python3-minimal" + fi + if [ ! -z "${to_install}" ]; then + apt_get_update_if_needed + apt-get -y install ${to_install} + fi +fi + +echo "Done!" diff --git a/containers/python-3-pyspark-databricks-dbx/.devcontainer/library-scripts/python-debian.sh b/containers/python-3-pyspark-databricks-dbx/.devcontainer/library-scripts/python-debian.sh new file mode 100644 index 0000000000..bdfc4d8bc7 --- /dev/null +++ b/containers/python-3-pyspark-databricks-dbx/.devcontainer/library-scripts/python-debian.sh @@ -0,0 +1,354 @@ +#!/usr/bin/env bash +#------------------------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. +#------------------------------------------------------------------------------------------------------------- +# +# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/python.md +# Maintainer: The VS Code and Codespaces Teams +# +# Syntax: ./python-debian.sh [Python Version] [Python intall path] [PIPX_HOME] [non-root user] [Update rc files flag] [install tools flag] [Use Oryx if available flag] [Optimize when building from source flag] + +PYTHON_VERSION=${1:-"latest"} # 'system' checks the base image first, else installs 'latest' +PYTHON_INSTALL_PATH=${2:-"/usr/local/python"} +export PIPX_HOME=${3:-"/usr/local/py-utils"} +USERNAME=${4:-"automatic"} +UPDATE_RC=${5:-"true"} +INSTALL_PYTHON_TOOLS=${6:-"true"} +USE_ORYX_IF_AVAILABLE=${7:-"true"} +OPTIMIZE_BUILD_FROM_SOURCE=${8-"false"} + +DEFAULT_UTILS=("pylint" "flake8" "autopep8" "black" "yapf" "mypy" "pydocstyle" "pycodestyle" "bandit" "pipenv" "virtualenv") +PYTHON_SOURCE_GPG_KEYS="64E628F8D684696D B26995E310250568 2D347EA6AA65421D FB9921286F5E1540 3A5CA953F73C700D 04C367C218ADD4FF 0EDDC5F26A45C816 6AF053F07D9DC8D2 C9BE28DEE6DF025C 126EB563A74B06BF D9866941EA5BBD71 ED9D77D5" +GPG_KEY_SERVERS="keyserver hkp://keyserver.ubuntu.com:80 +keyserver hkps://keys.openpgp.org +keyserver hkp://keyserver.pgp.com" + +set -e + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +# Ensure that login shells get the correct path if the user updated the PATH using ENV. +rm -f /etc/profile.d/00-restore-env.sh +echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh +chmod +x /etc/profile.d/00-restore-env.sh + +# Determine the appropriate non-root user +if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then + USERNAME="" + POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") + for CURRENT_USER in ${POSSIBLE_USERS[@]}; do + if id -u ${CURRENT_USER} > /dev/null 2>&1; then + USERNAME=${CURRENT_USER} + break + fi + done + if [ "${USERNAME}" = "" ]; then + USERNAME=root + fi +elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then + USERNAME=root +fi + +updaterc() { + if [ "${UPDATE_RC}" = "true" ]; then + echo "Updating /etc/bash.bashrc and /etc/zsh/zshrc..." + if [[ "$(cat /etc/bash.bashrc)" != *"$1"* ]]; then + echo -e "$1" >> /etc/bash.bashrc + fi + if [ -f "/etc/zsh/zshrc" ] && [[ "$(cat /etc/zsh/zshrc)" != *"$1"* ]]; then + echo -e "$1" >> /etc/zsh/zshrc + fi + fi +} + +# Get central common setting +get_common_setting() { + if [ "${common_settings_file_loaded}" != "true" ]; then + curl -sfL "https://aka.ms/vscode-dev-containers/script-library/settings.env" 2>/dev/null -o /tmp/vsdc-settings.env || echo "Could not download settings file. Skipping." + common_settings_file_loaded=true + fi + if [ -f "/tmp/vsdc-settings.env" ]; then + local multi_line="" + if [ "$2" = "true" ]; then multi_line="-z"; fi + local result="$(grep ${multi_line} -oP "$1=\"?\K[^\"]+" /tmp/vsdc-settings.env | tr -d '\0')" + if [ ! -z "${result}" ]; then declare -g $1="${result}"; fi + fi + echo "$1=${!1}" +} + +# Import the specified key in a variable name passed in as +receive_gpg_keys() { + get_common_setting $1 + local keys=${!1} + get_common_setting GPG_KEY_SERVERS true + local keyring_args="" + if [ ! -z "$2" ]; then + mkdir -p "$(dirname \"$2\")" + keyring_args="--no-default-keyring --keyring $2" + fi + + # Use a temporary locaiton for gpg keys to avoid polluting image + export GNUPGHOME="/tmp/tmp-gnupg" + mkdir -p ${GNUPGHOME} + chmod 700 ${GNUPGHOME} + echo -e "disable-ipv6\n${GPG_KEY_SERVERS}" > ${GNUPGHOME}/dirmngr.conf + # GPG key download sometimes fails for some reason and retrying fixes it. + local retry_count=0 + local gpg_ok="false" + set +e + until [ "${gpg_ok}" = "true" ] || [ "${retry_count}" -eq "5" ]; + do + echo "(*) Downloading GPG key..." + ( echo "${keys}" | xargs -n 1 gpg -q ${keyring_args} --recv-keys) 2>&1 && gpg_ok="true" + if [ "${gpg_ok}" != "true" ]; then + echo "(*) Failed getting key, retring in 10s..." + (( retry_count++ )) + sleep 10s + fi + done + set -e + if [ "${gpg_ok}" = "false" ]; then + echo "(!) Failed to get gpg key." + exit 1 + fi +} + +# Figure out correct version of a three part version number is not passed +find_version_from_git_tags() { + local variable_name=$1 + local requested_version=${!variable_name} + if [ "${requested_version}" = "none" ]; then return; fi + local repository=$2 + local prefix=${3:-"tags/v"} + local separator=${4:-"."} + local last_part_optional=${5:-"false"} + if [ "$(echo "${requested_version}" | grep -o "." | wc -l)" != "2" ]; then + local escaped_separator=${separator//./\\.} + local last_part + if [ "${last_part_optional}" = "true" ]; then + last_part="(${escaped_separator}[0-9]+)?" + else + last_part="${escaped_separator}[0-9]+" + fi + local regex="${prefix}\\K[0-9]+${escaped_separator}[0-9]+${last_part}$" + local version_list="$(git ls-remote --tags ${repository} | grep -oP "${regex}" | tr -d ' ' | tr "${separator}" "." | sort -rV)" + if [ "${requested_version}" = "latest" ] || [ "${requested_version}" = "current" ] || [ "${requested_version}" = "lts" ]; then + declare -g ${variable_name}="$(echo "${version_list}" | head -n 1)" + else + set +e + declare -g ${variable_name}="$(echo "${version_list}" | grep -E -m 1 "^${requested_version//./\\.}([\\.\\s]|$)")" + set -e + fi + fi + if [ -z "${!variable_name}" ] || ! echo "${version_list}" | grep "^${!variable_name//./\\.}$" > /dev/null 2>&1; then + echo -e "Invalid ${variable_name} value: ${requested_version}\nValid values:\n${version_list}" >&2 + exit 1 + fi + echo "${variable_name}=${!variable_name}" +} + +# Use Oryx to install something using a partial version match +oryx_install() { + local platform=$1 + local requested_version=$2 + local target_folder=${3:-none} + local ldconfig_folder=${4:-none} + echo "(*) Installing ${platform} ${requested_version} using Oryx..." + check_packages jq + # Soft match if full version not specified + if [ "$(echo "${requested_version}" | grep -o "." | wc -l)" != "2" ]; then + local version_list="$(oryx platforms --json | jq -r ".[] | select(.Name == \"${platform}\") | .Versions | sort | reverse | @tsv" | tr '\t' '\n' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$')" + if [ "${requested_version}" = "latest" ] || [ "${requested_version}" = "current" ] || [ "${requested_version}" = "lts" ]; then + requested_version="$(echo "${version_list}" | head -n 1)" + else + set +e + requested_version="$(echo "${version_list}" | grep -E -m 1 "^${requested_version//./\\.}([\\.\\s]|$)")" + set -e + fi + if [ -z "${requested_version}" ] || ! echo "${version_list}" | grep "^${requested_version//./\\.}$" > /dev/null 2>&1; then + echo -e "(!) Oryx does not support ${platform} version $2\nValid values:\n${version_list}" >&2 + return 1 + fi + echo "(*) Using ${requested_version} in place of $2." + fi + + export ORYX_ENV_TYPE=vsonline-present ORYX_PREFER_USER_INSTALLED_SDKS=true ENABLE_DYNAMIC_INSTALL=true DYNAMIC_INSTALL_ROOT_DIR=/opt + oryx prep --skip-detection --platforms-and-versions "${platform}=${requested_version}" + local opt_folder="/opt/${platform}/${requested_version}" + if [ "${target_folder}" != "none" ] && [ "${target_folder}" != "${opt_folder}" ]; then + ln -s "${opt_folder}" "${target_folder}" + fi + # Update library path add to conf + if [ "${ldconfig_folder}" != "none" ]; then + echo "/opt/${platform}/${requested_version}/lib" >> "/etc/ld.so.conf.d/${platform}.conf" + ldconfig + fi +} + +# Function to run apt-get if needed +apt_get_update_if_needed() +{ + if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update + else + echo "Skipping apt-get update." + fi +} + +# Checks if packages are installed and installs them if not +check_packages() { + if ! dpkg -s "$@" > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends "$@" + fi +} + +install_from_source() { + if [ -d "${PYTHON_INSTALL_PATH}" ]; then + echo "(!) Path ${PYTHON_INSTALL_PATH} already exists. Remove this existing path or select a different one." + exit 1 + fi + echo "(*) Building Python ${PYTHON_VERSION} from source..." + # Install prereqs if missing + check_packages curl ca-certificates gnupg2 tar make gcc libssl-dev zlib1g-dev libncurses5-dev \ + libbz2-dev libreadline-dev libxml2-dev xz-utils libgdbm-dev tk-dev dirmngr \ + libxmlsec1-dev libsqlite3-dev libffi-dev liblzma-dev uuid-dev + if ! type git > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends git + fi + + # Find version using soft match + find_version_from_git_tags PYTHON_VERSION "https://github.com/python/cpython" + + # Download tgz of source + mkdir -p /tmp/python-src "${PYTHON_INSTALL_PATH}" + cd /tmp/python-src + local tgz_filename="Python-${PYTHON_VERSION}.tgz" + local tgz_url="https://www.python.org/ftp/python/${PYTHON_VERSION}/${tgz_filename}" + echo "Downloading ${tgz_filename}..." + curl -sSL -o "/tmp/python-src/${tgz_filename}" "${tgz_url}" + + # Verify signature + receive_gpg_keys PYTHON_SOURCE_GPG_KEYS + echo "Downloading ${tgz_filename}.asc..." + curl -sSL -o "/tmp/python-src/${tgz_filename}.asc" "${tgz_url}.asc" + gpg --verify "${tgz_filename}.asc" + + # Update min protocol for testing only - https://bugs.python.org/issue41561 + cp /etc/ssl/openssl.cnf /tmp/python-src/ + sed -i -E 's/MinProtocol[=\ ]+.*/MinProtocol = TLSv1.0/g' /tmp/python-src/openssl.cnf + export OPENSSL_CONF=/tmp/python-src/openssl.cnf + + # Untar and build + tar -xzf "/tmp/python-src/${tgz_filename}" -C "/tmp/python-src" --strip-components=1 + local config_args="" + if [ "${OPTIMIZE_BUILD_FROM_SOURCE}" = "true" ]; then + config_args="--enable-optimizations" + fi + ./configure --prefix="${PYTHON_INSTALL_PATH}" --with-ensurepip=install ${config_args} + make -j 8 + make install + cd /tmp + rm -rf /tmp/python-src ${GNUPGHOME} /tmp/vscdc-settings.env + chown -R ${USERNAME} "${PYTHON_INSTALL_PATH}" + ln -s ${PYTHON_INSTALL_PATH}/bin/python3 ${PYTHON_INSTALL_PATH}/bin/python + ln -s ${PYTHON_INSTALL_PATH}/bin/pip3 ${PYTHON_INSTALL_PATH}/bin/pip + ln -s ${PYTHON_INSTALL_PATH}/bin/idle3 ${PYTHON_INSTALL_PATH}/bin/idle + ln -s ${PYTHON_INSTALL_PATH}/bin/pydoc3 ${PYTHON_INSTALL_PATH}/bin/pydoc + ln -s ${PYTHON_INSTALL_PATH}/bin/python3-config ${PYTHON_INSTALL_PATH}/bin/python-config +} + +install_using_oryx() { + if [ -d "${PYTHON_INSTALL_PATH}" ]; then + echo "(!) Path ${PYTHON_INSTALL_PATH} already exists. Remove this existing path or select a different one." + exit 1 + fi + oryx_install "python" "${PYTHON_VERSION}" "${PYTHON_INSTALL_PATH}" "lib" || return 1 + ln -s ${PYTHON_INSTALL_PATH}/bin/idle3 ${PYTHON_INSTALL_PATH}/bin/idle + ln -s ${PYTHON_INSTALL_PATH}/bin/pydoc3 ${PYTHON_INSTALL_PATH}/bin/pydoc + ln -s ${PYTHON_INSTALL_PATH}/bin/python3-config ${PYTHON_INSTALL_PATH}/bin/python-config +} + +# Ensure apt is in non-interactive to avoid prompts +export DEBIAN_FRONTEND=noninteractive + +# General requirements +check_packages curl ca-certificates gnupg2 tar make gcc libssl-dev zlib1g-dev libncurses5-dev \ + libbz2-dev libreadline-dev libxml2-dev xz-utils libgdbm-dev tk-dev dirmngr \ + libxmlsec1-dev libsqlite3-dev libffi-dev liblzma-dev uuid-dev + + +# Install python from source if needed +if [ "${PYTHON_VERSION}" != "none" ]; then + # If the os-provided versions are "good enough", detect that and bail out. + if [ ${PYTHON_VERSION} = "os-provided" ] || [ ${PYTHON_VERSION} = "system" ]; then + check_packages python3 python3-doc python3-pip python3-venv python3-dev python3-tk + PYTHON_INSTALL_PATH="/usr" + should_install_from_source=false + elif [ "$(dpkg --print-architecture)" = "amd64" ] && [ "${USE_ORYX_IF_AVAILABLE}" = "true" ] && type oryx > /dev/null 2>&1; then + install_using_oryx || should_install_from_source=true + else + should_install_from_source=true + fi + if [ "${should_install_from_source}" = "true" ]; then + install_from_source + fi + updaterc "if [[ \"\${PATH}\" != *\"${PYTHON_INSTALL_PATH}/bin\"* ]]; then export PATH=${PYTHON_INSTALL_PATH}/bin:\${PATH}; fi" +fi + +# If not installing python tools, exit +if [ "${INSTALL_PYTHON_TOOLS}" != "true" ]; then + echo "Done!" + exit 0 +fi + +export PIPX_BIN_DIR="${PIPX_HOME}/bin" +export PATH="${PYTHON_INSTALL_PATH}/bin:${PIPX_BIN_DIR}:${PATH}" + +# Create pipx group, dir, and set sticky bit +if ! cat /etc/group | grep -e "^pipx:" > /dev/null 2>&1; then + groupadd -r pipx +fi +usermod -a -G pipx ${USERNAME} +umask 0002 +mkdir -p ${PIPX_BIN_DIR} +chown :pipx ${PIPX_HOME} ${PIPX_BIN_DIR} +chmod g+s ${PIPX_HOME} ${PIPX_BIN_DIR} + +# Update pip if not using os provided python +if [ ${PYTHON_VERSION} != "os-provided" ] && [ ${PYTHON_VERSION} != "system" ]; then + echo "Updating pip..." + ${PYTHON_INSTALL_PATH}/bin/python3 -m pip install --no-cache-dir --upgrade pip +fi + +# Install tools +echo "Installing Python tools..." +export PYTHONUSERBASE=/tmp/pip-tmp +export PIP_CACHE_DIR=/tmp/pip-tmp/cache +pipx_path="" +if ! type pipx > /dev/null 2>&1; then + pip3 install --disable-pip-version-check --no-cache-dir --user pipx 2>&1 + /tmp/pip-tmp/bin/pipx install --pip-args=--no-cache-dir pipx + pipx_path="/tmp/pip-tmp/bin/" +fi +for util in ${DEFAULT_UTILS[@]}; do + if ! type ${util} > /dev/null 2>&1; then + ${pipx_path}pipx install --system-site-packages --pip-args '--no-cache-dir --force-reinstall' ${util} + else + echo "${util} already installed. Skipping." + fi +done +rm -rf /tmp/pip-tmp + +updaterc "$(cat << EOF +export PIPX_HOME="${PIPX_HOME}" +export PIPX_BIN_DIR="${PIPX_BIN_DIR}" +if [[ "\${PATH}" != *"\${PIPX_BIN_DIR}"* ]]; then export PATH="\${PATH}:\${PIPX_BIN_DIR}"; fi +EOF +)" diff --git a/containers/python-3-pyspark-databricks-dbx/.gitignore b/containers/python-3-pyspark-databricks-dbx/.gitignore new file mode 100644 index 0000000000..02a5571502 --- /dev/null +++ b/containers/python-3-pyspark-databricks-dbx/.gitignore @@ -0,0 +1 @@ +**/databricks-config.sh diff --git a/containers/python-3-pyspark-databricks-dbx/.npmignore b/containers/python-3-pyspark-databricks-dbx/.npmignore new file mode 100644 index 0000000000..7c7cb3186f --- /dev/null +++ b/containers/python-3-pyspark-databricks-dbx/.npmignore @@ -0,0 +1,7 @@ +README.md +test-project +history +.devcontainer/library-scripts +definition-manifest.json +.vscode +.npmignore diff --git a/containers/python-3-pyspark-databricks-dbx/.vscode/launch.json b/containers/python-3-pyspark-databricks-dbx/.vscode/launch.json new file mode 100644 index 0000000000..8adba7b49a --- /dev/null +++ b/containers/python-3-pyspark-databricks-dbx/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python (Integrated Terminal)", + "type": "python", + "request": "launch", + "program": "${workspaceFolder}/test-project/hello.py", + "console": "integratedTerminal" + } + ] +} \ No newline at end of file diff --git a/containers/python-3-pyspark-databricks-dbx/.vscode/tasks.json b/containers/python-3-pyspark-databricks-dbx/.vscode/tasks.json new file mode 100644 index 0000000000..ad7fd1cec4 --- /dev/null +++ b/containers/python-3-pyspark-databricks-dbx/.vscode/tasks.json @@ -0,0 +1,71 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "New dbx project", + "type": "shell", + "command": "bash ${workspaceFolder}/scripts/new-dbx-project.sh ${workspaceFolder} \"${input:dbx_project_name}\" ${input:databricks_host} ${input:databricks_token} ${input:databricks_cluster_id}", + "presentation": { + "echo": true, + "reveal": "always", + "focus": true, + "panel": "new", + "showReuseMessage": true, + "clear": false + }, + "problemMatcher":"$eslint-compact" + }, + { + "label": "New local pyspark project", + "type": "shell", + "command": "bash ${workspaceFolder}/scripts/new-local-pyspark-project.sh ${workspaceFolder} \"${input:dbx_project_name}\"", + "presentation": { + "echo": true, + "reveal": "always", + "focus": true, + "panel": "new", + "showReuseMessage": true, + "clear": false + }, + "problemMatcher":"$eslint-compact" + }, + { + "label": "Run dbx project", + "type": "shell", + "command": "bash ${workspaceFolder}/scripts/run-dbx-project.sh ${workspaceFolder} \"${input:dbx_project_name}\"", + "presentation": { + "echo": true, + "reveal": "always", + "focus": true, + "panel": "new", + "showReuseMessage": true, + "clear": false + }, + "problemMatcher":"$eslint-compact" + } + ], + "inputs": [ + { + "id": "databricks_host", + "description": "Enter your Databricks workspace host", + "type": "promptString" + }, + { + "id": "databricks_token", + "description": "Enter your Databricks workspace PAT", + "type": "promptString" + }, + { + "id": "databricks_cluster_id", + "description": "Enter your Databricks cluster ID", + "type": "promptString" + }, + { + "id": "dbx_project_name", + "description": "Enter the name of your project", + "type": "promptString" + } + ] +} diff --git a/containers/python-3-pyspark-databricks-dbx/README.md b/containers/python-3-pyspark-databricks-dbx/README.md new file mode 100644 index 0000000000..2fc984c74b --- /dev/null +++ b/containers/python-3-pyspark-databricks-dbx/README.md @@ -0,0 +1,198 @@ +# Python 3 - Pyspark - Databricks - dbx + +## Summary + +Develop your own **Pyspark** applications under a fully configured **Python 3** development environment. Test them locally or send jobs to your **Databricks** workspace using **dbx**. Get all of these functionalities integrated directly in **VS Code** without any extra configuration work! + +![Access to docs](./README/img/docum.png) + +![Code analysis](./README/img/code-analysis.png) + +*This definition is an extension of the **Python 3** containerized one. See the details [here](https://github.com/microsoft/vscode-dev-containers/blob/main/containers/python-3/README.md). Check the official repo with all the containerized development environments for **Visual Studio Code** [here](https://github.com/microsoft/vscode-dev-containers). + +*The integration with **dbx** and a couple of examples are demonstrated in [this](https://docs.microsoft.com/en-us/azure/databricks/dev-tools/dbx) official document from Microsoft. + +## Using this definition + +### **Set up the containerized environment** + +#### **Step 1** - Clone the repo using **git** + +```bash +git clone +``` + +#### **Step 2** - If you prefer, to simplify things get rid of the version control now + +Windows console: + +```bash +rmdir .git /s /q +``` + +Linux & Mac: + +```bash +rm -rf .git/ +``` +### **Step 3** - Get **VS Code Remote - Containers extension** + +You can get it [here](https://aka.ms/vscode-remote/download/containers). + +### **Step 4** - Load the containerized environment + +Click on the **Remote Window toolbox** and then `Open Folder in Container...` + +![The Remote Window toolbox](./README/img/remote-windows-toolbox.png) + +You will need to have **Docker** installed on your machine. If you do not have it, the **VS Code Remote - Containers extension** will guide you to get it. + +### **Step 5** - Browse to your copy of the repo +If everything went well, **VS Code** should be now connected to the container image which should be running a **VS Code Server** instance inside. + +### **Creating a new local pyspark project** + +### **Step 1** - Press `CTRL + SHIFT + P` and select `Tasks: Run Task` and then `New local pyspark project` + +You will need to choose a name for your project. A new folder with the name of your project will be created. + +Press a key to close the terminal. + +### **Step 2** - Add your code to the recently created file `project_name/project_name.py` + +By default, a simple **pyspark** template is deployed: + +```python +# For testing and debugging of local objects, run +# "pip install pyspark=X.Y.Z", where "X.Y.Z" +# matches the version of PySpark +# on your target clusters. +from pyspark.sql import SparkSession + +from pyspark.sql.types import * +from datetime import date + +spark = SparkSession.builder.appName("dbx-demo").getOrCreate() + +# Create a DataFrame consisting of high and low temperatures +# by airport code and date. +schema = StructType([ + StructField('AirportCode', StringType(), False), + StructField('Date', DateType(), False), + StructField('TempHighF', IntegerType(), False), + StructField('TempLowF', IntegerType(), False) +]) + +data = [ + [ 'BLI', date(2021, 4, 3), 52, 43], + [ 'BLI', date(2021, 4, 2), 50, 38], + [ 'BLI', date(2021, 4, 1), 52, 41], + [ 'PDX', date(2021, 4, 3), 64, 45], + [ 'PDX', date(2021, 4, 2), 61, 41], + [ 'PDX', date(2021, 4, 1), 66, 39], + [ 'SEA', date(2021, 4, 3), 57, 43], + [ 'SEA', date(2021, 4, 2), 54, 39], + [ 'SEA', date(2021, 4, 1), 56, 41] +] + +temps = spark.createDataFrame(data, schema) + +... +``` + +### **Step 3** - Once you are ready, press `F5` to run your **pyspark** code locally + +If everything works fine, you should see the result of the execution on the current terminal window. + +### **Creating a new dbx project for Dabricks connectivity** + +### **Step 1** - First of all, check that your python versions for **Databricks** and for the container are compatible + +To check **Databricks**, use a simple magic command in a notebook cell: + +```text +%sh +python --version +``` + +To check the container, launch this **python** command from your current terminal: + +```bash +python --version +``` + +### **Step 2** - Press `CTRL + SHIFT + P` and select `Tasks: Run Task` and then `New dbx project` + +You will need to choose a name for your project and the following parameters to set up your **Databricks** connection: + +- Host of your **Databricks** workspace. +- PAT (Personal Access Token) of your **Databricks** workspace. +- ID of the cluster of your **Databricks workspace you want to use. + +A new folder with the name of your project will be created. The **Databricks** configuration files will be stored inside under `project_name/conf/databricks-config.sh` file, but for security reasons these data will not be tracked by the version control (a proper glob pattern was added to **.gitignore**). Please, note that if you need it, you can modify manually the values in this file to update them: + +```bash +export DATABRICKS_HOST="" +export DATABRICKS_TOKEN="" +export DATABRICKS_CLUSTER_ID="" +``` + +Press a key to close the terminal. + +### **Step 3** - Add your code to the recently created file `project_name/project_name.py` + +By default, a simple **pyspark** template is deployed: + +```python +# For testing and debugging of local objects, run +# "pip install pyspark=X.Y.Z", where "X.Y.Z" +# matches the version of PySpark +# on your target clusters. +from pyspark.sql import SparkSession + +from pyspark.sql.types import * +from datetime import date + +spark = SparkSession.builder.appName("dbx-demo").getOrCreate() + +# Create a DataFrame consisting of high and low temperatures +# by airport code and date. +schema = StructType([ + StructField('AirportCode', StringType(), False), + StructField('Date', DateType(), False), + StructField('TempHighF', IntegerType(), False), + StructField('TempLowF', IntegerType(), False) +]) + +data = [ + [ 'BLI', date(2021, 4, 3), 52, 43], + [ 'BLI', date(2021, 4, 2), 50, 38], + [ 'BLI', date(2021, 4, 1), 52, 41], + [ 'PDX', date(2021, 4, 3), 64, 45], + [ 'PDX', date(2021, 4, 2), 61, 41], + [ 'PDX', date(2021, 4, 1), 66, 39], + [ 'SEA', date(2021, 4, 3), 57, 43], + [ 'SEA', date(2021, 4, 2), 54, 39], + [ 'SEA', date(2021, 4, 1), 56, 41] +] + +temps = spark.createDataFrame(data, schema) + +... +``` + +### **Step 4** - Once you are ready, press `CTRL + SHIFT + P` and select `Tasks: Run Task` and then `Run dbx project` to send a job to your Databricks workspace + +You will be asked again about the name of your project. If everything works fine, you should see the result of the execution on a new terminal window: + +![Results of the execution](./README/img/results.png) + +Press a key to close the terminal. + +### **Step 5** - Finally, to close the remote session with the container, click again on the **Remote Windows toolbox** and select `Close Remote Connection` + +## License + +Copyright (c) Microsoft Corporation. All rights reserved. + +Licensed under the MIT License. See [LICENSE](https://github.com/microsoft/vscode-dev-containers/blob/main/LICENSE) diff --git a/containers/python-3-pyspark-databricks-dbx/README/img/code-analysis.png b/containers/python-3-pyspark-databricks-dbx/README/img/code-analysis.png new file mode 100644 index 0000000000000000000000000000000000000000..51e252aa33b86909bd5e4918e315d1bba6944364 GIT binary patch literal 121653 zcmYJZQ;;r96Rp{{ZF{$OW4CSFwr$(CZQHhO<89mS`OeJwV^&4o|XA z4U%}rqu#H_j^ABba;i@2rX<72BjxM%(SFtmpWnwP*W1^Ruixa|{pXUN?~UcRFYbHh zTPAN``OzaQoUg54@4*<);MenMt&bn-)~n#2-dDT+Vopow_an)>$JZD4@omV*JAFsn z$D`t@8K|$1-dpO!_tj_5V%V+9v)$_>q9eKFR=YyveR2HPCzqdU{1f)lepOE&R~;;0 zuba2T`IscsIK$TG{mN$^=#x~upZDnx&Npvg-;dPY(OWOcH+d=mm$di&nj!F;_l;qg zZ|C24L*d&cgDmv5-PXJBZr4`NX;W7MEHd3*tN!+4rH<;>Dr;q`=vwR=r8iu__sA2p(0 zS&^Kb2R<>SNr8`*-O^mW(b{yRlGSWSRDvAyPsuV#JTB{sc1^0LRaH-Hs;2F#WK~Ve zu4o)jK)|Y0UC;CGSh%fy(Lf7C9pRVpXohJn+f;Sa4DVQ>x2kUIWMx%N?`SNSjlN-; zj@y<`9O^bOwJfjeWJOty?`X-YZRh(YUE8kfC%&KYb=FFs&lf`@Mz2N`gOaDtH>uT^ za{70VFj989qv)t1f^jzuD4MhKtT!x&n29XMBhMllhDI?pyDg+iG};Mj;m)1d&Cm39 zUkba~_1>h`ms*CC7Ttq<`+UrHWaXMXxld3JIHx4+~)N69*=S07%euQ?? z7FjLQn0nDR{#ryAsyIfDI!wPkRH%TM6Fnzu8rRSbSo5~i2Z+g-afKJ?a?ocnN4qmD z7@u>$gY$aT^}>m@uZxY%u`dmuT(x0}=PLnE7{|r57od@=qX>-Bid>n6S>j(LrT~jD zh_PmnR#^Cz^U;|_%*JiBnMyB8Sko)mi=tNWkF%wFn`Hk)B^QzHY{1=c6oWo}!Jd(qCWKqo%7+st)TZ zCvYz9Y+*SX-rOPGzfn9)ZAY+btScTlZ7i*Pom{Hnh|sn+$ld%zrr>>RI!-U~(4Pe? zhD0@JcbYvP_!gJKi#%{nA}+oEw1RUO3EAXd9?aMTn^z3G=1^dJ&=on#;3KO_yVh-5 z&7Jl*CK1rf)tfj5x2duQGWDC}ois)?2_YMnEZ(s@yf?lww`MH{&!l4j{H(J`?d~Bx zNx1Qk*W#fNPPO#%q%z4o?1U8JL`(IylD>5<49pW{Mh8z_2o>0$+u3IJ!S#z74SJD0 zlGI==*spoQ0+%|yO7!Q;CH>pXR3%bV^9L*g#q%V{r9^hR*UGznMr8-Hz%}Iyqp|&D z!_YgqfbC;6Un|)}20OjBNcVkBZ(ynLw2O zVmXNtW1EXV^L;;eP1s z{}2@#TVM1HcNxb6@zYv>N(InBr{J%NbSxBt(4@p5f~Z289+G%-daO6o5e)4 zgx3k6wqE#ZRZqk#cxQAi0>F%!lA+9B9)Lx+;Y-4XR{wm>q~0-V{c+8z$Ox; zz`Vygk3V4+zrYt}pjQ|%!6dzfHQY%QX7ll@@8BMH=GzjFV&NT&R6T7bB8$)5>^JUX zp?T5nML5e8%BC}eWR%;8ITeQ_NRY$HGaq}LQ3M!;A^4|sUScyEG{Jut8i4GJ9w-A9 zE{#LNWx923i)lXrfT_V#!JgidD)|a5-5?L7tA-kJ!4(IB15a9&L1UIHO6p}p=Cpe_ zj)6Uud{;eCC)oz?RahG}YK?_X*UE|{9Z?G)HMs_8B~n~b|3Hs`2dxfuDfq(!v$DO{ zf=(&2uj?3dr9=HGK7{0;aJq`Ll20)yax`qXiC1_ceVZQTvC>CRJ_#q{+52q8Z3MCa zei_1UM|wi-17bPgN_2Z$sId3ri$5v_Hl@Zr0g6*3*U)~00Xge2Qgd#Js?1K*F)DD! zQJJz^k$#@O@{eCsi~cNBfx$vH*S>g61kQff1S*k0Qc)uC7hT>iM?%?W4=1ew6L3{i z>7=qzBu92X0SKPF&W|Iz}No^!%=NP~MSkGzx6Vy}I_w^cJPo zC`UgEmW7{w9Ax5Nrq#U{Tg`8v0MP4Gw9Z!*fl=|}R0t^^pNX<@@-U!Mpem_D&B9$M zgs&zINs9=!Hb}G=~z_wE@2(hiNuGL2Y|?_9A9)8BrP?Flgome z@bv>S(QP>Ds(U3FG}XW*`e>POZL#IL{>DIlzx<)6gGLrALG-18PEK29fzm{vNjnUx z$8f~ji_K(%*NFc0Paxd;dbSe2Nt?w@9Y*>W#Br;1nB+)kfYHJFO=A%>(Nq5dM7Q%hjA9LSvYDNTndU5KSRXa9?!A(kmW8#t^; zaQ!&6SiPq>z$zzGMRU-SzSX)lmeIy1?O#`xyv@@%l{?mvNr%$D;wnm7yDys=)ENr2 zW9ZR}toR>=p9Gka5(l=a?FHA@XejLMDeg&Dd1W$)*I=-Wsah z4s&pQ)H|R~l-B_L@JuuXiW2BUYPJJvNn}7=F2ue(yz4W*XZ5VX-)WSIPQd2m8l4PpN0j!@;}0mHro{|+bpp9 zr=f2aIVMCTic3Hb%WkMjBgf>6HaOrU?J(fffnrKi0Of^nh~l<1GjLl_-K?+eH$yDLp`8^xe}Db9q~Uqcj(){6l6f9 zcr~HZ1L70*ffeGZ;bfu<^y2GEvG#bZg06{0d*+bQvdTh=f0Y<#&=Znjtm1LSi~}x| zSOs9^oG=k#1U61?cftN5eEQ~S77dm~h^6t@ zFUB6yJQrzu4AzI6W6EQ9a*#W`pgRmiNm3dgJ3%fv@8fc?AAZ{;+EVsxiLW;rhHw%8 zI!|G+CMV#GXUZ7nnuyq?@rZiE62@=~{N>wfrLF?Xx?@;qGyt87VxZ_gWVO7^z?9_?V!RUFlnN(U zlg-uqrXemFRH+LTURxtv{y6itX*Fywc)kKXK?6OX4@lsEFv}9&HrF_Dv^8u6J8mQH ztypP-N*bFL5r;bHvkg=HQ=vCeDW0D7Actxut3j9_i;kt>q%}O3=ng5?T_SM3KP)%6 z|Jwx`N}B-hu-N^XHCqH0@ypOcFWCjZXgS7(Uao`DPr|xW5eh$k`-%}+le;uX%xh5K z#TQaMD`bC2EX&10^NFplAtD@Xy?VK|q~SWxE4EK@4GCiCc zVklkUpnwCEdMgI^;9|YZz7Q(zR~w(1>=Tb8)FlK@agYimK|RpoM&_`C(h%;v#S`)l z=%b{!MSeu(5Q;(EaQZxd=#GLbGTCA~IoU^2B!e^GWE2pEbI_eHQ9bf%@|Lj&4meyo zdz+hOX#inAEQbv~uZ^@e$sfC1I`p*Sz6j+5f{5F03g)~uNp=z|R?hB*r^TO%9!K?TMGM~#HY2+>Vx0U@vQO78^9O$d@qw+ZX5k}oVk zW1ZzO(n9%KNOL&UBW^U3Mo~52LjA%PK-lU5;*KPjH^`Wm?A=YoMe+YWo0K z1Bu2~T>rHNI70a-$Ztrp@$GrlCsgJg&&0*p9*x=ku+zqTj=`J#jwl&W40Drp{3l(Ng+wdQqzf{9mWDPgR%Q!RHOp>zn&t)0OstJu%1n zDC9B8$mdWiolb3z@zx#VSf0vMcwF@!{ ze)IM|r?hr%_b$s2qU1A=17w0hUnDbw3`?v;K?>$GzRShOwX|;FGIfiVw8`S(L56xv zW611@%<*<3Y|TU6h#8aW2ioe3l{X?poGkef|In=Ch9`-_#$VKKKsmlqAYm>DI_Xzd zP)~E1CP46nUX~j-kXa0pw{+JRx|h^C^o^ln$OIb`>zm!-y8O*mXOBrTULdJDbrc3y zXg96IeQ#|Vuv(Yanpi4oHZVhHR5U-$EfGqq-dMxE^0)&GX2)Z-3dCs|BkM($U9=be z+eg^LlR@PG3bmzjoLZ*1hQ_?N1wy+9rY}b8GVeZW?2?sbMv99X0TF@ZM`2TEnS?Qf zrG!aiF0(N(QO>1{nQmlsgNqppjqzzvHW10?I8mNv!VAr}=H`t6%V;u&!tGIJ@D}Dr zbi^bXH6STR9-$^VDr{!Cu0TauX5sbYR8&A>aRg;C-b^rd;sDVODYKE90zKSLUN>M% z(A0eqeVjP@8=HdzYMFGF3;~ZNgpZPv+dx8QCp8Hv9E*slFDEr8)SseQgPM6FNa~WP z6-R*S;0DB{ei8B-acbn5WGRyw*JNDPX7%aTg|d)&g$?4fzfk)H-5Y(V~Fd z2Sqc@G!?6XO-2FrZHHp6{uf?(e!p66jO8sC1hEQua#3PRRCv$lAUk?S|)5D)Bd=m|TpG=t*XKWBAvHm#hFShd&SSYe78l43?CNfZ2#Sski9vU5i zfOe(Ppf&{vcI1K+NVmtibmS<_q-m=7zvAT0Z1`Oi~96&ZHC~vNq{|ol?76obrkr(iZ1kC4p|!!BOibogL$+>46AX z>dQN~Ij}D&IYxvx>dR8eOa&Ovaeps7QneaoFBvpE-X>TngiV{-n8#Gi3h06V0A}bt zXY(dF!(sxj(v;y2xR_W*Yq^4q7&+X0Wc-;vXtd#$NRnH$%QA531sC~DBEk+LuY5Q> zkH|IY1dhW?RTQXmpE=kxY*S1nux-~95KPk~i-j^P$+NolJP~mP#s+vse;2_9{J3WPh?tgAhULf2bW@4B8T`Lu|*KLB4h?u!#;p9miiEiORy6t)`2($51{GfJvR{3 zYj69;Yz*c@i;~wOj5fG1WI*D8uV5VB*P!!-5;JnK08{ZVXMED-10+VgI(K(hp(&SZ zzF2l{RDTDl+`e+QGHt$zVT(dQu>^JD1I)|9vZFEA*5967^V}4lfua-HkRP?xNn9(Q z_83FR?7+zUr0SFdxXP!~3bC?$nV3VS#*U{%1t!(-?R2gZ!I0dUTw7dibscRhWGyEY zQ}8&3RdJV?%d!JE*OVa?}`RqurAq{yTzY2Epm-&ovDgc(9{#T4(nONO{rxqN$A{fD%=Dx13apm=uU!TD?Gj4A^y>T^@#BkK?=m zQo?^z@XSB@;=Jj3w3{=|b`njH_@q7tBw$#JYwjtjvw$%KbsD8 z;w2WY3j@xWB5)#@h#V;zHqxP6RjS~X%nCpJY*oO|Y7tySpqLdBD+ajcIscRIGgNYL zt;aln)gV~?dX~*zOF1~Xy1dbn1YvF|{R!7TQ+H8jIVHTMG~&9`;*hJltZm#if-s8v zC|=v7ro~=%Cjrj#m7dNZuXOyu9!N=hdygEm^${prV~PKcZgm&Ybylu*5hp9EYp$bw z!3Ys{)={));1%`OSn)hJe2;j?vY&)DlhP)%Cyqc9(4ykL8ssXpwLRcY$E(b-p^0iH zB`~z)b;2Af%GSRw8h2g*|F5v08jrdPP2yNVz1SiLm_z9blIrs=TVgIHQ5>iuZ3jI+ z2>dzU+6dt+c^bCHAg~n8zdz$R|DAi80G^hZ%g)C(kl8y{eIkYhqk4%Bibk63 zBI>lYbh24u!gh#dJHF|M-X}ai7a){iy!VZnJ)a21mjT@>4EqHf0zr0f8I+Ye;d_<2 zXghwq=96h`(a3yC$7^`nc7SJ65L_{zVaGRmlaPssKgKVZi^4=3JyQZ#`%wwISwjRV8yV{nlt5zzV&wA)Jh(Y?a~&6~ z)x#FGWl@*n1Q(A;488a^4n9rKwk2(j%ul@fY`BaJWN_P`kbnR^FtqTs(l9aENUpG3o3rn#)yJfd+oQRmAazdr@=!67HPazjCr6+rR z=Aq920*gYnBjTOSKVm-0rKr8_$;t{(k~%UBd!kzR5`>~vMj83b^Sto3OtTkqtrD2 z=Mx572noqc2nqeq8vM@?%=Ss(5g+2m8`0}4my8O+7sg{&ri4`#!7GUrsh8h`so_l5 z{s5Q3NkqdG8)(_zA2VDT7|~FJR1-k@0&;M6RCG>2`AtL99Co+gj&pk@aJvPvs~SZc z;)ABDtT0Q2Ce~9&53GaACp36N#>^-vX^i4%kFE1^fAhpV(zrSO!87PEXY40Mnq!h- zk6;fsW0Q$_NVS!y#BbP z(|<{NGF6Kr01n#fA8}Djdhwsa*ME~hby-ODX6Y$Gx$_m>Q~s~g4+vl_Yn$bjCG^L0 zY4HZd!vi@onbdXjaMQE1`^`OndIi-mam+c{y9oKuv4t>{5)%gc{XZ+er!48e2(-Pp zh7%AFiu3>3z`Le|?*9cLoh4*MAWy)d;h{+fwWn%;fQW!3gawq{H?MNcyj4b4!}5GI zJxO_h1<&w-fkg%&TmhS29g)u$D^{2yo(lCO7)Zj!R4Sa2Ea&(jWW}$#a*mE48<*eq zf+;Al+CI0wynr^jm)*ZsrZZn=GgsIm!eH?KS22BfoHJlJ|5usvOY{FzaAxTLr_nva zAk)AlQvU}xnXaPP=%;U_ogB7=fZvhib?Rw8{(?;LHB*JRTozh;1sxZE_%20<4~Y2n z8yrtzNQ1X7EFI-Z#kGX;Jn}04jKvBozWl1KfR$?p1Q6a(402eE(Q)pwGyUM zir_X}deT!A1DoP%LPAH~PP&`eQo1;Mbh-nD(v%?io{WULb6I~=nSQoPqSZa1ns$5! zm@|5x|N2gYhuI)Xv00Pt`QCV;Nzq@f`c9YMiZNLp2>a5%!KUUm#mn^3n#7e5JO@uz zHpL$2G)d6c{W|TXeWK>oZACn{LA7k7SI^Q975sEwynl=?wfJzWZ2Z;GG9Ud;`>GZx zsn*AxaxGE3{wGitA-NIIKs#OC$v83piHy7wp^NO|-Q&o+;kU@nAKPp)z3G87!I{$INNv20wNn-#z98TGc$d3#fBq{fBeA z?WO#3v7Dj5ojJVQ>9agf-)$~pd3)HLm|A}F)&7XnF&!Ow(U)lSm#zq@7j`8>Lh#Nt zA$P!O-vM{gnRyQ7#gFf;cx+8b@5nY#m7x^}4xGhJ=zl3)hXaV_2qu4JQbfyMM~oQ_ zK2)w$JBsu*)mD?NKBo9REXQ`2%EsqGmtVY~7wY8gOYZS>h^t^hCGZWsO?C7EHJP9XL zhxW-VhN&AwY!)}Ap&kOunJRbJ@IYq4g41j|Wewa1_0DuNzRy6N*_iIH%s}QF8Mb2; z|HPT;I@|rCZ!w#L;^JekQ171b<%_?7X-U-&mK*mj)?eD(X{EfU{H665XZ%;HDjI=0 z&>rgK>K>`|yNW<&MT2SEu?Ev^)x62!``ydQV~yn1T%O_b*r-ug zBF8s=(VxbY$xs$M-3P*t7#p#&h8_>YUCvk@g?q~F)_?>(9>LcIrl+hAP?xfGEgl&j zq_7a4M|uHl17U3Dbi&C$7SeF7wo_V9SW5{nMw|BE&2bwFCpE#X#~smnX;U{Lh>IT9 zl(p$ka8r(RwFvmnDbqQ{^G{uv>CSJq^d5D8ZNzB4j6{GV3-iysC+6gg7FfVd+6SX9 z9UJgqWmFg+Nx@B&Sti4b)>D;s*0C@zPZ3K$*JYitmZHM?zH_&erb}{3gtL>;=!^#s zRgO8jU6@^F{M}hAxrwY1DR=)S4q)PdUu3F;LFtqK#e#T4Q9%|(jS0wMkxxr zJuv?^I@WuLY13x8q9+(_qH-B9{(Z&pOaoCz{Oj^DX8m||u!Q)r=y(OBtJw%WMtG-G zMw(UEhdoxf^Dg}>Er9JwtyipiXdOQ2`4TIHO(2|voa140tvabtPnbT+N9lS(h+Xe~ zIpU1Hg<>-3r|ww}Cqiwuf+&=ppc*8`uZ(DAwl|hc#uceK-3iERY&3-3oArw1E!uM0 zX)aL*YDliPc&0t6{z!1++nWPVj zES);GFFVotg{AI_wK3(KG1nrh$z2KMQ7^6QoN-2pY#Wo!94EzeTxqi=2;8y(79)NQ zWrWZbtsFI&ieM+lRl#E9u{xG6ZL&^w$Afh!i(w(T{5+G~+EkAe1lA|&-OvO_S%k2k z;iQzD?#+GjLr3)p-%%_6OeZwt(I*Z8Od5R-+8$%)rasVNO&)q38$6Fe z>h(qu2+6%fQYKx5j`^zG!XK|X;)fu4Qy2Jd;vZ0d8uZ`k90AiNiE2$^yIE`^FKX^} zYqq$=Go>}?$jD)IGNV18`QB+)XbCVHZ4Uw-wko~qJ0yxMY9HMJPrqM29QsZY7Xio* z%iGtA2PhofSGEQpqPL*MYv_Frt`KXcz07NNh--~a46UOnp7;WW2mBwBMPN6DNmH>*KRb3 z9+#>{N+QG2rZ8`xV<hN@b^0zz5#DtYA+Nu-R_l~3=^vCACF34La0{pxs04r!BQn(`7IBqkIoNgR*=SbWYhZqSj`evmR- zKglp#=?!qu9wt74IvwAvA)j1fhS4;BZvl09|L`KmX(VFs($(4S(6N$`Z19cqka|r< zd;f<-;1eNJ33&@@TBc4)BL#)JKe8a;FRz-#O+Y zXv|`nYd2ZLf_LZUVfk;Rl6Z^UJ9#6z73I+&Ll&eqT&+NW958o+DoW!QMWx9+oM`)K zd{z8dhZyjuUFgulEhRsR*fnVS*;G%X4aezh8aAqfKiN++{hb>xr{)L-#y>+u!U8pZ z&rydPE2Rg8gpgV=E(x*e4qrL~>}tLJd6|&Vw*s~T@s!W zMR&*zsMvlwT#O$J#ZqG2A};L#$xqM4tbKvG{@Ukqp~3FvA-IVcEV zd4rz&tR*Gyf+thl4<@5?zZ}{)!$0uY>8WAde=SvW z-!CK(#{V`{1_LZdtE45>?M=>d*dZM_kp6d!O?!_Lqc}SK>9_XA$e#pfPkgoA?{}t! zS1%t9s&+aD1qI!6bSLaOK5uNGtSUcaferCO80U3v{rQS!OFwYlkD>{;BC5_M+~f45 ziv>bdbs7RUx7rK+!!wlGo8PzC8~?IdPtQIJ;SZg|QSdk5LYOl&<$M=y_D$Q2fxt*M z=eFX;<_|+#8FRl`)_}?jWBtqCvnTl6#e0d~b10V;FDNLefw|N6zD;dZ1+}+&0$K9-65g^N0%%h4y|>u^x}S6RrM6T-<>a#7q<}gfFN|glL5ERwtN3 zTMyXC*q(Mk>J?qu(fQ~R)~6_fQig8}rrP2NnNE3BG-G=`2_U4b4tkB_CAuZD)8$X0W@MLK=zLa-yg?D`=z*JJiHAxW|upU1D9drrFic zb4*;1g6&C+0?jiTyv~vR1Qy935O>f)&qni8b*NZb=7Kvzw@<~}&u7IV1b_uxC+xMH z?dG!=sy$&nomQspdaGyCq&fFuY~83-ll$Y^;Y|bXV;cVB$Isb`q67J`MESN1*efbe z5w;*Vc&^8=X}Z^t2MfV~8**hP9K zJQ_K&Nj$kQTlPiPe~DGK-nkiBc_xYPY$!Yk)+j^hJ@gcl#@t2u7vAUcy*f`AM{O(i z6;q-nC#v*)4|JSho@L2PHK9jKX_YD7S=k;iU%{SeJJqQ8FFq5A&!&b#K1`x;`=PDC z?syvMI$}i(w$-aR*_F7^=Od49#>}=4jNEF^qX0)D-|LQU9WN4)*+mIRm%{^GuKUr$ zjdrh*fWjdLq6FO?PE<>JeZ3Rk$ST){MoU)StS%)RYsos^9;jCFrp6g~VIu9pHScS?ee zOVvWBxstKK0yvMwfxFsLQMd}_i_1Uu zIi-x6x*tq+b)G4dYP@6kGTQ4oykxFdB4y$B()?AuN)|uQ_Pr53aY*-NK2f^OaS_Lf z7&#j4X4FxaLDX^_{1r9+P;x?G>jtBxxhE!$5PiUR$_-Yh5RNn8)zohZS2_*H$xjER zH~9C@%LcV>Ea5Z*M-^qW=b}JaLxB^Nfp1ixB!NwA|NR1boTJ%320>#+C8Gx%CY?G1 z|0Qhe1KxNxATC4K4Z8umX(Q(ip{Oggpfj-i@2_wsfHU_DeK^+SuZHbUqy{Hpemq^4 zn?mhU!P-ia3dUSj&L--4nM>e*FvunO(TS=nZ1NCgx|gXLZYiid9o9*_1|2SP{~o%) zN)U_}lj!88T%2?USs?8nq2nXWeT=nK^KZwk)2f<6GH;ew-!Howq9kBOYV#0YvAtgT zG*MSIM2S%3QlD~UmcrQ<=ivI6rvy)8Zm9B%fBP-ewLqwu&r=MUnw&nfRh;My?sl6= z>EQ7W&vZL^{ta&P&ji)P*wvjZV1&Y%T-L6vxfv)h3mN|yv=(L&b~>1{wGf)9GNgJM ztx_4E(;>*us&kZmU_Jc7K9$_5d^3{9Gcc2-L`0d4*JY$Q1yku%QD7~2U7nro&>a0d zs-x6Du#x5ps!-5fTMi`%I1bgxf=0|T=M5|7Y&d2KK4GRa2`L|lh80|*SZn(XZ9oR< zTC0U@*#4y{b4<)=p4$BacD!8Ce3drWNevQ{ zRmI~tb!{I7F?3&68|_ri#(@TXw6r-I3QUgfT?CL`n1yP>T)0od_a_1c^GqID3C`vy z6XnW@9(RjsTBdCH1_Ujd7rEJbAsfhwd}Cxo*%R&!%ekB$vywhI)kYKb!oiS!j;N;; z;oeH%j}W>^DA=ah=K+nJfn5oD5J=^j1{>NEalyz!Nb`l!Z*QA6*AH`r8OOl1pjhxZXsO-mY-gCtml;`L>|Uv-&s#?I>U4 z%z22A3`Lhzl>(3oD+=;nqUnGDe33Mw2!{k32Y2|Pn6)v9%+S|>u7IZlZ;LEqXFnSm zO-TCpRszytadk14nLS(s@DHWP`LIpd6`U_>sphz}T&XCN;qoPN9B3DBXl}#|<0`;@ z{7R_nqyfu8$%?*s5nHfn9O=Q=H|EvGjk!I?Z9&+@h2~NzB+RZSuC?IfujE>1CdlN~ z0813yfb&IP$8|DVsl5IpQDj2;$&UF6BnjI?d?csu9b7DtH+SN#2TG0obOm&L13I#@)|dc{!>*}M>f=KqtRjL`ptYWe>&_)kKi`c*)^ z$DA$9csQNEvm8cCumaA>a_|1MC{YSG&jnTm9 z?S^i($F|3qV>+PEy6`TJyU70UqZ`4n?=Ff3p#SClcjZIg-5mRoot|>zK`cuiUF#mA zls<2;IS-?uryVU!6_%BSzFWR(igz&j7K4d>=Dmtko(6X5&GUpg^?3QPn?H}K*fzVr z(b`_!jnQsq?)c{hL#2KTMx)CM6VTenf6nos(dNp_bwNvcUC%%dd;W=;sOmvXu7u4; z-;>m(ddnS$uS#0JF`GOhHQqaa+k#Q=ItDR`SIhTBr8TO2lj+1z^*bd<_nh|ne!_n= z;;UnU6P4CI62Bqj?sGEL&ErPQF?L}IU$KQ*Ph9qoiSc@qPQuh^bhk}uUc$Va$lVZ>If_=)9%-+U?#a>9QJ+$MSO&+tBir;e4n7V zt}oqx$iYpHrK)GBc`=}?G=JeH@8pvYdfw(DS(9^zW;4!}+C9W^e>7MUK6$MLrpp;K zV|@KnQu5aVcF`n9Z$zR$)djFC?`9D`8$@R`)27=MU?v&C0Y`Pz%ODW$$;imT z(dLsqPQAn>2W(pf9Nb*Q$-&Sy?Y8#;s5B{APv1)!|M@sJQ)5m$>LIO11STrz`_(6R z9~RSE4?KVSEW?HL$#)>f6{yy@m$cJXhhYm9{RX}yY$WLTXs6D*1+Z5*tPl6SfeM^qj(p( z{f8cLOCHJ~J$rtEtSe>8Gt+rk&kAId<5VD_f4q~OYY}8iT*riUbNXWOA3Y1ZOXf{q zFe3wW38AEwq)oc@GJ78{UTB($WLbjiG$<& z89{u$J6cbNXea*#cN7GvqVaG7oCPrS0zrnL9udjrv82uA83lMDW_QBY8s4J*qwz%w zqB?VLE?#$@hR9Nwz9X&M@7(XYY9yjAW_R5!cvS%O&y1gE{$%8VVcE_Qmzec6S~Ceh z=c7W8(lb=sEM%E0dvKFQ?+ctk0ajT^FSXt1hgQMkWIGOZ$8MjBjXS$&(m|$}vjHb5 zRfR<=yDI&q&9-*ybiw0tVS-v-fpQI)jqW?y& zH0L3)hPH(x;FKj7U_ol7u|HI#Xvqn34aeg2R4^8Nh=56^0&d$-wUZGPRzZRHV^B5# zCouR{Kn3@mt&*WAYr!w2lDM6_uE`0+D6OX4=0^owE|%Qz85J*AwaXWd-```o;#MLl zQ8`^&>J&hu&w{jvS(%Emim-4>XQeVWt3!h|9bF2(6~;udkBQ;)I5?5SC_EwQr*N_5 z4wA?F*v=vE<*ybQhecwvZja7n4hcb<7B|V^E{FDqZ<934G~{uMTKwtSb&95f$zxzw z@oym9f0SBF$>ga$%*3}+Ot{kIs1g!zj^1JD5+%N%ef|isc>5R!h#w+7mnb@p;Mgi2 zrY8yGL*Kc(cjOlyR``?E*P}vVEsHYN(P%1{a;~(OTC#GIMBjBOX*{DGJa-~tS`%)x zC$?zxMMfwaF?)d z-XFz`)Jx?1b>qG2-iJjwukO}4O}aXS9wm%5b>U*|-dumi6Wt7tb-LKYorguSp<{c8 zn%5LS6rdIP2(WKgL1N+sh?3|ru~w{Tm;D%^@X*Mc*k{0$6s-vtubqBx%z4sCCMB`0 zczgH2{^wVc92uT$ljfi&9S>@n2_<>;zgd8&@Nnz&$n6$x5n*9TC)LS)i6yd*>5XwP z{N#JmA7WT3Icdq#>CYFL3>|gP*bgs|N-;~n_cq2S;`bZ+9 zr;$&EwccB+cT%!LWK5i4O7)Y(!*VlvqKWPC=c-KNZ4{1n0k4RtoV%*8xJ#sXL-pGM z)4?9YxM^JYIGcyldJQv!l(4o(qv1div$t5Px`;51Z~T^Mbi#LxdtOtmw5ha@iti+E z3D`>yW%rVTu4WSbVQoB}Q*I)YL|BQeFxfk7gq^wHutfrXlU)K6?n};4TTWu*Cv1n; z2M%>xWOzEDD=r}p(epZ4qh6qug7wXC7twKS9@98z>$0!*Lo$Q)*+V12qcTOxc(O~z zc>To_*+JZN5|Y_{6lbE!#`=Ihg)21f<)jE$Y}OO|17Mb@W%GMI(^w_wT8B+ac*R9U zNl{=9aM#d0|L$V(DuK&o(Mg<&Jt+}A@hu)Nn<(S{^Go?Yx2|?<*0omoK4q7H+p9ya zid-?dO=}k?_-5RzzD=TH^-EnUcOuzhZvN%-_DEuRqshF4h_qG4Zj%{Y*a zB0G}XbLOQtU7`T|PiJ?}`P9S9Hf<+zL^lW>Hl+of43mID-c0zpT~2e!h1nMj3)SM# zXjr`?MNN!v3@1%qu`{auE>*Z3VUTlX|2(P_HCXV~R7axjUNmsq!)n~tm0gWYlSxaa zIqphKa~6Xe-l;p_$L;Qc?zc2aLd^Z{|KaJIqB9BFttYl^+fLqOV%wS6wr$(CZQHh! zi6*w~Ki_xG`S(R%)#|QZ-D}lT8&C0ffGy>NxKJ?cRa@rfCl_a6rzYRJD8-d`jiat%kJY3_4v~LSemc=wxZVUS# z<4So9+U_3Ge3aKzmW*gf@f+}2STDoPCxje?y;Y6{m9b0io7x|wJYo7pdP*M{XrrkD zA#A02ENVMNJOTC6Yhhgy7S07yXQn002cH9;`5{hFAnZoTcC9uR>y8xW@2zTYd6pDn zsq&y!TdgGFWNp(cDptv(Ysh33VO;A+d|?AMMIH8{S}dw5E{|2TyzJF9z=2qgdn$MG z7|%^3iqp8>@7#N4;e^?ef~kLb=)qLpiQOzTPnTD-pqND+L4U(KRH3ojtS_4{)ZUX( z(}tpLL{L^#<#%kaltB?F1h`-9m9`f;a)OF2b`&!dg9QxW^X{Zi!F<*XVh~o5rNVke zH3%&)ZN#LMwo2phRT>j+vL`|GcE@V+b%m@-NR{Uws%uTpr)=kdsT54 z8Hq7|@LS1yCTen|C@RC}sN*ok`o?{st)^RE;yi=uJ|Iv&E{=C)->DfVI<#X7gm5C; zoW&_uJ9K+rVy@xa(tKA`(TpT3;YtlHN>p-Vji#o)J7-1z7}Ls)b!!NXSl=TYno9(I z$3Hu>#7TW!T$4CPk?b-CsjP6Is;um{Qv0?`AKAWPR?Dm?YgsB#3N*jg44G8%P0%eX z?ILGkNT17zonN;o?5M%coy;u}+68}F4b-t$GW3e}M$n1v#UG&W$X)%oI8Ljdn|ram zrKoOFET@+`thALzvoTO;LCgKJ#%)UHXV#N@ghVu8POub|!p*ZR{uN$;Y%}!~Lr+HH*J% zWa@5K+hK;W1+0>QxEwC^dz%qEDdg2}fwkn&hS;H>edtiol+i(APX^Nonh-Rjrbwvk zS$7cm?WPv=`eFL%jlbn0yYB@SLl*VnWwHKq7&OOnKGH8NzyZ)sXOm?!%Op@r4X9K^cn>Srr%F`npqln`FE|ja^x6@R>p^lJUP0FD|?&1R@ zW4=l0r@$HxyyC!}-yhngBI9l1=vd5>DSr_~F}mUY;t~6;dMhN*KdRT~*t97BaDoPg zikF1i5ZJ8D;Iue&%D;T70L*>s*SmAg2N**gMf(&rSO>(;sxH^jF!(*^c=$&S2+h{} z2-vxAwgG`qS3m=5>u;GJNNG=ASW`B%WAB4dE7p8AGPv!Pj`;jV5W2+$%Yf7-I$wHU zsd09`lX`qRVmp~iWsWD@JKOR_Kr2QiM2lnIWidb7x9EBN;1O)*J?{%BPtee26%Gp* zze^qo0s{=)CBL%ny1!#OrVxq7xDwG@nX33$P;o11JodYia2FYz<=*NA?66N_AWEsiAApk#A08Z7@pkT!7(KImIn^9J=ei}68Qk4nx%?NH>~K8Q1X{zjeY z$9~4da)1)WllBx0pEe-c!UquP-_ed4HU^1`uB&IT_h}vi|2;CNscB$QWsBo<^01t=rtHM;eTxtO zmk_*9wf`%(hUDN(dc?&N>QNIbKGR1Ko2bu`HJYM}sRb)Fa$qn}r?hbAx;z2g>4d9U zGNh#~s-KP?WFnoHJYtd~>8D+Y5lN2eK;B~EDvq4^A+M;XaOu$Y)VZxEI;alzGbG6enc5{FtCa0Sj~k?;?NMdbl(2ylFU{84n)w37V` zr;~ZMCpYqbFFl;tI$J$d>A5cU@c1eu-nYePwjwzt5e$?xjh3Y(YO4#mk?>A4V@H=A z3jM)uiLTeLKGAUgU5oK#qM;;DqrIe2J=*gJ%@yvI{n|h~cO}p*-wd3jM0z(PhKMu! zF^H`<(BE2+vBD+6g02+F!lOgy70i|Xg`(EhEkjCpnCh#^dO0%(s^0WFL3$_ZoV)dS zIQQt^A~|RxuU}2KO)&|ztR|R)vUqr;q9YqUQdE5E?Sb*@R0eZalntJJXBDCgTC1yd zOziFyo@%A+Gh$AA_$!V@eJAvq-JGDj90wqe(3+%Ea)OeZHQB=v^q%7o3Cod7tp#dy zzozu>F7I^zQl8p`x{n@_|H{hJm5%sl^0lKbi-5w0{c0XUTH~pENxH3Q%!i;z=y6-C z_b)hGd*x^6d5^Aq?V~g<8?pwg^vVUW1vp@g{_*K1%BR z+rYMbYs74N?|66|)!W-WL4e4GspRgZbjP_tfcz=99-?h6rii?N($p2HDz!paeg{z* z#COlL$B6e6b<3pZz6+N;NC$JB5=H^=tm3{U^IBYjaQZo6q$T>i&!QJ8dB)Y&`{HKG z=+nWp(HbhO$8Waut(S{0OGutnEjyJ&SF1_H{!_}qJ^Ov45Y4M?;dO-AyY{bK&?J-i z((}cv1N>iF&3N%gub7I=~MF-NR$ zW;Ip|kCg)q;Z_?A7UTWSB{WxIHkYFwsgB+A@?kAoTg)g({8q5W#o$Sh?An*G&Lh6@ zbDW%iiVF67V&~}Hr^%SECOhz9g_h^=UQO26N|ApRRf|ZJ{Q(^Rj>WucIn=wX;T_Ka z@`_!t)~}Y~)^^T?w!gGHo=xu<_Un?JMPTvTO4T?%3~lx9=#;RS-I)=dV){-S_6gQw zX*QZkA80|?)oy`Sj7#)u-onAyB%C(aGB)$rwN?soEN+2XN=OwdF_})Lsu14%z%U?k zX@+Da7JyfhRO546(&+qMq{+HZTwmFo?ks12`OdQs9+_qR$I40gme z{%cUwiI=M`=b{_8w0g*>IBi2qSJE?YB7 zX5PQ;7Rp9!(oXkbhM9SCYltCv({7RgX zmSFc}psXojA0KS?wzzbnJS|!~>Z&?oHxvkd7Ct1Tg{Xlr;bZf#G5{pp9NFp41?I^D zz8Y(sqodNjPRjBDpI{F~6F+tOqQ9)<}5yvj1NdsEE zrc6KuP5KLINK6#Pj^5vbu$Vlz>kOc*zzE)4qFvd?Ly`Hp|=Z69s6uJs&i7ZLuIW#g!{l}cacmmow>3>_clQpbW!IcZ^NJk$V(2pow$9E#5 zAPgoTqRv#~=VkyLMMkS%2iWwVl$C)z|oUz$F! z=S7R(Zy1(mxcDdP5v)kv%M1%A9Ec;OgV{pMUpN@taKT?AdpaKboM{IhDw>R!_T{3- znnUT2f_U>{G*cUhy#;&omvuOTvOOpD7z6bvJ@rT)B=Ci5R6Jey(G zc~tC3*(y-;fzh$P0MM=8gRSI!82L-Kaobfn4S8n;awjI^DH@Lrhes|sYhWFh5VfOI8*)p@= zJwLJmD=T(%cSfM)wyD(SAkTVZwTtlo1})6z-o&E6kps?ZMqO4I?0$&!AAGgW6aRm4 z$XB5$?Ei8Z|Gm|Jhx}afOMY~w|Kk6PP5%|7%Ezuh5b}H26P-6&<85EBOO@ZD4`MeI zzsT7Zm6FFhtTU-99Y5%-Yc(p@dcQ!nH38ugI4+8ouAh&V&g(Wk8*R}UFp4*VcTeh7 zvs}7|nS&W@k(>{u&6?E4uN$wo{P4{)EKBQWxa_uRQvri$oY>A|)xp|0rVYj7mX)lm zGoA7*ruavZwmpU;NTIFkXvrM`mr);HybNAXzZtv$Kf?8fRQ8s)0CRem3uYv9pF(f2 zty&H7hR;Iwj z$WJkIxZ=?EekqBdgvLz;)F*0`~HFo#! z5bN*bF2+Z!ob)&5M2_n>X9$z^{`bc1qd_+AI<_PK7z=n?+)=(>uc&^dkh+S8gOyEd zD#P(jikCBf@a7yo6EPrEF=pLa&tPBgN80ZUrle~n?T_V_K4n^nG4Vn#YHP0!IDK!kozvUeJ(&vg}>Y- zV(XfxoO_fhaM-;jb4x)~rjkSdq$;?KCsIX#B?fI+Obdg;^A(;LV8;MgmyS< z1*>cP*xnVzCuS&?3)o@fz)Gj1cpKmbbK0`&W4C6O1hgS9R8lotUlCD&Q*TkO%NPc4<|>@8H-^E3lyHBl=<&#d>|yk?jsoz#;y zaHAvj4``2Wi_JIR=H8=vKgnRZbM(G?9#CS29O_)gH+wKd?A|;t1i+N5R-B{0@7z_FMB=oqdC#$8qLHDCH(pQ9Wv$?Sqg}6kW9?ge z#XGCkFUsC0so3C)@cTW^`gbDW3w2_3Y+p3lStGWE?aXMQkZxI@xgOIX!LrxJ>BmOD z)S&ILy~0>^Fg9*>8)ah^O8NF#qNm^Pjwm6XG&PaTEQ-dX z_01q4&CIgY@>S&&C-9#@TlO1{>|b&gFyG%^5Y1ST|6Pm4}y}TPY zczv3}VprsKec+yFiy40Z&>k3$JCXktp$2QI*g1w$o}x=Ps;Z6XPb2S20`7)s;Zt*H z0DD5$IAV7LBGZt!_arBG0$qwN!5@-!DMt;IbV;Fqn@Ru9@()bc-pWsBR!IQ`7+~3r zo^SqTLd{-?_mSCPY$jZ$Dc#IWPhU?okL?*Uo&jKElr=P%oTFBBeui3leL_LO9FJ69 z85sW6zrzy?G)HkSxe(Af+ZB=_5p;5q!mDh(G|+qJZGuq_>-fOGf^|zx_WZRxy;5A+ zb`)Rh){=~t8On$G>dG^4^NAl737U{Zf4OR&6^*PAdbSGm& zP?-m-_VV)2zE?`_GvHiTMSfe=jGGTk%@CerlRJi2=_o$=mTQ-RhVsYET{gCPQ<(0a zh@uceLuq)mJBimmdOfK%<^x{dhE`(v*qNn zhLfL%vmQgChWn&bBVoLI2X^u2ig|{T zCyaT?bLX)!HNl6KI*C0yk~dw*eBR2VwTtfWX?Un#vpF0WJ6cCI>mCD<0SPC*TZuJ0@7xx^?8W+y4Q?$0)`aH2PY|LWYRi4+yn}_66_+z7J z-VwGIpptSOB{D+ojJB1anMVPZ1`fhv*_(E zo8=WuO=F2p0x{p%RbPpc(3hOjta58de`d^oskUve36H?Rdwm~OSUF%mib8JD)|t_i zy6cMW8l2wMcG-Rxk3(X!I&1US&z!6;-v(!-y5uqmnidgFZDevonWSMZw;d9HFB89Q z(=G$}N_*6CLPJKJ%H`=Y3Sv3(5sXkL@|71S_jb%ycbLy3V%;!l_roCQNX0F_;>dzh z61P%^y?c)A+OL zxpnd>V{fdl`{hIqzRB$`C4|TM9c2h(wT?T%H`QkYg$V~uUy9ec7&CbJBpXi6+on`g zpzQ(Eb` zyvM9R!MMVq;?#ZfYj|uNr)JG1i;vf`pXPk(Y7fVNwQV#`!@)8!+*+)4tmkVR@{>Ca z+^p~oYZnU2pvnE${ODK)R1x9AY~CE{s*7Ue#AGn3A=eX1kdxfBTtG+5{2 z{hue#T9=+zuH~H}3p|Ii%wkY?s$hj)Jtjjlc*J}2!EKxvC_CTsV; zb6>C0_#SL?A5W#B5)hQ!}%@MFO1%Hy8|F~+H0bdx3M_v$a%(waLj zNm43DcaJrMJ#V41%h84Dt!7y5&8lxLXA-+T1eD-&ADnl-N8*B-3&^uq;E6^omKZUk zo3_y0?cbia>uPWV>Uobz2)v)|5cOWXCc~6g%ITN+`YqtRn_$6qF<)-_oiJ zHHb;(*wVUf$kb$)>UZ8DJ=Lu9+L-mCmcT>g-<2Sj5e z)~yrgLJ(O8$pEGC^Bn96iwUY)s?w?w=u%w}(jd}(r;ELm=>-^M;N{~ zcN`XU+`u8ok2S`P>M*R)1)pg6ZcIdOfemu1U;iP7MG4VvzRXutWqwXx?h?o2m*>Gd zD*S9ev~UZ~%&>DXDpH@b5zDwW_dIP|#zcp_htcL()9=bmRaW~%x0i&-aH620s(2?t zOscv_ktE1=&05J291ur)hfyQ&&Lw`vyETwb+tHU}o!)5XE?? zDgl#@VY+7>jO>MQ{iA0EOjcg?Z(LXBEIH`ER{m(X_y(a5hn6`43yzlW^rg1$NT35! zjxg*coIn+afl{s^8#1eQFpRkVSL{1rt7$^$1m*$0oSiwfA|ck#gQXL6$bqDtPd zJim}_E|bE;jozHSVs7}I3%Qc%_TB$AREmQe2rizISQqsv$jBetZ_@Y<2Ml7D>A+u6 z>AAfF6>&6Zl~d6JvU;;|8V&sP-i&nz(LMXa1A!w-a+Gy5Y8r{Ih2Dmm38BRy~Kh$`@dH|B!DonTOz!>TFLw7q{!|f@5UP;%4lhf z)C6Fw7Y)P3&qt*Yo-FIEJ=0$X8Yc#A{?7|ATjgWHgCPQ%+vs;S5g3`?d1PFYkwJp) zJJvH0&1sQZ$4D2|&s7a+>-2&cwu2SzJUu%vQBYx?CgL3zax`Xo8T;%gCBr~Ys$NEw zKDnEwKbkXJ3vmOY>g1#%BuRB^WdgLDt5hE`96VOH3#au}lQ~NCGCfGck+Y7C(Cy3` zoj&T?+cGbyFzEk%q6MT@A2&{ip0G58Y=QAU+%a!n4PMaEk+PIMpGSLM!PXX3C> zdyM_MB4C45hwj&Ya9tUtJ=)r!4nqiuc5GpxzFp_*B}$V?qr+B#C+I*QEju@5l>AOE z)z$PTU(D_7%~Jw{ySWRV!1(VrgPYT z&27_x4ComtNYNBJRL;FqV!UcLG~c}b%!r5pEOlTaQrd(V59MqLMHG_;;6zxcd?Jb} z+&23P#m~lb=$acMr|`0GYhfqPn(nsWZmxwS`4tmP2yGH(kCq?2VBIb(jqFQxrna1B z*yQJCB`r}9tgO{oTMNWcFg2};bl^PWN;2nLtS3Z4`8%>9B830QyGLy&L~?37qA z&Ox@?yJW0>#J#eJ*g_e_Wo6pE3o>#jfzqY;sK4B){#+#G>&^Z*E~)WIsGx1KXB-_h z`Dc4WIwpF0kj=i{f>nl0Vh$3$PQWS<>sl!P;)L=Q>GhA1%;NDQUxRWze^UH=C}VLm zhgXDv@@)G3o;YlL-IS4{%8>fK#_YAX-xFz7&hYqpm0^@kB$S!jHya$!6DOyJO&=_ymPitLTp|@W zXnZq^Yo5)VWp@Q&XcW}3tM23z1vSJbSOCdj(l`?lk+m3P)p9(?{La1MTkBGm`MuSN zn!D9!B)c1bkE50d?+~TlZZaYF=(md4=uO0A<(cd+kCkd?O)2 zN6yl~sL!M+i+xLm6mmuX<&EIzqiu(0%HfM(2{In7f~k&lGjs*_QSFEjaI2`&1?DQq zA&A|SIhVZnyF7)kYDH19!svkV2BXHr-I|r@1kqp^HC~|Cq}ATHc1nro6CM=1!o@4S z`*dtnC!oR^SFe*JI7i^|ldgF*@Zs%62;D}aFY9+q72jbo2#m>i(6~`QF&>>MgmI7C z*GdQhigbpKl4zfEX1B9y3-X$t}8nF zFrZ=eX@+}-gZjXw6qzrFB3<&;wRH8f#3;d zT#e0BSU1$Vv)28Gy1qkjyS!&sH%<$LHJl!vr)Nu;$Sb12izO+HI>w6^(Rj_BE;Gd=K_t4~)~-&>M$ zgg3fat}cwy<%`$eY6|ex6@4GxPWF8zV^WKBLqs?ScHEs17wH z+0(JbKqlP&jL&0Zz+4#WoAbh}!#3)Z$w>aF)|B%Wa~(SU(bm@MA0w!76;boXcg&nD zfgAN^CCj&>?ZHm&?SsGtN?{48_avUb)PNIeomRZ>`U!Zw*302i0eNNSPXN`_+n4*( zh`K_-`HyiH=d{qIL#rv$d}nJ$DAK05v@s4?jdfLfGt&(vuZ{DBG1muVk-mPi)?Ykt^|f$~$(FnpZ4?R1LR75O_+1k-t)LL}tnM7z*9y8!UYjd$ z76YM#v@1feHGI2!G!c7wPSoJt(e>=o>8~B$`IUst0-DNj^mHliIY)*qA9pwPzf!AX zuOoBsf3;<_+ae0C_TQIz=}&hu|0Kq2=npz8FIzy< zyW&NdTwt2td~*n{&WhxviE7Kh*+{{L>m#24YR()ac>u_S0Rs||FoO>SH)aqPkyP-) zaY&XW1r69zM$kWfhRL5+6%8s!s?8v!g*he(ks(x?bq0v{q1Xb?C=16hq&>7qJ8G0P z-NUGRL+FE%^;aOr6i7uvi8?1K?KCB#V?{-W532^se^x^zj}(U+1tq26K@9%Iglw1X0|5~L{~Cy|y_MB1F7v@OFL=gEloG!boYacW&&I)H3@(aCGPFiC{B8*WW#fkQI00 zQ6R@*(TSy~)#T+cepe6%4X>ZhxMI6eP1H3Mn&)Q@MkL6UZad5Cnh`QGq7^W~`d~v{ z0S%LpCIrRHZH-u<=8izjvji0}O2nQI8~v*;>VIsO$+I?KG!W_#kiqF}&gv@|^|Ji1Q_gOHpFD6%)EA%eLp0W0yBiqf34eUo$; zuL;YC4^A-}`-d5%C33p`?$`{ngXPwvR6z$JWfc}w{BE?(?mtIm#jCR6+Cau(L~ED) zKT2lk2ZbX#k+|qWLCXPs66)!AmR8jBCUCv8qUOeMyuZVOOo%wooHB+-=hv=~fjoi0 zW_|$z^6K1>Nl4D_Ps$7WfX1iyIIG`2os-ro>>!kj@{AUyTq+KPkq3?VhZQtMiw6T& zZmRSZ)=5Ju+;CNvkR`R;)(FzfxvA)?2NwTvv8x+OM1>Qb1Vz-UvxeXsBgA=r_K{2H zIrG$5TznDk5f3s-A=Y6i6o|y*<3(+#iswu9TQL(kp{WGlyr8o-F4g^Xrn;DP_n$ZS zapLM5Ib2-FcjZn5H&)bC2|!Pg^@2R{!BBCQ6(lx*YEFxq|J#RX0S*rCi@xl2D=L*W zXxTIiIc^1b1Lr{kQ|AJ@< z78ZLLGJthrwC_JawD3KfGlR|X#UJoYBctK{_lU;*eL!QWoNRRv`q)|i^yQqpqmn!- zSE;6?D96Z3-slFjo9e*5Jg%l3x% z9neI@;K*~X_YfL`_rSkdj=h^NQ`qkrU3>!|l;q_j_K0=Ed>v zac%T0k&liWmX#FcujhM$GNb8|4R<@kuUz`Jtr)KrJnG?3j1@#5S2mpI>(UulL0h}a zy%w<6TW|kQhMgqk;Yt7QNlJ%`kYU!Jmw!RHKfW$hbqxyMfVZn_iV%-osVgjq*>DeP z-PRU4nu~m`Fy!$;t2+9gVz=3@;Gx~(H^aB^RvgqHiI!OVg?1QPPx!gx#0?E6_K0M3 z$ncLH?z*1^p`7Qjw3EtsiW~<0wDvNQIdkgIff78ftq;xFJBlp8_JJYOcRfmkod{~* z&g$pe1b(yn-eF!DY~;{ggpK9fYAoCuswWvB#i z>D)Q!VWyya)z>#KD*VTDANjEFN4c9%C;Zzv&&Wz9PGVi`5T?J@?r@oMKP9hRw~*u@ zhq!Ya3NK!umJVS?iO8XcSghL}w-J(9y>eO|kkx#1rN(DDh{&NDbbI{WLvebl)`mP6 z-xxQCnhIYD(|?gdEgJc|J?O8T<-H-?9^s(8oQemAZN0OtdNXEf z!~!edj91dRc^5`rZ=;2Uf+19|2|5e0(_pL~3-ByvX!fLwDWFv38GIYyy9*Hhrdqu1 z9Hb~a#&)6g$&@?xni;*OAkIN-N^9)hO{;1wq@-y-7VuBAB7fbVw~7yp;W)^)T}mA# z4UgpWh_s&BKgGq-FBq2z87d60^x@&JuFW5}6zTfR;+JMd`O?Aox`Nq1IaS^` ziV?`L63hWaaj>_?9_O1OA$T>XedJz|T1z6Pn0m4}Yj54s+I&W6=3s=47kpurIvGs- z?PyF%K60$vjeP5hqySZ;TMN8AVZJ@0GWv8xZ-?0Ke~aV)*e9@J;62-IWICV595P}l zNj1Q_mhSUt>c!6)4N}t}A&1eB?hX%T6HC0%KZ;UQ@AZU7*})M!5f(U_dJIWs`_CU= z3EBL2KoQ+4xxHMCSzY0*e$Ycj#`yk9@NS~@zBT-M@p~Sl{q9(>!u$B;R3plGg@o^( zIC1?ZD&q2^_ulN=`owjMKXi!2{eHk~`G_7}Zfg`mLhf#H5I!j!P%Jm2=SIs}34c2r zK+yi>!tsWhEpzH<#=(IB=Z{~fbSd9rfSypIu~sb0H$>Z|5AvDXqEn{s!IT z?Y^FNd#=<7tx)dI*q1PlW8N8ZT2d!BQ13e=9}BkNT#BT8>7{vHk6>P4R?q}e$EkP{ z&=SK4^}NbZce_)NFnz$pAdY-1ecr@w=$37Kb3TnH`RPfbt6l0B7y;deuwlObvD2b{rc+tMc4}%K-wlhmOZPZ*b9}Hyb(pJeSNg-P&O%uOY=%E%_xxlwnQ3}E1!C3XoC53XwJYcSx4EJ7LvJ;q z;q~ajYnI>0-g9PS@v6Xs1W=Z%)gf5reH!F%+Y47S6tt95+yT!5#^kD@6gaZRH;ovc z{b3FZR9&0wX#u~9>W?}Lu0h+zLb-=qM++Q4(_5P9Th?Kot9HJ}p%9PWS8&C#)5GYq zz5V=QS3@hRuMi$CfH~&arK#vc1tSxHad+!4m<6cHWeZ4EO3jyp&Er)ajH+|+8UU@K z``ACWZT}aMeS+c)AAL5~ohxTZ;KztG0qY^7+uw(SvFCAv;eqECVM_xmWT~g8ccY78 z;n6qa57KEi--@vwA754x6L*&C>C~O@8EUjpsLAAfcUhI=KqvGxyB{c2V`y-t^{=|J zWK&ffPUS%DpfO*MMzK#SdsV2ANXGwHMM*xTKZ-%u2j}MbEja=yPghJq1++$y)A3FI za1zkibUUFTrZ>r%xD87}Q<)WQ#dcj!Rbu)ZYV4yWJSbA;QsO^q42@sx7vJ%t@6kjR z4ROSpR1TeANf-nr!eCGc$@9AMj?S+96#3^{57DS=kfpAf^nXOqf)B}(hFDDl+zE!o zAVkRg3Mwfr{P~J0Xsc#BQH!iEZOML%TY)0#gQ-{3QcvgjL~M z%RkwkIb%T-oGhYT@<3G08M`Ohhb8DMk-u3qrNfUVLcvq?c zmq8k-4`{miRg_tt8dDe66~qMu`Ab8749p1mRqZ}E|BK$pDd|-S81va&DP~Yj)i;BK zg@yf6`6?o0LeqTJ+15BTULyl~I>OVljm}k6!p|f&Fwgu6+9y#qI5t3nL&}y|y3jgW zJUG6&7|ZMee01&s3>k@TK9^cg$rnW{o$<@T^{{5?GJi-;*|Z6>DUDU&n(r9-(31_c zl_bJ;Vojj;0(E{w5gxO$jDP|dRuzH8rTrWlP#-!!f;wVELHWz4ymMz|H^DVWJAB7W zAFmF6bOG3lIH4j~_GK{>4>e+CJR}2I%xi_%jaO1zUK2B2BO>;+EpMC*>3W74#DQOmb zMWd6--%>paE(AqXOj4viUlT!qSOm_Yi#K3F;Ysk9PexZ?m>$)@3|IP-8UdWHL<|`9 z#*Y^CZFe`i(%R2apN=})tT(ru!dVK};kdgy@3f1vy|IyP znJ7KAPJh2=3<70+5FwBPDOf#9FtzWxzY=3wB4cE#p>cj}MWrr;nF_XaWSGov&e@=u zATxF%?zdH{p#92n&wPqfgl^>(Cj$ zX+?FoeR#?P@Svoz@EzR##;3QL4>}MK0M&%yRJ0L<=A%Y7l1AckiurL<6QPK6*T$FO-)iQdG~ zv=}X}B9dS4J*ZJxtKr(MQG{eoa4QQtV`|)pjs=P9q$Cv01+l#h9MPEKFX3uSNi6O4 zgv)Nx2!peMSxJ)Z3&$_;PI9N$)2VV7ql-m`q11;4RPq z&)Z1IhN{RkPd5U!<|zTy$u)1z*!#aa!K4Js1!t%Dt8jL`4d1^dM_#f#6+WDKn zL!sbAQ`VTwFS3B3+=Ll$U@#GGp%!I`U@@15$_c^M7`*US4MrWg|7=oTnZBK>a$ ze_Fmmz z?O{cw%L7z<15RIl^2p2w$J56Y2gR|G^lwh;c)%+|!YZx5ff$qQ`}RJQT>iMz^l5*@ z^7p^>PxT-Rs;I6HXomyN5a-Cl8uwX+Bvk))7qAz{Gdy9lwzH>#%FToP&IP1Jz6bpJ8c_C;kv8 zD>1uWz|obKoOzy)gS_S|;$qFE7$V}50V4-m&kxf>Q9p$pij{0aXS16Dzgy(oktLv_ z%Vhqhv4A3|fJT9Zhi@p&Ubelp^nwE|rDn%fd20@>A#Ypq;1zixx3l-C93D3T?V+BD zGDn)1Mh1gXw{dPbn6HJ-r9)a(f)m&QTS^B+jSIi@Ho-qC=d2CqgBLJO1PTGoC7+LE zyF5>GM_G0?*w%v=H@gHP4EjmG&i}~ZYQKY^HrL{X2eDtX-ve)+1_bN^-_}*?*(`(i zs+o(LVA9>+1Pf)`9xH$ZTsj+@#g=poklp{X)h&{9LPQRz3%kc;GW!+Y5wKxBTAQ7Z zjE2L_p*<5;J7c>4@Eq2v0SjaGQ=!bIWz0K&;#Tomv{{_rq>+Td#K*W}^E9(5dz^a< z&EF~nO&A1aUZfL2{OtHSQ}p^OF;kr;DRd0#PitV0%FZ z)eu#M7m2FhE+;Edh#(un={X7+g@pq`VWgyScQtkSt743=+Z2MpSOW$O!=bpoS_+P6 zONS1{F!D%Hu(4&V%our1(OlS)Opy9ZyT~f{>^JJUw#qtUs7R3`XCt|&{lf|;sQ#6d z)PSc} zadBn_gMu@AXbN_nR!f|}3De_(LMl$v0u#kyQQUCZc}f@ygaQy>>Xz4#^r3{=6!{_w zqZDHaiu45Oh{gmdI7wk6VPC9JSJ^MTOJQjHF_H(34A`|oZ)MxKQ;x?0J+C!fyKmSD@wD$BoroO7-IQDqyz z7?C>SLiQJS%~$_|2E)F88Svc1AvyL##r*R{OH=^;CUVMlkYR}_VSd4YEH!*XgWdMw zV4AHNBF$7-`o*BDAIAFMXE?*g{vWB+uZucP3H(1C5D17+8P*RI^FI)r{x8Y%U!LbX z6H~$SKZ|EbJ3HeG453^GtNFq=1H!p0*Pq9I3yT3Ii5SkF&_vU7;v7#)IKIz@NUcy& zlZ_$K*dwMF4yZP&KkY%f)O1Eg!uB(*ypP8X7BgbBnjKjno|i*THZm{Qm|hC`%+4jU zqcz9^eu`?MSF4SbP_uoyby0Zt_fXsniE+N}rAH;%2REG(U%^Gug?pPlK|E^Mw@i1v z@!xzTj(`q&*>#4xa7uiub6*2r5y<-S_>WZ=Gj0m!hn>ly3;nmJLmQ1DheToCBTVV=OeO@W--SyGU*c6nL zG?@S^DYHdE*q6q7V%A&h;REuI2g`0Z%jiZH`@Wr#sUUg^e|_T_IjoEpBv`Myk!u z$d=gf&WC-dnCXMhp3f1mCAuZUv4PNae?qUtq3zJok6_kypCmdLNKPCJ zj|+srHrYG8dTtoyZorwZkN)KA0pp4B zS78%+Kr*&+!^Fai@wuQur zBH>YE=KW2SIrcI0vC8v<_sn7dk;|3wU-YCoIp{f3{^F-=JX)<{n-}?p52BO% zmxW@$l#GqsS3$ONNR#k81FLECiD`;Y#r$nK66u%9VY){Do7y(fN5^=Rl=+_HGJd|9 z80Z%Lnb%*}lozYNJ0GwuDz)`)-pjd(;~CkwIhUjsLxe?-x(evasVmXNP%}g_zS$z0 zVhi=7-(dMxdN(`h7R_KE4J5IOoq>?LpZUJzj==i_ZOP}qNU@ZLWy$4sB$bn5UJzWU z)l(S97sfx^4j^+&59*jG9#|13)2mYj)r(IL>^n zeGhS`RtZcVMB>XL-~|OOI3Km0(e-L?zGTsPI9DJNKgN%ppb%1t zu8$%Z2*^2ygn!D2`f(q=Et_nS$_*>rv`^qpF9gPi2;2ZVk9~VMJaWb%_GE^ITmc(b zZn_=cG_Ndz^0s#yG=Rsae8u=#H;`Z`HG}`Pq4LkYoRiJFx3DL=EqzlH zPjFWYWLd@}%a{a|6xHDF(2YJ;Ym>T^=2uY9WV1CD#eX)-Q_1J!CnHjxB`NJSu0Ifl zRjwbHHJ~crj$PH!bRAVuQO|NI&F5y*v?)Yp&F0w`H_)^5^CXHV5tG&*9m1$AT*Mgh zcZ7HhF>x)$b`7ZEi)&wH`pl0ItzYNimzVPXRd>+mpZll`K?95x8QXDzpgClSpjBQoCQ#~r2UK<%QBxMX$_ ztDb)vAtur7)~{Gme9SG$;@98jyzD6?WKL$-<4dV+D3ATi=JCwze3pJRn;~5%BgD&m zTXDSQ2_cXG$Le{E>i7hg%qeMHCkKl7?jFITH@4-GOU5v7^FG{1zufZ%50CG_Qxn?r z*k#@MaPCTMO?mJVjj#3#KD=7SbYiavOC{%?r|8)}p4e`8FmB3lOtOTom6N-uh}cf& z5e^8G>6l1ska!4_36POZCS+MUHYqu~n2?Z6rusA{6H>#(!ggREd-M7*>h>;#hFOp# ziRM?_Pq$b=$UwtyWfQVVLN>{NXt>g|6NnCrz}nOVvI*H_K9)~$Tn!+Z>gRv)nVnGG zDgr;Abw6UcU=(TDK|m;-@4AEN_m;Cm2VsuH+`Tnw?rtiMxG?3kCp5}QrPjEjDE;ywI|7{mm>(RC6zBEcjsubqz=K zmVI|8pEb+i3ri2?zOaB&fbtEj`uG!;+<7Tqh>@)P?JK?#naqE8Egsw){(cs}E%}Te zAHSMeae~jT`I3rb-R%hbr(MCvBAS^(@a3m@G`1V&-hW=m7s zfXeCyX}RrvzDP=EW>N&RZkovMqT{yP92U&3X~USAoyDwv7xUYJ(h7H+ql*@mAe{?e}D>gBhPCPmQv=7|jKpVhRmh@7!JocuCxtK9}d0>z4&P=OCI5i)(%wJO!c<)JayB{Ts^T6$Ld{NKjsZa-#&vmJ6&fkENcke z?tcbX!L>|$q8FQXK24eaJo==b0Qekvy!AjncN9!T*C(;5)i(bALm7U+6yjudZVn|s zrSbK~T3X(5J6AjR;BqJkYbZM(`pd@bf4UV+CIkWro$@V9dHCfTp4j|6^RGy5G>>IJ-NC*=2YBhzJ-j^sJ4ScSAq1d$ zKCir4&b9CD#>{B}Don+=!EhR5jxoH#4iBhq_d3Jv=6I_%DmSzI)h z>k1ar^}g{~OV?7EU)KaDiv;dk+zokj*E&!Dfk3EkJoZ}ro0jX^RJ;}(hqE(vy*@|6w|uD5x8j%UElX}Ya?$ZH&`(uDLO*(UpldcP{0+l8q#fKA-k$4>C^I zx&GNv#K+3Oepc@6&vm5*j4Q2U)U{T2&s{_{Ajwvw9|lv{at7lL=5h6#!#Mct#~eA^ z%@il4t3PH}*h5^Y>x{R}Mj1JYEuM4fZwzL@n5&pEZy|~?t{eQ1@uw|)>V6yh4(z2> z|Ndk}L{h2hNRo7l0nsTrSj_IwOzsX3Z{p3v8S>Q2nBRGW_kO;b2}4^FYyuQF`@ese zZ=7S8_DHwik1MNP!}OQqc&#d@UP$=^D4Ia=2Z%kd7gIm!h40syBz_pp+|MpPb_Qo- zp|(E1f~u(a{Q)?^Z>Z#NVPAG8*O|*XlrIq$mB`A)JJ7D~jIP0DSN0{$^%YkPY(eV| zVQgC45IC(V6y19%PxlW8{Fpms5%-0cT0cZ4x8T~%OZj|X7U!GIgfvSaDg5jL*YC8m z`t#>lQ58*4gZEJJSQ+%-<6O|}>=r$tdn=0OEMZ$#Ut%m_Bs2@dDgkPM{dZl$w=^XA#AR$0V2>9KY_=uVAkas0JIxAkVn@Fyt zV9=?o(ut3i5lx)B2uUJk%$tnv_!Dd2f0nru&7}Q(0__L3CsLL`ucdD0G*)g2Li0E< z^}CU-!@3X^a`t(X9DRu5*I&aCWJVWeLb{Bl`(>?>gv6izep{zoxulbdb9*szdODd| z*-f+>q3FxrXH7vf1Ry1LqU9Y|ks`~Os~584y$W`$`UiFQ9OAmGGf~`ixEwx$LQf1X zh{TfK8(#=iAO4NV?pBgbcD8P^)APKR1WUP%hz~a#W_>`j*Y=PB;P;^@8X`zWcn@N) z%B000Clz^lpjk<{Dw>L&`=Fg5^tua(kB%h2=0;i#k4BxVBAGS3&W#k;4xnetX7%l? z&E>Rs@+r!;0=*jZ@&-UXed%-0n#Dvu@H!EB2dR`)ugb^XGXpK@CEB)^ac^-D zHgOEup6%?ln305z?zIrVVy z&}m*DrMWhe+r^;dAEMe4OT@)*QnKk)7;x?evyi?w^US?{dH-NOcV`=eyMZCpPBl~5 z%69Ym3MWHH*0E)85nC($9AO`;gW~DlE*UGJyQ|r<=Kz^4hY}g`M;2znIseXk5Hr7I z_7a7$eG({KHIKCn;@%s&p4mtV6Cuge2WBIXWYfvZT7cKDqN^%iI40B@&6y6mB#=)1 zWtp8h`~3`CF2JoIOyNwrxCLgD3<3fH(1k!%HB?nU`9@0M*xeqjnBXS0hS+X5GgkeKZNGlcewRW*>#o708Je}E97i1;{ylP;xONCd$N!B`@E_+2_@ zQy`|G7<8qfJf!-iXI%g;FIw#Z%GU(rHrFr9N~t%I-60mup9cX2P1n#Rr9OV}1bZQe zS`en=-h6*$dDDtp)6u0CfdJY` zsdaRpzupUbG*qL`i3dS=#s&1f%R=t@b?o?X8`@11=r*tu(T&FE>FAm`Z5<_0Q^0Re z+)8!Nt4S6rf!YJ8!F`VlWyOyY(6Cr#gb2f&6pnlr$F8dB)aHJM$JLR%LLEgB1o!`( z*6HUurZGcFFffGL3H@IX9G1$xW1~@Y2X&qRepN?N0(ksNgC9Vn zrX-K$u2gQzisuh6;7HN*zv@clpMK=Mmnul@dm&d`+J&=*K8J@8SzAqhxu919*tW0X zt3wv<{WtA})hw`-SK`prlZsGeCo{q<7^1+jb zyv`zKFW5>}?@?q&{%gA&NtQTz4jn~mdneJ&Q;2LV_acXf|GAd92fpS|M}ejYF3$m1 z;Do`{@22)(@E=-K3Td9qfG#p0P5zt(NudNQ2 zMvP@bFC9-o4Gsl#;i0T9fT}6f+8g4QBoi%`t>cBj0o*PHe{cqO-rtUB{Onu4hM(3| z;6b42F7lom&Z|AnZh_3f;k4~Pg0_FSg!A}v;zn|<5+*$4`C@qb(o7Pq;1VX7))`Uq4Q0=tR_u{c6 zhJm^Fy>wjs0H1u&k4V{t65j?3Isyn3u4T!}wbWLx<f*Z)$0GTciv%A6zL!Tbob<$&ATi)=bS}Eq978@fLYIY-l^xIzfF3~B%tJ+1j%`UCBx>tvy(gi{+MA|5M@2V*q`U&0cNJVtE=9s`qo?FJuO1C zP!Zv6hQTaXRHgIeJ5hfrusmnW}g z)s~#!Jk?5LPyfk(S#(@246$F z`k<8f(G1Z1-w+P#7|4VVRiGIH7CZRfeT_9+8euIQPf?FduDm=R6h9^Fr?YisE>@Qf z@!%Gm^Pi`=c>d9+D8|wHFORaa(><&n*9fiAV7d_6{rL%~-4=!IC7pyfB=a7?d8^!2YjR;oCWZz?=@OR99o|aub&iZcK}j_u-%R z9CKIHC($FLRWzm1bE9b@U1YDCy$yeU!^+dM4>ItM=4KCX@|!>~4C5lS>jojepNPdx zqC<>zY%*S71e?o++x8P4N2vrTDvzKW0#=6;kEEhl-MDSh##ayeC=H7wCA)vnA(=`z zNJT{$)d0ilq-Jc|&d@`Y7KTuTs2GtviKKWSQ0hZ>C*r}6H)6r#5(x$r+zD<1%Ri^? z>V_PDqywsMV0XDm^wRIHpt z^A&a3`$z{g18hzgNpaC>JtGpL%omO>rWdTZ-3F@RB++9<4h0CRR^mLiGmqjhbRvE~ zLD5dU+m57$C@l}6NKVp|qIsz)Au7s!;7TCgWshC25hyJttP2Pj7^0oH_;_rwIqh&o zF+o{J6mkpwB0-`9O>pG&vi6`;YT1kWXFRIMxEHVDdYIG(> z5MP0h(+)Tb)|5n?v7Mh0ZxM3Dpt5>dUAW>sSS$isu$*8(#)t_KL$uKJyLJ~B-uT;@Y} zC*ZPuUs)&wi_1|p1Ht0J7AK)A794In0vh46GGsaCK@%lxiK$ri0O5#;(JHquYQMI7I_XVhvn&z|c^AWduT6Od^X| zlGAWivq>L~3qoN$`nozm7~bWQzBE!u*Wpj2zRSr0QH# zu5r&OogtI0T$5_rrOD4$+c-9FJO`w!=rg$G8B_iy)$;kGL=*(8{ab62O-z1`n8)g$ zg;rL;qBq`U_;V6cvQT9t3f%@Z;q{Mi;j#<8CP)yd{Y!691c4gjujwGkm`6$!s=r1i zQR1U_4H`bJ()(QtDQ0yr0bm>Rg|3vPpLLXKK>?8`R&eKY{VEUsy>Rs{wqw59!{{9> z5IR$Ojx>fJU&@^?4ERugAwK24aARaD}Ii)#?T2LozLQ@ z|I3lgvfpemD!XT~X7+AEKhoi<`3l%S^zx*dCZZ*Qmxo@y; z;V!(wFWE4E-GZoi^}DQme+7lYuO^xbefe<1b_~hSg&Msif)3>l< zwf9u_PT`JuEO>7PR*K^i@`%@d8sXAIQu;eVEpv5woWrtww(N*!s+E%pNg_>3uI} zdi%Rrv+f|lY9%qu-uEA3)pq5VF}}-RNAB8O!e`j{0lsGyK@ zi&y!gTMwr9?8%~K-qYq$c#O3V3ry+Io~gHgPOhw;QGb2e^O@V>Np>H|uYQ>xnaLMd z^kI6RL45VfOtO`;g-(#I|Gbeo5538O>Ij=i30YIeGPzBArZu~nuaAY#tlr*y*8iy$ zix=(1r=D%LA&QqQ`2p=u1mE7pL+8n zuMKH}0BD2`Z^fGvJfqQ6%1el78j+l31Vg9aR+s$*OM=nRN=g|O;`Q`dwTe-z)-Zbc zdtBPP2@)!D(SF1SzTmoLtGIsQqXb4g#D;AVuxLbbtfc;T5+hfw;)a#$xnc2rG^vgk zC>SLFL<#Pz8c}p2pHS6?QjlNG-a6_lk)g=t^Rhc#4MXr_4Hvev`js)atFY9{d)Y-`^ek zxk{wG6jePdc%&fW==~(OXUwHrzXsSIe4F9M>vT&?I30z<0PIP)Q<891JAKDEIkhIu zp6<`dCA&@wd^wCJyiL}wFsclo5L%Lhq8n)50JgivF<{LaMz5bu(&8(bG5O?q^C6P6 z1nd3N8MS&HLl-_qc7zX*JVUprwF)BR{l3jMJzs4Ms5~+9XU%0Y= zb8I#HG4#WGsBgCb=_IucW9ZQiMpX@l57eN^6&;bXbBI(K((ZmqV4|f4pN@Qry_r>9ItM;w z!nG5~Ix>l=g23lJ?_l%V96)3LN3XGJ&r%i*9>UblU70fS!%A-tfdg!L;Z7#kY{%5r z0-ugs7ZvB4kGutMvuN%N*3Ei}8G^va?fy-c3i^p@OtsZu`QJVy|IgR*MVE$56dq+; zg^ZCqll|{MLYsGQrhcrEZgqJt(&|TTD{uB6`MiB6rnYIy)Z3qA&xtCX&C1uAHlPzz zJ9lR0{a@m6osuhI4$gjq8LbnU+Ct#dacijlsV)Yj)Fs6gcj4^0)t%g8JVwiNhz}ta z)3J4*dr>p9D2MYO=qtqRP55?o7i~^j{AoH@-Pj=(@Vgou-a%nf3hCXiAnEnxWL9?2 z8qQJAVk}!jjz9cuJ3nRPo=)_8_Ep4NK4y1y@;P=7Pu-SudjA=U=jX<$-CT5tb24Y6I--liqMDE z$EwjwOIGY%t|IWtJ*-}`w|ZW^D%2S@jnU6sQ6(_DjTU!4#()lKSOgHe_a}AFRstnJ z-~=U0_EBSG8=M{>bui8U5};t^POv6XcjNsKc2(F3-W8mD3^i=s%k&UIM3 zZ9U75ma%@r5f0RBN>`he-_D*)1IZc(dWiCj1zDZ*b; zK%k@yy)yd+gs_)DaS^_P0=z{fghN`iqZ2LYWrb7}6yw{okcv9PNv&rC-9V5Ol%gUk zii)TxD4?R)cmAn7T_tjG9cs;vH0qpyJ9`f&y%m+$wf~#JEx{u+@8LUBtVk~G8Mm|c zIT!s5gB#xK$I)w};raE`r!#G-Hf%u6*r5YJD(|QQMAltMLY1 z;%yXeT8whd71Vd1!|Ad8J(fS;nLfLBFec+L!*5S!_s5@;6$TE^Vf}j!IxgG6nC;sc zJ^orkK@AZo-m;WEnY9_Zqnt6BI^8XQWcrkxYL&XhZX=~udMpq*mBPr{u`y4G05zP=m%KT>CzG+hc;5I%@d*?VzR=pg6i=uaS)6KhEZNXHr^Ni2ralh!#5D{}fGK0c7>(Z~dj3b!b;R z!kWV3AH00R#N?;qf`yRMBc)_y9;8j5-Zb*WQ=l1$QZ!gEf`q3@7an3iOI9rAkfRO{ z^r(kq`wi`75FoL3Q}T}<#E|w9dU%O5VQGHO3ZIcZ@|Y}!kD|NC%StR%M<*?XXzHg%JcV%DHUBIx>Ap_dFDBl!tB zd~hu_F$guhf>xL0Vpk0+!ezv-e4VZ}q78M^F1N7fn_TkC1PXV1XmMW((eT}Mx#S;@ zbMS-Bl#d#Wpa{gj_by#})ItCn-aP?93;=eJBdCgsp~xuFPhweRMbM3Nb~roceTnGaKSuM|rK+q&r$6!I&&?*TS#yd`9Kfqd)Mz)9@azqg z+rw~dyIhmb z1nhR}51ul%*=^W8c63E2x%Ut_yr0}`13luyRo8_c@B%I%f~M-|vI13nox!3~O4K^k zQi~|qR}A*rBn^Cpf?Y4uu46TxpHPQOUwn}@SG8mQBfA)VV>|5DA164pid;6{{{Xg^ z7tYz|c9q9)zUpnsiU;oYTYhza@RVm3|5qsiF6l2#>z)pv*X8nqs~ zjmw(WsCFLezP%isw1c?X+c@mgDLH(Evi!XS`qsy0v3zeC8*MsM(-Bpr&ZNe47}6M! zDbPg>IfANzMG_DM4LuZzj_RE*Y=T%RPQqn{2-HOY3_S%;-8iC;HAwGyE9qwr&c79_ z)8k1*&B?(BRo44N(RL{*&G|7TdlYYma@zFcLqLkKWP%kYrjIdWFR$T?-nWD#OhZTlXA=BbZcEr zap65l?#^s176F|^(&AG{Oi03-5Kq~pskD6SWtug%AeSFz_2L9tS<2Ztgn~!OTeXSQj%}%#5~Skz0Sfa%h=PF;*M|7!wXg~vj6-ii*Bj_Q?mb*q*#F*k z+|8f*MYh|X<{h}q@*$hDuV7dUE0THirpa$os2Kw8gvR{imRhKWjv!gFO9B>)6{pRD zrh8ekU8DOQX*d8!?b>usTF-`ZouSd#*}nl6HI5lhbSUABUBXnP^#pVtLtdu=X4@t*-+{X ztc!?3(a|j2X|+jfQIFl@S5VY{Bx!(tWF6(UX1MFPk*#+8N3yV7nGWF$woP1*wPjDh zgC(gJTDUbGuJ4EaETMS5?3E)CiVtLBv`NOD;;2lgq3alx?jqyDg))oX)6i6ePW4F; zV$KP{f@E`?`x=KGi$y}P+J5vpy8Nu8C|w7U7MFy@qGQy%n&i5Fr)1~7q}eRdO&}3y zFscI?AAW-oS7mzNYKt~FksNl9AB+6XTw0!2s2K>hXsM|FgJevaOUmGJcoIrjJ9aE_ z?;3Qv6e!)q!o?dny?oP&-J?@+BnP_JuOjpvB*dp;zkE2In!A2bVQWg5%&Bj1tgspF zAGnTsKhpc66dqvLyHAtfXAXmgw5jxji%AK@wYdyYbrUZFl(YNu=kVP0e^}88m2PFj z>&r;F<{`TF|F*)kI_!uR3DIUhqk$J1_NIHoZ>OVihvsaWAHbRz{k5FA2CvirdwM!e z?s%TmvJkp10CohyisW!0i3T{*so(Ev>YrU=wnQ2Y9>^h^3&{#ZGRXcUjnqX|)+<=- zSgaNV!;Z!BGdJ9hhV<|5;GQLi8PU8Wn1`gB{3eB(AmT`@OV>&lM})|Z4eYPil3s1> zV1&8p?#4Xv@l1}kOGe8tgl<>yrxwoN)abNNWHu~MSP`6etIhhN{&u&GEC~!Et zkVr(Y;vfkTDziq{@o6%V@vpG1=2jep(=ws9nz_@Az zX%D}}lCi0X-b5TX_kvYHRBY6}@&<}O8_&uIXCo!Vfv({m`4pF38h_q0SN$dI8#)qw z*GQAWNqAqw#aM(K^}l!#&wr4y7AU;=cM?nzXAKYdj1R zPSjz=%Jswz)=>j8x&~s>AZkahV*CCPr0alojJ#W(V)da*aqQSY;1AOo*smQ4=Mia2 zP7wvq{*C3U?fAS_()&KfB}1BFMWFd#?qJp1PqK8L0A7JW3jwW49czByKRML(FD%{L z0>ha`t?T|s`&Mn}KBgC|Kl>Z=6%RJ6R;_x2t`OKfos)8J`u({VE{F6ZOVrDeuij7vbp5`~ai0{+a`SSjHBp&l& z{p2}1)(}D0$XWOzyO!q@-t-=2ksYj9-kzFc$I`V)9OBQ^AJiT^j7x8s$eJ-7>01L6 zlV22qaZ#d{8$?1O6v>Il7V`iYGCrSNxoF+ybm6vM+)2+vhYr!GQ6nrr(U8rE_z8s# zEUq|gRu$PFL@{i*<7`BHJ`8Id4oCEzH93SV3)o!_L@h)p7{Sm_Elw7ZoG$DRD{9b( zCb}!HGa`OM5doXW3E=>8q^i)dVoUOTuh&HOmt$BHu-i|!h)U3hYB+JabwZ^9bRoKn z6hp_5oH#sABrQlNBw&koo;A^<^$S8U{9O2@+aRz~-?d0%|yb9Fi;ZQgkHXbe`Wn4NWED4}ilJU2}Dj zY)e-n$iZOrouf4fdy*RqVCWi3(1)TJr@ccKH|{t)M!14tP(u)fsAo?Xu_VM{m;Q&k zXQ;@5Fowg4%_3F4s}c4SRwX36fUXGGoOVP3l%N+y5wUpUuqZx4AvHSXZ3swl2{^3h zKa@1$^Py>abYqKP!RB^jjkOzkBtSSMV;CTaHbk3&C_1s(Pg$r+#Op`Z4G=^u&iJU^ z>k4upfT9}Jt~X$yhXW{r6T4ly041s+2ZE@I28L1TRTgbd>~1?EI!Z8r5>YV>km8bX zSWlO=s&e`|!$5Q=AI$$`+i%w{N;BB@@oY|dLuQ}yOTo9Db^myrWwSD?^>fW%%>G%Q zawN0N6eSn&J{)W`QnWbX}F!Ra9lz%JF z0+erjj;uZ9KX2I`o5z=r&!X^L-<53V>#M`x%uPCjSy+$bCJdq`nzTx8LevrJapD!z&9sKx+PE2ntYVZwn3k^D^*7lxw| zTD})I120eG&iUKTUfaYJYCoE?BP__sLP0>$qP5S^6b>w!!iEMziM;qHimpy&-1IGY zL+Y~?_T9_%Cr)y!@hI}L3^W-~186;- z;;Nmy8FgSHsmots>-M8WfYRlEXW^923_84_ERs1`oVB;H)+~8UwTH zpEJLs%6Dho^IX?diK%(c>$EVD;xJnD#+APfAK%FGJFceU#{Y1`?)}`9`2wK{b59GQ z?@HsT?_z(S9V_xXulXO)f(00Vo5+y0I~cQnKMlpdv-r%VeObT7p>UtM97zV@Nx&5ycGfsxpsYYl_j9_#XLv2gbDDm^y>oMA(!yciEhgM`xu!`tNhR7obOqh@3-& zexHq>({^K@>5&RToj>QQHCq|8eLG|J9i~s4^S!u$AT}cL>Nso-YGY09L1G;%Vs%bX z>Twn0o_dx!g=WWXVhXh%fsc)Qc9K=24L$4Ds60GS)%Nh8?eX;M+8hB$ZCjV2S^=A7 z<=5v`lM<OTs(dntixb-L+-}E^Jo2%hBsm$~w z5*l^J@D*VwK>J&1-t*raTv!H_P`r5_w%!AArn%1NPUGmBt+*O=rNuCbyswK+dkQmF zv*h;XOcVq@|LABH<>4c5!J90cHHEdaUg3+@A|E$?lM}K5n#|$P?qTjn8(I61$TU%8 z#)N(NBLF(#eg9>GcnLH6x979rPq1%$K}=b$V7B;Aw)s~xPi)ELZdb8%)-Gf%hu!Zz z!t@bOATNK3HU0ZDz0WY_y||bnLKN;@NX2yv=+h~kXjG$AdOdt~G{#{hkAqVlW0KgD znY~2*bKe|7vCdAbC}iieb@;q%KW2;=#}*lp8^;h zpE!WY?|i^)aX8D~ewD?~UC#TXrxTEYEiduu-&b(><7=2wTV&!NKjK)9L1j=#;jF)7 zjauHS|MJy!morfinDIsNnbw-Im_@g=Wm=y>OcF&t5uMDNb`Xorf@HOlwP+Sg?&`#3 zQRIt7W#|{RK{+i%&m@N(??29r<~^BtsmS!tj-ms>qP;A;b0qVhzLoKJea41Y{>m&- zp6TW)@OovBdhjhAy*?6IWz8OLf`((>~#q;(qriXw0 zcca;Jo0vlFe|DxF%VJ5Un_Iirq<*|s#ho2T{WuBxDdi?uuvtSKE&8#e_+JnP?EU%` zQoFVy6^&4FE~Q!7loeK-9_^MF;w>s5C-Wq^ks36rnS`Z0hk)3Ylr+7vA+uUju?6-M z$vckly@&OR2%?Ax7~uj!`Mq(b+s>cV!ZLz6#rTW1b8@$zIz5_Tu>r)>0~zlxBD+ur$$`D^a~!yF53S`)MlE@az=CCD71g2rgKsfv>Fe09{0CR8TE(cf z3mEa_RisrE64(|ZzGEEHx4ecS@^(yP&#{3FRlniJ_2u+w_z5#UK8!|$()mwwq|QRF z-@1w$mo1~o;5c*x1Qd2W-XHO`B1W&<#OU{jb8N;}oXCk>K!dAC{2$ze3w)?$5rXdC zjPUK`#tnXYq&~%>uY(9;SB4me81|pXNO=5xhOSu47-I&a?$>_3!1-eGo5-$~c6bw+(t&X56Xm_0rd1p!g_U@eZP`GIi+SA}VD z{d2T(9w9?jL2@BX`hdJP!ZEB{F6ZmY;{@-TfCKSjm5EeN87kvW6Fo@DwMS=_kq7K-0nO3?THcBgE4 zo0aeOq~DrV+_-)-!~Rg0jPVo66Kjx=AW)&}2$6Ei3o1|r0Yi$z>aaz-^~@P7VwRNiHRxHekfX`m_3(}NltJCY z%$U1@jH4%6z5WPaA5%$i30$-&)<1`{&5{@uWm*NLJ6E#x=@Cr*+Y%yjRG4Uc)^m8$ zcy_(~83`lqplQ80@cU8HJUFW~_@X4>Osj!labTRICy08{1Vkg3(p@_^x!%L=Q~&@V z07*naRA(={Ui>G)8(yG8XV-btX&l)^!J?V$p0<=iy*|mBj1+s0(egUF3|vgc!xM1! zxt8R*&hy@{aOZXc!~4=q5~2A}Y6a$VBKyqWsPea`xeY}m{(~3k+&vwLqtU%HXqXrR z0}Mw88eTV&dZ~al1yAP)!7M0R@Hydef1yoU^d9kbdeE{>2}S;jm+7n{2#`WNe?g5H`+Qum%Tu~%Rj(uX`yqu-0vb zRO~|!>4>REDV#Hxj5)K(`0^``tdK+Ktf)La`}S z11Iu7ebycMiAwx#c7!dCA?i zgOfXp@Lb-LvTt@F2qL!2x{=bb8P>9PH0+ZKRuFAbufEvsE}C53mn6WMT9Y)rjBHK& zzK5}E!2&!_jii3;6HA9t)Nvgn6tv;27YCXZ|2LmA_n~RzStR^rKAh<((ShReWgPx0 zRt8@zBR6=F8``VEq3UauCd4r(G?VgL>jnbBV5H#MZk@vW*sEE9r4sO4+4_S zhU9b*^_%$!=jTx_`H{MfrPr`d(doMLsq@Q+()57`Xq^V(j9Gm7;vC{Xyo+>?9RZy> zeec0G@c?x@w~mUC@1>ahjNPb(Y3%y)IHDjC&{iUEJBePm{`Zaq3_Tf7Z4Z(qnm=jX zZXgXXR2d8nO;IY}TR=ur4UlXIRtr+429ODsRS;b&q+#ccJGxe>(h(&gIt5o;L}