From e93ad48ed92028be416a1331c410d2e955cbaa90 Mon Sep 17 00:00:00 2001 From: Lauris Jullien Date: Mon, 17 Jun 2024 17:52:38 +0200 Subject: [PATCH 1/7] Remove docker setup and update readme --- .github/workflows/local-setup-test.yaml | 8 +- .vscode/extensions.json | 5 +- Dockerfile | 24 - README-DOCKER.md | 229 --------- README.md | 189 ++++--- batect | 205 -------- batect.cmd | 473 ------------------ batect.yml | 41 -- docs/citibike.drawio | 1 - docs/citibike.png | Bin 39247 -> 0 bytes go.ps1 | 71 --- go.sh | 268 ---------- scripts/install.bat | 4 - scripts/install_choco.ps1 | 2 - scripts/mac_or_linux/install-with-colima.sh | 20 - .../install-with-docker-desktop.sh | 17 - scripts/mac_or_linux/integration-test.sh | 5 - scripts/mac_or_linux/linting.sh | 10 - .../run-colima-integration-test.sh | 5 - scripts/mac_or_linux/run-colima-job.sh | 5 - scripts/mac_or_linux/run-colima-unit-test.sh | 5 - .../run-docker-desktop-integration-test.sh | 5 - .../mac_or_linux/run-docker-desktop-job.sh | 5 - .../run-docker-desktop-unit-test.sh | 5 - scripts/mac_or_linux/run-job.sh | 38 -- scripts/mac_or_linux/start-colima.sh | 6 - scripts/mac_or_linux/unit-test.sh | 5 - scripts/win/install_with_docker_desktop.ps1 | 23 - scripts/win/linting.ps1 | 5 - .../run-docker-desktop-integration-test.ps1 | 1 - scripts/win/run-docker-desktop-job.ps1 | 1 - scripts/win/run-job.ps1 | 41 -- scripts/win/run-local-integration-test.ps1 | 1 - scripts/win/run-local-unit-test.ps1 | 1 - scripts/win/write-test.ps1 | 1 - tests/__init__.py | 0 36 files changed, 132 insertions(+), 1593 deletions(-) delete mode 100644 Dockerfile delete mode 100644 README-DOCKER.md delete mode 100755 batect delete mode 100644 batect.cmd delete mode 100644 batect.yml delete mode 100644 docs/citibike.drawio delete mode 100644 docs/citibike.png delete mode 100644 go.ps1 delete mode 100755 go.sh delete mode 100644 scripts/install.bat delete mode 100644 scripts/install_choco.ps1 delete mode 100755 scripts/mac_or_linux/install-with-colima.sh delete mode 100755 scripts/mac_or_linux/install-with-docker-desktop.sh delete mode 100755 scripts/mac_or_linux/integration-test.sh delete mode 100755 scripts/mac_or_linux/linting.sh delete mode 100755 scripts/mac_or_linux/run-colima-integration-test.sh delete mode 100755 scripts/mac_or_linux/run-colima-job.sh delete mode 100755 scripts/mac_or_linux/run-colima-unit-test.sh delete mode 100755 scripts/mac_or_linux/run-docker-desktop-integration-test.sh delete mode 100755 scripts/mac_or_linux/run-docker-desktop-job.sh delete mode 100755 scripts/mac_or_linux/run-docker-desktop-unit-test.sh delete mode 100755 scripts/mac_or_linux/run-job.sh delete mode 100755 scripts/mac_or_linux/start-colima.sh delete mode 100755 scripts/mac_or_linux/unit-test.sh delete mode 100644 scripts/win/install_with_docker_desktop.ps1 delete mode 100755 scripts/win/linting.ps1 delete mode 100755 scripts/win/run-docker-desktop-integration-test.ps1 delete mode 100755 scripts/win/run-docker-desktop-job.ps1 delete mode 100755 scripts/win/run-job.ps1 delete mode 100755 scripts/win/run-local-integration-test.ps1 delete mode 100755 scripts/win/run-local-unit-test.ps1 delete mode 100644 scripts/win/write-test.ps1 delete mode 100644 tests/__init__.py diff --git a/.github/workflows/local-setup-test.yaml b/.github/workflows/local-setup-test.yaml index 847cb5a..09e165c 100644 --- a/.github/workflows/local-setup-test.yaml +++ b/.github/workflows/local-setup-test.yaml @@ -23,10 +23,10 @@ jobs: poetry install - name: Run local unit tests run: | - ./go.sh run-local-unit-test + poetry run python -m pytest tests/unit - name: Run local integration tests run: | - ./go.sh run-local-integration-test + poetry run python -m pytest tests/integration windows: runs-on: windows-latest @@ -69,7 +69,7 @@ jobs: poetry install - name: Run local unit tests run: | - .\go.ps1 run-local-unit-test + poetry run python -m pytest tests/unit - name: Run local integration tests run: | - .\go.ps1 run-local-integration-test + poetry run python -m pytest tests/integration diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 9657cff..a584819 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,4 +1,3 @@ { - "recommendations": ["ms-python.python", "donjayamanne.python-environment-manager"] - } - \ No newline at end of file + "recommendations": ["ms-python.python"] +} diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 0b1dade..0000000 --- a/Dockerfile +++ /dev/null @@ -1,24 +0,0 @@ -ARG PYTHON_VERSION=3.11 -FROM --platform=linux/amd64 python:$PYTHON_VERSION -USER root -WORKDIR /opt -RUN if [ "$(arch)" = "aarch64" ] ; then ARCHITECTURE="aarch64" ; else ARCHITECTURE="x64"; fi && \ - wget -O OpenJDK.tar.gz https://github.com/AdoptOpenJDK/openjdk11-binaries/releases/download/jdk-11.0.11%2B9/OpenJDK11U-jdk_${ARCHITECTURE}_linux_hotspot_11.0.11_9.tar.gz && \ - wget -O scala.tgz https://downloads.lightbend.com/scala/2.13.5/scala-2.13.5.tgz && \ - wget -O spark-hadoop.tgz https://archive.apache.org/dist/spark/spark-3.5.1/spark-3.5.1-bin-hadoop3-scala2.13.tgz -RUN tar xzf OpenJDK.tar.gz && \ - tar xvf scala.tgz && \ - tar xvf spark-hadoop.tgz -ENV PATH="/opt/jdk-11.0.11+9/bin:/opt/scala-2.13.5/bin:/opt/spark-3.5.1-bin-hadoop3/bin:$PATH" - -RUN curl -sSL https://install.python-poetry.org | python3 - -ENV PATH="/root/.local/bin:${PATH}" -RUN poetry config virtualenvs.in-project false - -#TODO : Change the user to non root user -#USER 185 -WORKDIR /app - -COPY ./pyproject.toml /app/pyproject.toml - -RUN poetry install \ No newline at end of file diff --git a/README-DOCKER.md b/README-DOCKER.md deleted file mode 100644 index 9daae3d..0000000 --- a/README-DOCKER.md +++ /dev/null @@ -1,229 +0,0 @@ -# Data transformations with Python -This is a collection of _Python_ jobs that are supposed to transform data. -These jobs are using _PySpark_ to process larger volumes of data and are supposed to run on a _Spark_ cluster (via `spark-submit`). - -## Pre-requisites - -We use [`batect`](https://batect.dev/) to dockerise the tasks in this exercise. -`batect` is a lightweight wrapper around Docker that helps to ensure tasks run consistently (across linux, mac windows). -Similarly, `go.sh` / `go.ps` enables commands to be consistent across linux, mac & windows. -With `batect`, the only dependencies that need to be installed are Docker and Java >=8. Every other dependency is managed inside Docker containers. -If docker desktop can't be installed then Colima could be used on Mac and Linux. - -> **For Windows, docker desktop is the only option for using container to run application -otherwise local laptop should be set up.** - -Please make sure you have the following installed and can run them - -* Docker Desktop or Colima -* Java (11) - - -You could use following instructions as guidelines to install Docker or Colima and Java. - -```bash -# Install pre-requisites needed by batect -# For mac users: -./go.sh install-with-docker-desktop -OR -./go.sh install-with-colima - -# For windows/linux users: -# Please ensure Docker and java >=8 is installed -scripts\install_choco.ps1 -scripts\install.bat - -# For local laptop setup ensure that Java 11 with Spark 3.2.1 is available. More details in README-LOCAL.md -``` - -> **If you are using Colima, please ensure that you start Colima. For staring Colima, you could use following command:** - -`./go.sh start-colima` - - -> **Please install poetry if you would like to use lint command. Instructions to install poetry in [README-LOCAL](README.md) ** - - -## List of commands - -General pattern apart from installation and starting of Colima is: - -`./go.sh run--` - -type could be local, colima or docker-desktop - -action could be unit-test, integration-test or job. - -Full list of commands for Mac and Linux users is as follows: - -| S.No. | Command | Action | -| :---: | :---- | :--- | -| 1 | ./go.sh lint | Static analysis, code style, etc. (please install poetry if you would like to use this command) | -| 2 | ./go.sh linting | Static analysis, code style, etc. (please install poetry if you would like to use this command) | -| 3 | ./go.sh install-with-docker-desktop | Install the application requirements along with docker desktop | -| 4 | ./go.sh install-with-colima | Install the application requirements along with colima | -| 5 | ./go.sh start-colima | Start Colima | -| 6 | ./go.sh run-local-unit-test | Run unit tests on local machine | -| 7 | ./go.sh run-colima-unit-test | Run unit tests on containers using Colima | -| 8 | ./go.sh run-docker-desktop-unit-test | Run unit tests on containers using Docker Desktop | -| 9 | ./go.sh run-local-integration-test | Run integration tests on local machine | -| 10 | ./go.sh run-colima-integration-test | Run integration tests on containers using Colima | -| 11 | ./go.sh run-docker-desktop-integration-test | Run integration tests on containers using Docker Desktop | -| 12 | ./go.sh run-local-job | Run job on local machine | -| 13 | ./go.sh run-colima-job | Run job on containers using Colima | -| 14 | ./go.sh run-docker-desktop-job | Run job on containers using Docker Desktop | -| 15 | ./go.sh Usage | Display usage | - - -Full list of commands for Windows users is as follows: - -| S.No. | Command | Action | -| :---: | :---- | :--- | -| 1 | go.ps1 linting | Static analysis, code style, etc. (please install poetry if you would like to use this command) | -| 2 | go.ps1 install-with-docker-desktop | Install the application requirements along with docker desktop | -| 3 | go.ps1 run-local-unit-test | Run unit tests on local machine | -| 4 | go.ps1 run-docker-desktop-unit-test | Run unit tests on containers using Docker Desktop | -| 5 | go.ps1 run-local-integration-test | Run integration tests on local machine | -| 6 | go.ps1 run-docker-desktop-integration-test | Run integration tests on containers using Docker Desktop | -| 7 | go.ps1 run-local-job | Run job on local machine | -| 8 | go.ps1 run-docker-desktop-job | Run job on containers using Docker Desktop | -| 9 | go.ps1 Usage | Display usage | - - -## Jobs - -There are two applications in this repo: Word Count, and Citibike. - -Currently, these exist as skeletons, and have some initial test cases which are defined but ignored. -For each application, please un-ignore the tests and implement the missing logic. - -### Word Count -A NLP model is dependent on a specific input file. This job is supposed to preprocess a given text file to produce this -input file for the NLP model (feature engineering). This job will count the occurrences of a word within the given text -file (corpus). - -There is a dump of the datalake for this under `resources/word_count/words.txt` with a text file. - -#### Input -Simple `*.txt` file containing text. - -#### Output -A single `*.csv` file containing data similar to: -```csv -"word","count" -"a","3" -"an","5" -... -``` - -#### Run the job using Docker Desktop on Mac or Linux - -```bash -JOB=wordcount ./go.sh run-docker-desktop-job -``` - -#### Run the job using Docker Desktop on Windows - -```bash -$env:JOB = wordcount -.\go.ps1 run-docker-desktop-job -``` - -#### Run the job using Colima - -```bash -JOB=wordcount ./go.sh run-colima-job -``` - -### Citibike -***This problem uses data made publicly available by [Citibike](https://citibikenyc.com/), a New York based bike share company.*** - -For analytics purposes, the BI department of a hypothetical bike share company would like to present dashboards, displaying the -distance each bike was driven. There is a `*.csv` file that contains historical data of previous bike rides. This input -file needs to be processed in multiple steps. There is a pipeline running these jobs. - -![citibike pipeline](docs/citibike.png) - -There is a dump of the datalake for this under `resources/citibike/citibike.csv` with historical data. - -#### Ingest -Reads a `*.csv` file and transforms it to parquet format. The column names will be sanitized (whitespaces replaced). - -##### Input -Historical bike ride `*.csv` file: -```csv -"tripduration","starttime","stoptime","start station id","start station name","start station latitude",... -364,"2017-07-01 00:00:00","2017-07-01 00:06:05",539,"Metropolitan Ave & Bedford Ave",40.71534825,... -... -``` - -##### Output -`*.parquet` files containing the same content -```csv -"tripduration","starttime","stoptime","start_station_id","start_station_name","start_station_latitude",... -364,"2017-07-01 00:00:00","2017-07-01 00:06:05",539,"Metropolitan Ave & Bedford Ave",40.71534825,... -... -``` - -##### Run the job using Docker Desktop on Mac or Linux - -```bash -JOB=citibike_ingest ./go.sh run-docker-desktop-job -``` - -##### Run the job using Docker Desktop on Windows - -```bash -$env:JOB = citibike_ingest -.\go.ps1 run-docker-desktop-job -``` - -##### Run the job using Colima - -```bash -JOB=citibike_ingest ./go.sh run-colima-job -``` - -#### Distance calculation -This job takes bike trip information and calculates the "as the crow flies" distance traveled for each trip. -It reads the previously ingested data parquet files. - -Hint: - - For distance calculation, consider using [**Haversine formula**](https://en.wikipedia.org/wiki/Haversine_formula) as an option. - -##### Input -Historical bike ride `*.parquet` files -```csv -"tripduration",... -364,... -... -``` - -##### Outputs -`*.parquet` files containing historical data with distance column containing the calculated distance. -```csv -"tripduration",...,"distance" -364,...,1.34 -... -``` - -##### Run the job - -##### Run the job using Docker Desktop on Mac or Linux - -```bash -JOB=citibike_distance_calculation ./go.sh run-docker-desktop-job -``` - -##### Run the job using Docker Desktop on Windows - -```bash -$env:JOB = citibike_distance_calculation -.\go.ps1 run-docker-desktop-job -``` - -##### Run the job using Colima - -```bash -JOB=citibike_distance_calculation ./go.sh run-colima-job -``` \ No newline at end of file diff --git a/README.md b/README.md index 5cb73d6..0d05eb5 100644 --- a/README.md +++ b/README.md @@ -1,83 +1,160 @@ # Data transformations with Python -This is a collection of _Python_ jobs that are supposed to transform data. + +This coding challenge is a collection of _Python_ jobs that are supposed to extract, transform and load data. These jobs are using _PySpark_ to process larger volumes of data and are supposed to run on a _Spark_ cluster (via `spark-submit`). -## Pre-requisites +## Before the interview + +**✅ Goals** + +- **Get a working environment** + Either local ([local](#local-setup), or using gitpod) +- **Get an overview of the codebase and technologies involved** + +**❌ Non-Goals** + +- solving the exercises / writing code + > The exercises will be given at the time of interview, and solved by pairing with the interviewer. + + + +### Local Setup + +> 💡 If you don't manage to run the local setup, use the [gitpod](#gitpod-setup) one + +#### Pre-requisites + Please make sure you have the following installed and can run them -* Python (3.11.x), you can use for example [pyenv](https://github.com/pyenv/pyenv#installation) to manage your python versions locally -* [Poetry](https://python-poetry.org/docs/#installation) -* Java (11) - * To run pySpark, it's important that the environment variable `JAVA_HOME` is set correctly, check via `echo $JAVA_HOME` - * [test_validate_spark_environment.py](/tests/integration/test_validate_spark_environment.py) will help you figure out if your environment will work -## Install all dependencies +- Python (3.11.X), + you can use for example [pyenv](https://github.com/pyenv/pyenv#installation) to manage your python versions locally +- [Poetry](https://python-poetry.org/docs/#installation) +- Java (11), you can use [sdkman](https://sdkman.io/) to install and manage java locally + +#### Windows users + +We recommend using WSL 2 on Windows for this exercise, due to the [lack of support](https://cwiki.apache.org/confluence/display/HADOOP2/WindowsProblems) of windows paths from Hadoop/Spark. + +Follow instructions on the [Windows official page](https://learn.microsoft.com/en-us/windows/wsl/setup/environment) + +> 💡 In case of issues, like missing permissions on the machine, please use the [gitpod setup](#gitpod-setup) + +#### Install all dependencies + ```bash poetry install ``` -## Setup -### Run tests +### Gitpod setup + +Alternatively, you can setup the environment using + +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/techops-recsys-lateral-hiring/dataengineer-transformations-python) + +There's an initialize script setup that takes around 3 minutes to complete. Once you use paste this repository link in new Workspace, please wait until the packages are installed. After everything is setup, select Poetry's environment by clicking on thumbs up icon and navigate to Testing tab and hit refresh icon to discover tests. + +Note that you can use gitpod's web interface or setup [ssh to Gitpod](https://www.gitpod.io/docs/references/ides-and-editors/vscode#connecting-to-vs-code-desktop) so that you can use VS Code from local to remote to Gitpod + +Remember to stop the vm and restart it just before the interview. + +### Verify setup + +The following commands should be running successfully #### Run unit tests + ```bash poetry run pytest tests/unit ``` #### Run integration tests + ```bash poetry run pytest tests/integration ``` #### Run style checks + ```bash poetry run mypy --ignore-missing-imports --disallow-untyped-calls --disallow-untyped-defs --disallow-incomplete-defs \ data_transformations tests poetry run pylint data_transformations tests ``` -This is running the linter and a type checker. -## Create package (optional) -This will create a `tar.gz` and a `.wheel` in `dist/` folder: -```bash -# Install pre-requisites needed by batect -# For mac users: -./go.sh install-with-docker-desktop -OR -./go.sh install-with-colima +### Anything else? -# For windows/linux users: -# Please ensure Docker and java >=8 is installed -scripts\install_choco.ps1 -scripts\install.bat +You are good to go! -# For local laptop setup ensure that Java 11 with Spark 3.5.1 is available. -``` -More: https://python-poetry.org/docs/cli/#build +> ⚠️ do not try to solve the exercises ahead of the interview ---- -# STOP HERE: Do not code before the interview begins. ---- +You are allowed to customize your environment (having the test in vscode directly for example): feel free to spend the time making this comfortable for you. This is not an expectation. ## Jobs -There are two applications in this repo: Word Count, and Citibike. +There are two exercises in this repo: Word Count, and Citibike. Currently, these exist as skeletons, and have some initial test cases which are defined but ignored. -For each application, please un-ignore the tests and implement the missing logic. + +The following section provides context over them. + +> ⚠️ do not try to solve the exercises ahead of the interview + +### Code walk + +``` + +/ +├─ /data_transformations # Contains the main python library +│ # with the code to the transformations +│ +├─ /jobs # Contains the entry points to the jobs +│ # performs argument parsing, and are +│ # passed to `spark-submit` +│ +├─ /resources # Contains the raw datasets for the jobs +│ +├─ /tests +│ ├─ /units # contains basic unit tests for the code +│ └─ /integration # contains integrations tests for the jobs +│ # and the setup +│ +├─ .gitignore +├─ .gitpod\* # required for the gitpod setup +├─ .pylintrc # configuration for pylint +├─ LICENCE +├─ poetry.lock +├─ pyproject.toml +└─ README.md # The current file + +``` ### Word Count + A NLP model is dependent on a specific input file. This job is supposed to preprocess a given text file to produce this input file for the NLP model (feature engineering). This job will count the occurrences of a word within the given text -file (corpus). +file (corpus). There is a dump of the datalake for this under `resources/word_count/words.txt` with a text file. +```mermaid +--- +title: Citibike Pipeline +--- +flowchart LR + Raw["fa:fa-file words.txt"] --> J1{{word_count.py}} --> Bronze["fa:fa-file-csv word_count.csv"] +``` + #### Input + Simple `*.txt` file containing text. #### Output + A single `*.csv` file containing data similar to: + ```csv "word","count" "a","3" @@ -86,9 +163,9 @@ A single `*.csv` file containing data similar to: ``` #### Run the job -Please make sure to package the code before submitting the spark job (`poetry build`) + ```bash -poetry run spark-submit \ +poetry build && poetry run spark-submit \ --master local \ --py-files dist/data_transformations-*.whl \ jobs/word_count.py \ @@ -104,7 +181,13 @@ For analytics purposes, the BI department of a hypothetical bike share company w distance each bike was driven. There is a `*.csv` file that contains historical data of previous bike rides. This input file needs to be processed in multiple steps. There is a pipeline running these jobs. -![citibike pipeline](docs/citibike.png) +```mermaid +--- +title: Citibike Pipeline +--- +flowchart TD + Raw["fa:fa-file-csv citibike.csv"] --> J1{{citibike_ingest.py}} --> Bronze["fa:fa-table-columns citibike.parquet"] --> J2{{citibike_distance_calculation.py}} --> Silver["fa:fa-table-columns citibike_distance.parquet"] +``` There is a dump of the datalake for this under `resources/citibike/citibike.csv` with historical data. @@ -133,9 +216,9 @@ Historical bike ride `*.csv` file: ``` ##### Run the job -Please make sure to package the code before submitting the spark job (`poetry build`) + ```bash -poetry run spark-submit \ +poetry build && poetry run spark-submit \ --master local \ --py-files dist/data_transformations-*.whl \ jobs/citibike_ingest.py \ @@ -145,7 +228,7 @@ poetry run spark-submit \ #### Distance calculation -This job takes bike trip information and calculates the "as the crow flies" distance traveled for each trip. +This job takes bike trip information and adds the "as the crow flies" distance traveled for each trip. It reads the previously ingested data parquet files. Hint: @@ -173,9 +256,9 @@ Historical bike ride `*.parquet` files ``` ##### Run the job -Please make sure to package the code before submitting the spark job (`poetry build`) + ```bash -poetry run spark-submit \ +poetry build && poetry run spark-submit \ --master local \ --py-files dist/data_transformations-*.whl \ jobs/citibike_distance_calculation.py \ @@ -183,28 +266,8 @@ poetry run spark-submit \ ``` -## Running the code inside container - -If you would like to run the code in Docker, please follow instructions [here](README-DOCKER.md). - -## Running the code on Gitpod - -Alternatively, you can setup the environment using - -[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/techops-recsys-lateral-hiring/dataengineer-transformations-python) - -It's recommend that you setup ssh to Gitpod so that you can use VS Code from local to remote to Gitpod. - -There's an initialize script setup that takes around 3 minutes to complete. Once you use paste this repository link in new Workspace, please wait until the packages are installed. After everything is setup, select Poetry's environment by clicking on thumbs up icon and navigate to Testing tab and hit refresh icon to discover tests. - -### Common issue with VS Code's Testing - -If Testing tab complains about Python Interpreter, run `poetry shell` in terminal to get the bin path, replace activate with python3 to resolve the issue. - -If poetry shell activate with this path - -`/workspace/.pyenv_mirror/poetry/virtualenvs/{project_name}-py{python_version}/bin/activate` +--- -Paste this into Python Interpreter prompt +> ⚠️ do not try to solve the exercises ahead of the interview -`/workspace/.pyenv_mirror/poetry/virtualenvs/{project_name}-py{python_version}/bin/python3` +--- diff --git a/batect b/batect deleted file mode 100755 index 3fc8809..0000000 --- a/batect +++ /dev/null @@ -1,205 +0,0 @@ -#!/usr/bin/env bash - -{ - set -euo pipefail - - # This file is part of Batect. - # Do not modify this file. It will be overwritten next time you upgrade Batect. - # You should commit this file to version control alongside the rest of your project. It should not be installed globally. - # For more information, visit https://github.com/batect/batect. - - VERSION="0.85.0" - CHECKSUM="${BATECT_DOWNLOAD_CHECKSUM:-901ed73295be75d295cec1d06315f7026b36ccb1666660b8af432cfbbc7feae8}" - DOWNLOAD_URL_ROOT=${BATECT_DOWNLOAD_URL_ROOT:-"https://updates.batect.dev/v1/files"} - DOWNLOAD_URL=${BATECT_DOWNLOAD_URL:-"$DOWNLOAD_URL_ROOT/$VERSION/batect-$VERSION.jar"} - QUIET_DOWNLOAD=${BATECT_QUIET_DOWNLOAD:-false} - - BATECT_WRAPPER_CACHE_DIR=${BATECT_CACHE_DIR:-"$HOME/.batect/cache"} - VERSION_CACHE_DIR="$BATECT_WRAPPER_CACHE_DIR/$VERSION" - JAR_PATH="$VERSION_CACHE_DIR/batect-$VERSION.jar" - BATECT_WRAPPER_DID_DOWNLOAD=false - - SCRIPT_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - - function main() { - if ! haveVersionCachedLocally; then - download - BATECT_WRAPPER_DID_DOWNLOAD=true - fi - - checkChecksum - runApplication "$@" - } - - function haveVersionCachedLocally() { - [ -f "$JAR_PATH" ] - } - - function download() { - checkForCurl - - mkdir -p "$VERSION_CACHE_DIR" - temp_file=$(mktemp) - - if [[ $QUIET_DOWNLOAD == 'true' ]]; then - curl --silent --fail --show-error --location --output "$temp_file" --retry 3 --retry-connrefused "$DOWNLOAD_URL" - else - echo "Downloading Batect version $VERSION from $DOWNLOAD_URL..." - curl -# --fail --show-error --location --output "$temp_file" --retry 3 --retry-connrefused "$DOWNLOAD_URL" - fi - - mv "$temp_file" "$JAR_PATH" - } - - function checkChecksum() { - local_checksum=$(getLocalChecksum) - - if [[ "$local_checksum" != "$CHECKSUM" ]]; then - echo "The downloaded version of Batect does not have the expected checksum. Delete '$JAR_PATH' and then re-run this script to download it again." - exit 1 - fi - } - - function getLocalChecksum() { - if [[ "$(uname)" == "Darwin" ]]; then - shasum -a 256 "$JAR_PATH" | cut -d' ' -f1 - else - sha256sum "$JAR_PATH" | cut -d' ' -f1 - fi - } - - function runApplication() { - java_path=$(getPathToJava) - checkForJava "$java_path" - - java_version_info=$(getJavaVersionInfo "$java_path") - checkJavaVersion "$java_version_info" - - java_version=$(extractJavaVersion "$java_version_info") - java_version_major=$(extractJavaMajorVersion "$java_version") - - if (( java_version_major >= 9 )); then - JAVA_OPTS=(--add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED) - else - JAVA_OPTS=() - fi - - if [[ "$(uname -o 2>&1)" == "Msys" ]] && hash winpty 2>/dev/null && [ -t /dev/stdin ]; then - GIT_BASH_PTY_WORKAROUND=(winpty) - else - GIT_BASH_PTY_WORKAROUND=() - fi - - BATECT_WRAPPER_SCRIPT_DIR="$SCRIPT_PATH" \ - BATECT_WRAPPER_CACHE_DIR="$BATECT_WRAPPER_CACHE_DIR" \ - BATECT_WRAPPER_DID_DOWNLOAD="$BATECT_WRAPPER_DID_DOWNLOAD" \ - HOSTNAME="$HOSTNAME" \ - exec \ - ${GIT_BASH_PTY_WORKAROUND[@]+"${GIT_BASH_PTY_WORKAROUND[@]}"} \ - "$java_path" \ - -Djava.net.useSystemProxies=true \ - ${JAVA_OPTS[@]+"${JAVA_OPTS[@]}"} \ - -jar "$JAR_PATH" \ - "$@" - } - - function checkForCurl() { - if ! hash curl 2>/dev/null; then - echo "curl is not installed or not on your PATH. Please install it and try again." >&2 - exit 1 - fi - } - - function getPathToJava() { - if useJavaHome; then - echo "$JAVA_HOME/bin/java" - else - echo "java" - fi - } - - function useJavaHome() { - test -n "${JAVA_HOME+x}" - } - - function checkForJava() { - local java_path="$1" - - if ! hash "$java_path" 2>/dev/null; then - showJavaNotInstalledError - fi - } - - function showJavaNotInstalledError() { - if useJavaHome; then - echo "JAVA_HOME is set to '$JAVA_HOME', but there is no Java executable at '$JAVA_HOME/bin/java'." >&2 - else - echo "Java is not installed or not on your PATH. Please install it and try again." >&2 - fi - - exit 1 - } - - function checkJavaVersion() { - java_version_info="$1" - java_version=$(extractJavaVersion "$java_version_info") - java_version_major=$(extractJavaMajorVersion "$java_version") - java_version_minor=$(extractJavaMinorVersion "$java_version") - - if (( java_version_major < 1 || ( java_version_major == 1 && java_version_minor <= 7 ) )); then - if useJavaHome; then - echo "The version of Java that is available in JAVA_HOME is version $java_version, but version 1.8 or greater is required." >&2 - echo "If you have a newer version of Java installed, please make sure JAVA_HOME is set correctly." >&2 - echo "JAVA_HOME takes precedence over any versions of Java available on your PATH." >&2 - else - echo "The version of Java that is available on your PATH is version $java_version, but version 1.8 or greater is required." >&2 - echo "If you have a newer version of Java installed, please make sure your PATH is set correctly." >&2 - fi - - exit 1 - fi - - if ! javaIs64Bit "$java_version_info"; then - if useJavaHome; then - echo "The version of Java that is available in JAVA_HOME is a 32-bit version, but Batect requires a 64-bit Java runtime." >&2 - echo "If you have a 64-bit version of Java installed, please make sure JAVA_HOME is set correctly." >&2 - echo "JAVA_HOME takes precedence over any versions of Java available on your PATH." >&2 - else - echo "The version of Java that is available on your PATH is a 32-bit version, but Batect requires a 64-bit Java runtime." >&2 - echo "If you have a 64-bit version of Java installed, please make sure your PATH is set correctly." >&2 - fi - - exit 1 - fi - } - - function getJavaVersionInfo() { - local java_path="$1" - - "$java_path" -version 2>&1 || showJavaNotInstalledError - } - - function extractJavaVersion() { - echo "$1" | grep version | sed -En ';s/.* version "([0-9]+)(\.([0-9]+))?.*".*/\1.\3/p;' - } - - function extractJavaMajorVersion() { - java_version=$1 - - echo "${java_version%.*}" - } - - function extractJavaMinorVersion() { - java_version=$1 - java_version_minor="${java_version#*.}" - - echo "${java_version_minor:-0}" - } - - function javaIs64Bit() { - echo "$1" | grep -q '64-[Bb]it' - } - - main "$@" - exit $? -} diff --git a/batect.cmd b/batect.cmd deleted file mode 100644 index aba86ae..0000000 --- a/batect.cmd +++ /dev/null @@ -1,473 +0,0 @@ -@echo off -rem This file is part of Batect. -rem Do not modify this file. It will be overwritten next time you upgrade Batect. -rem You should commit this file to version control alongside the rest of your project. It should not be installed globally. -rem For more information, visit https://github.com/batect/batect. - -setlocal EnableDelayedExpansion - -set "version=0.85.0" - -if "%BATECT_CACHE_DIR%" == "" ( - set "BATECT_CACHE_DIR=%USERPROFILE%\.batect\cache" -) - -set "rootCacheDir=!BATECT_CACHE_DIR!" -set "cacheDir=%rootCacheDir%\%version%" -set "ps1Path=%cacheDir%\batect-%version%.ps1" - -set script=Set-StrictMode -Version 2.0^ - -$ErrorActionPreference = 'Stop'^ - -^ - -$Version='0.85.0'^ - -^ - -function getValueOrDefault($value, $default) {^ - - if ($value -eq $null) {^ - - $default^ - - } else {^ - - $value^ - - }^ - -}^ - -^ - -$DownloadUrlRoot = getValueOrDefault $env:BATECT_DOWNLOAD_URL_ROOT "https://updates.batect.dev/v1/files"^ - -$UrlEncodedVersion = [Uri]::EscapeDataString($Version)^ - -$DownloadUrl = getValueOrDefault $env:BATECT_DOWNLOAD_URL "$DownloadUrlRoot/$UrlEncodedVersion/batect-$UrlEncodedVersion.jar"^ - -$ExpectedChecksum = getValueOrDefault $env:BATECT_DOWNLOAD_CHECKSUM '901ed73295be75d295cec1d06315f7026b36ccb1666660b8af432cfbbc7feae8'^ - -^ - -$RootCacheDir = getValueOrDefault $env:BATECT_CACHE_DIR "$env:USERPROFILE\.batect\cache"^ - -$VersionCacheDir = "$RootCacheDir\$Version"^ - -$JarPath = "$VersionCacheDir\batect-$Version.jar"^ - -$DidDownload = 'false'^ - -^ - -function main() {^ - - if (-not (haveVersionCachedLocally)) {^ - - download^ - - $DidDownload = 'true'^ - - }^ - -^ - - checkChecksum^ - - runApplication @args^ - -}^ - -^ - -function haveVersionCachedLocally() {^ - - Test-Path $JarPath^ - -}^ - -^ - -function download() {^ - - Write-Output "Downloading Batect version $Version from $DownloadUrl..."^ - -^ - - createCacheDir^ - -^ - - $oldProgressPreference = $ProgressPreference^ - -^ - - try {^ - - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12^ - -^ - - # Turn off the progress bar to significantly reduce download times - see https://github.com/PowerShell/PowerShell/issues/2138#issuecomment-251165868^ - - $ProgressPreference = 'SilentlyContinue'^ - -^ - - Invoke-WebRequest -Uri $DownloadUrl -OutFile $JarPath ^| Out-Null^ - - } catch {^ - - $Message = $_.Exception.Message^ - -^ - - Write-Host -ForegroundColor Red "Downloading failed with error: $Message"^ - - exit 1^ - - } finally {^ - - $ProgressPreference = $oldProgressPreference^ - - }^ - -}^ - -^ - -function checkChecksum() {^ - - $localChecksum = (Get-FileHash -Algorithm 'SHA256' $JarPath).Hash.ToLower()^ - -^ - - if ($localChecksum -ne $expectedChecksum) {^ - - Write-Host -ForegroundColor Red "The downloaded version of Batect does not have the expected checksum. Delete '$JarPath' and then re-run this script to download it again."^ - - exit 1^ - - }^ - -}^ - -^ - -function createCacheDir() {^ - - if (-not (Test-Path $VersionCacheDir)) {^ - - New-Item -ItemType Directory -Path $VersionCacheDir ^| Out-Null^ - - }^ - -}^ - -^ - -function runApplication() {^ - - $java = findJava^ - - $javaVersion = checkJavaVersion $java^ - -^ - - if ($javaVersion.Major -ge 9) {^ - - $javaArgs = @("--add-opens", "java.base/sun.nio.ch=ALL-UNNAMED", "--add-opens", "java.base/java.io=ALL-UNNAMED")^ - - } else {^ - - $javaArgs = @()^ - - }^ - -^ - - $combinedArgs = $javaArgs + @("-Djava.net.useSystemProxies=true", "-jar", $JarPath) + $args^ - - $env:HOSTNAME = $env:COMPUTERNAME^ - - $env:BATECT_WRAPPER_CACHE_DIR = $RootCacheDir^ - - $env:BATECT_WRAPPER_DID_DOWNLOAD = $DidDownload^ - -^ - - $info = New-Object System.Diagnostics.ProcessStartInfo^ - - $info.FileName = $java.Source^ - - $info.Arguments = combineArgumentsToString($combinedArgs)^ - - $info.RedirectStandardError = $false^ - - $info.RedirectStandardOutput = $false^ - - $info.UseShellExecute = $false^ - -^ - - $process = New-Object System.Diagnostics.Process^ - - $process.StartInfo = $info^ - - $process.Start() ^| Out-Null^ - - $process.WaitForExit()^ - -^ - - exit $process.ExitCode^ - -}^ - -^ - -function useJavaHome() {^ - - return ($env:JAVA_HOME -ne $null)^ - -}^ - -^ - -function findJava() {^ - - if (useJavaHome) {^ - - $java = Get-Command "$env:JAVA_HOME\bin\java" -ErrorAction SilentlyContinue^ - -^ - - if ($java -eq $null) {^ - - Write-Host -ForegroundColor Red "JAVA_HOME is set to '$env:JAVA_HOME', but there is no Java executable at '$env:JAVA_HOME\bin\java.exe'."^ - - exit 1^ - - }^ - -^ - - return $java^ - - }^ - -^ - - $java = Get-Command "java" -ErrorAction SilentlyContinue^ - -^ - - if ($java -eq $null) {^ - - Write-Host -ForegroundColor Red "Java is not installed or not on your PATH. Please install it and try again."^ - - exit 1^ - - }^ - -^ - - return $java^ - -}^ - -^ - -function checkJavaVersion([System.Management.Automation.CommandInfo]$java) {^ - - $versionInfo = getJavaVersionInfo $java^ - - $rawVersion = getJavaVersion $versionInfo^ - - $parsedVersion = New-Object Version -ArgumentList $rawVersion^ - - $minimumVersion = "1.8"^ - -^ - - if ($parsedVersion -lt (New-Object Version -ArgumentList $minimumVersion)) {^ - - if (useJavaHome) {^ - - Write-Host -ForegroundColor Red "The version of Java that is available in JAVA_HOME is version $rawVersion, but version $minimumVersion or greater is required."^ - - Write-Host -ForegroundColor Red "If you have a newer version of Java installed, please make sure JAVA_HOME is set correctly."^ - - Write-Host -ForegroundColor Red "JAVA_HOME takes precedence over any versions of Java available on your PATH."^ - - } else {^ - - Write-Host -ForegroundColor Red "The version of Java that is available on your PATH is version $rawVersion, but version $minimumVersion or greater is required."^ - - Write-Host -ForegroundColor Red "If you have a newer version of Java installed, please make sure your PATH is set correctly."^ - - }^ - -^ - - exit 1^ - - }^ - -^ - - if (-not ($versionInfo -match "64\-[bB]it")) {^ - - if (useJavaHome) {^ - - Write-Host -ForegroundColor Red "The version of Java that is available in JAVA_HOME is a 32-bit version, but Batect requires a 64-bit Java runtime."^ - - Write-Host -ForegroundColor Red "If you have a 64-bit version of Java installed, please make sure JAVA_HOME is set correctly."^ - - Write-Host -ForegroundColor Red "JAVA_HOME takes precedence over any versions of Java available on your PATH."^ - - } else {^ - - Write-Host -ForegroundColor Red "The version of Java that is available on your PATH is a 32-bit version, but Batect requires a 64-bit Java runtime."^ - - Write-Host -ForegroundColor Red "If you have a 64-bit version of Java installed, please make sure your PATH is set correctly."^ - - }^ - -^ - - exit 1^ - - }^ - -^ - - return $parsedVersion^ - -}^ - -^ - -function getJavaVersionInfo([System.Management.Automation.CommandInfo]$java) {^ - - $info = New-Object System.Diagnostics.ProcessStartInfo^ - - $info.FileName = $java.Source^ - - $info.Arguments = "-version"^ - - $info.RedirectStandardError = $true^ - - $info.RedirectStandardOutput = $true^ - - $info.UseShellExecute = $false^ - -^ - - $process = New-Object System.Diagnostics.Process^ - - $process.StartInfo = $info^ - - $process.Start() ^| Out-Null^ - - $process.WaitForExit()^ - -^ - - $stderr = $process.StandardError.ReadToEnd()^ - - return $stderr^ - -}^ - -^ - -function getJavaVersion([String]$versionInfo) {^ - - $versionLine = ($versionInfo -split [Environment]::NewLine)[0]^ - -^ - - if (-not ($versionLine -match "version `"([0-9]+)(\.([0-9]+))?.*`"")) {^ - - Write-Error "Java reported a version that does not match the expected format: $versionLine"^ - - }^ - -^ - - $major = $Matches.1^ - -^ - - if ($Matches.Count -ge 3) {^ - - $minor = $Matches.3^ - - } else {^ - - $minor = "0"^ - - }^ - -^ - - return "$major.$minor"^ - -}^ - -^ - -function combineArgumentsToString([Object[]]$arguments) {^ - - $combined = @()^ - -^ - - $arguments ^| %% { $combined += escapeArgument($_) }^ - -^ - - return $combined -join " "^ - -}^ - -^ - -function escapeArgument([String]$argument) {^ - - return '"' + $argument.Replace('"', '"""') + '"'^ - -}^ - -^ - -main @args^ - - - -if not exist "%cacheDir%" ( - mkdir "%cacheDir%" -) - -echo !script! > "%ps1Path%" - -set BATECT_WRAPPER_SCRIPT_DIR=%~dp0 - -rem Why do we explicitly exit? -rem cmd.exe appears to read this script one line at a time and then executes it. -rem If we modify the script while it is still running (eg. because we're updating it), then cmd.exe does all kinds of odd things -rem because it continues execution from the next byte (which was previously the end of the line). -rem By explicitly exiting on the same line as starting the application, we avoid these issues as cmd.exe has already read the entire -rem line before we start the application and therefore will always exit. - -rem Why do we set PSModulePath? -rem See issue #627 -set "PSModulePath=" -powershell.exe -ExecutionPolicy Bypass -NoLogo -NoProfile -File "%ps1Path%" %* && exit /b 0 || exit /b !ERRORLEVEL! - -rem What's this for? -rem This is so the tests for the wrapper has a way to ensure that the line above terminates the script correctly. -echo WARNING: you should never see this, and if you do, then Batect's wrapper script has a bug diff --git a/batect.yml b/batect.yml deleted file mode 100644 index f401b4f..0000000 --- a/batect.yml +++ /dev/null @@ -1,41 +0,0 @@ -containers: - pyspark: - build_directory: . - volumes: - - local: . - container: /app - options: cached - - type: cache - name: poetry-dependencies - container: /root/.cache/pypoetry/virtualenvs - -tasks: - unit-test: - description: Unit tests - group: Test - run: - container: pyspark - entrypoint: scripts/mac_or_linux/unit-test.sh - - integration-test: - description: Integration tests - group: Test - run: - container: pyspark - entrypoint: scripts/mac_or_linux/integration-test.sh - - style-checks: - description: Lint and type check - group: Test - run: - container: pyspark - entrypoint: scripts/mac_or_linux/linting.sh - - run-job: - description: Run spark job - group: Run - run: - container: pyspark - entrypoint: scripts/mac_or_linux/run-job.sh - environment: - JOB: $JOB diff --git a/docs/citibike.drawio b/docs/citibike.drawio deleted file mode 100644 index 55ba18c..0000000 --- a/docs/citibike.drawio +++ /dev/null @@ -1 +0,0 @@ -7VjdbpswGH2aXDbCGCi5bLNuq6ap0yqtl5VjDFhxMDUmIXv6GbBJ+ElJu6atql5hH9uf7XPOZzATOF8V3wRK4588IGxiW0ExgV8mtg2A76lHiWxrxPNBDUSCBrrTDrilf4kGLY3mNCBZq6PknEmatkHMk4Rg2cKQEHzT7hZy1p41RRHpAbcYsT56RwMZ16jvWjv8O6FRbGYGlm5ZIdNZA1mMAr7Zg+DVBM4F57IurYo5YSV5hpd63NcDrc3CBEnkMQNukt+X2z/+2V1x/fMaLufF3Y/4TKuzRizXG57YHlqlE3iZLLLy0a9bmEq6oEtSTk9TwmhC9Bbl1vAmeJ4EpJwaqEGbmEpymyJctm6UUxQWyxXTzSFPpJYeOKqOGI0SVWEkVFu7XBMhqZLkQsOS1wupVq7aSHGQEtAQrRxK+IpIsVVdtBkh1NpsdtLOjF7xvqyeBpG2U9TE2jGuCpr0JwgAegIYdu9pEpFMTtPtI+RaTyc3pIzNOeOiigXDMLQxVngmBV+SvZbAW3iu959MmwEmYQzzfp/5ps8+8ycj3h5yPpOasBbf3kPOTcNZVlF5oToAJy3qBNHtqhSVT6PgFGdrE1StsY5bd+lJqk6HtCwmXJJxTRcIL6PKBTe5rPKvxgMkljdqFJUlz9bUcscNECDih4MG8LBPFuHLGAB29Pf6+vtDiWedSn94UP+ArrtK3gc0kyjB5F4dQzhnSFKeVJnZqLs36gPkqzMbz1f7VfPVeZl8PZysipyHnMjPhNWKO92MdfsnNhhwwOxUDnCPydiXNUWT9wddMZz/J1rWqEePOo0+unPP/Y5zXXt6pHcBcE9k3vMB83Z0IUlwUd5YVA0zlGUUP/5iqAOQoHeB6RClJuG5wGT8Y0giERE59rXaJ36P1qF3gsEEKV+c6/Zyh5jWM/zitMqWon2wmJtk99Og3qUetH8R6sQBnZPNdTuBahp6gSrdm10/3wr+u7aCuWSPWcF5SyvY5x0vgOd6wWsHcuzX9cLsXXvBOdIL8C29AMGIhMd6Abpv6wWzj3dqBnikGdxPMzxqBlXd/fSru+9+ncKrfw== \ No newline at end of file diff --git a/docs/citibike.png b/docs/citibike.png deleted file mode 100644 index 807c2c65b70767df0101aa3d259c9fd1209a4997..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 39247 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

ds`41L}W4jWM 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 [--] [options ...]" - Write-Host "Commands:" - Write-Host " linting Static analysis, code style, etc. (please install poetry if you would like to use this command)" - Write-Host " precommit Run sensible checks before committing" - Write-Host " install-with-docker-desktop Install the application requirements along with docker desktop" - Write-Host " run-local-unit-test Run unit tests on local machine" - Write-Host " run-docker-desktop-unit-test Run unit tests on containers using Docker Desktop" - Write-Host " run-local-integration-test Run integration tests on local machine" - Write-Host " run-docker-desktop-integration-test Run integration tests on containers using Docker Desktop" - Write-Host " run-local-job Run job on local machine" - Write-Host " run-docker-desktop-job Run job on containers using Docker Desktop" -} - -switch ($action) -{ - linting { - scripts/win/linting.ps1 - Break - } - precommit { - Write-Host "Precommit Checks" - Break - } - install-with-docker-desktop { - scripts/win/install_with_docker_desktop.ps1 - Break - } - run-local-unit-test { - Write-Host "Running unit tests on local machine" - poetry run pytest tests/unit - Break - } - run-docker-desktop-unit-test { - Write-Host "Running unit tests on containers using Docker Desktop" - ./batect unit-test - Break - } - run-local-integration-test { - Write-Host "Running integration tests on local machine" - scripts/win/run-local-integration-test.ps1 - Break - } - run-docker-desktop-integration-test { - Write-Host "Running integration tests on containers using Docker Desktop" - scripts/win/run-docker-desktop-integration-test.ps1 - Break - } - run-local-job { - Write-Host "Running job on local machine" - ./scripts/win/run-job.ps1 - Break - } - run-docker-desktop-job { - "Running job on containers using Docker Desktop" - ./scripts/win/run-docker-desktop-job.ps1 - Break - } - usage { - Get-Usage - Break - } - default { - Get-Usage - Break - } -} \ No newline at end of file diff --git a/go.sh b/go.sh deleted file mode 100755 index c269556..0000000 --- a/go.sh +++ /dev/null @@ -1,268 +0,0 @@ -#!/bin/bash - -set -euo pipefail - - -function trace() { - { - local tracing - [[ "$-" = *"x"* ]] && tracing=true || tracing=false - set +x - } 2>/dev/null - if [ "$tracing" != true ]; then - # Bash's own trace mode is off, so explicitely write the message. - echo "$@" >&2 - else - # Restore trace - set -x - fi -} - - -function contains () { - local e match="$1" - shift - for e; do [[ "$e" == "$match" ]] && return 0; done - return 1 -} - - -# Parse arguments. -operations=() -subcommand_opts=() -while true; do - case "${1:-}" in - lint|linting) - operations+=( linting ) - shift - ;; - precommit) - operations+=( precommit ) - shift - ;; - install-with-docker-desktop) - operations+=( install-with-docker-desktop ) - shift - ;; - install-with-colima) - operations+=( install-with-colima ) - shift - ;; - start-colima) - operations+=( start-colima ) - shift - ;; - run-local-unit-test) - operations+=( run-local-unit-test ) - shift - ;; - run-docker-desktop-unit-test) - operations+=( run-docker-desktop-unit-test ) - shift - ;; - run-colima-unit-test) - operations+=( run-colima-unit-test ) - shift - ;; - run-local-integration-test) - operations+=( run-local-integration-test ) - shift - ;; - run-docker-desktop-integration-test) - operations+=( run-docker-desktop-integration-test ) - shift - ;; - run-colima-integration-test) - operations+=( run-colima-integration-test ) - shift - ;; - run-local-job) - operations+=( run-local-job ) - shift - ;; - run-docker-desktop-job) - operations+=( run-docker-desktop-job ) - shift - ;; - run-colima-job) - operations+=( run-colima-job ) - shift - ;; - --) - shift - break - ;; - -h|--help) - operations+=( usage ) - shift - ;; - *) - break - ;; - esac -done -if [ "${#operations[@]}" -eq 0 ]; then - operations=( usage ) -fi -if [ "$#" -gt 0 ]; then - subcommand_opts=( "$@" ) -fi - - -function usage() { - trace "$0 [--] [options ...]" - trace "Commands:" - trace " linting Static analysis, code style, etc.(please install poetry if you would like to use this command)" - trace " precommit Run sensible checks before committing" - trace " install-with-docker-desktop Install the application requirements along with docker desktop" - trace " install-with-colima Install the application requirements along with colima" - trace " start-colima Start Colima" - trace " run-local-unit-test Run unit tests on local machine" - trace " run-colima-unit-test Run unit tests on containers using Colima" - trace " run-docker-desktop-unit-test Run unit tests on containers using Docker Desktop" - trace " run-local-integration-test Run integration tests on local machine" - trace " run-colima-integration-test Run integration tests on containers using Colima" - trace " run-docker-desktop-integration-test Run integration tests on containers using Docker Desktop" - trace " run-local-job Run job on local machine" - trace " run-colima-job Run job on containers using Colima" - trace " run-docker-desktop-job Run job on containers using Docker Desktop" - trace "Options are passed through to the sub-command." -} - - -function linting() { - trace "Linting" - ./scripts/mac_or_linux/linting.sh "${subcommand_opts[@]:+${subcommand_opts[@]}}" -} - - -function precommit() { - trace "Precommit Checks" -} - - -function install-with-docker-desktop() { - trace "Install the application requirements along with docker desktop" - ./scripts/mac_or_linux/install-with-docker-desktop.sh "${subcommand_opts[@]:+${subcommand_opts[@]}}" -} - - -function install-with-colima() { - trace "Install the application requirements along with docker desktop" - ./scripts/mac_or_linux/install-with-colima.sh "${subcommand_opts[@]:+${subcommand_opts[@]}}" -} - - -function start-colima() { - trace "Starting Colima" - ./scripts/mac_or_linux/start-colima.sh "${subcommand_opts[@]:+${subcommand_opts[@]}}" -} - - -function run-local-unit-test() { - trace "Running unit tests on local machine" - ./scripts/mac_or_linux/unit-test.sh "${subcommand_opts[@]:+${subcommand_opts[@]}}" -} - - -function run-colima-unit-test() { - trace "Running unit tests on containers using Colima" - ./scripts/mac_or_linux/run-colima-unit-test.sh "${subcommand_opts[@]:+${subcommand_opts[@]}}" -} - - -function run-docker-desktop-unit-test() { - trace "Running unit tests on containers using Docker Desktop" - ./scripts/mac_or_linux/run-docker-desktop-unit-test.sh "${subcommand_opts[@]:+${subcommand_opts[@]}}" -} - - -function run-local-integration-test() { - trace "Running integration tests on local machine" - ./scripts/mac_or_linux/integration-test.sh "${subcommand_opts[@]:+${subcommand_opts[@]}}" -} - - -function run-colima-integration-test() { - trace "Running integration tests on containers using Colima" - ./scripts/mac_or_linux/run-colima-integration-test.sh "${subcommand_opts[@]:+${subcommand_opts[@]}}" -} - - -function run-docker-desktop-integration-test() { - trace "Running integration tests on containers using Docker Desktop" - ./scripts/mac_or_linux/run-docker-desktop-integration-test.sh "${subcommand_opts[@]:+${subcommand_opts[@]}}" -} - - -function run-local-job() { - trace "Running job on local machine" - ./scripts/mac_or_linux/run-job.sh "${subcommand_opts[@]:+${subcommand_opts[@]}}" -} - - -function run-colima-job() { - trace "Running job on containers using Colima" - ./scripts/mac_or_linux/run-colima-job.sh "${subcommand_opts[@]:+${subcommand_opts[@]}}" -} - - -function run-docker-desktop-job() { - trace "Running job on containers using Docker Desktop" - ./scripts/mac_or_linux/run-docker-desktop-job.sh "${subcommand_opts[@]:+${subcommand_opts[@]}}" -} - - -script_directory="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" -cd "${script_directory}/" - - -if contains usage "${operations[@]}"; then - usage - exit 1 -fi -if contains linting "${operations[@]}"; then - linting -fi -if contains precommit "${operations[@]}"; then - precommit -fi -if contains install-with-docker-desktop "${operations[@]}"; then - install-with-docker-desktop -fi -if contains install-with-colima "${operations[@]}"; then - install-with-colima -fi -if contains start-colima "${operations[@]}"; then - start-colima -fi -if contains run-local-unit-test "${operations[@]}"; then - run-local-unit-test -fi -if contains run-colima-unit-test "${operations[@]}"; then - run-colima-unit-test -fi -if contains run-docker-desktop-unit-test "${operations[@]}"; then - run-docker-desktop-unit-test -fi -if contains run-local-integration-test "${operations[@]}"; then - run-local-integration-test -fi -if contains run-colima-integration-test "${operations[@]}"; then - run-colima-integration-test -fi -if contains run-docker-desktop-integration-test "${operations[@]}"; then - run-docker-desktop-integration-test -fi -if contains run-local-job "${operations[@]}"; then - run-local-job -fi -if contains run-colima-job "${operations[@]}"; then - run-colima-job -fi -if contains run-docker-desktop-job "${operations[@]}"; then - run-docker-desktop-job -fi - - -trace "Exited cleanly." \ No newline at end of file diff --git a/scripts/install.bat b/scripts/install.bat deleted file mode 100644 index 57f4555..0000000 --- a/scripts/install.bat +++ /dev/null @@ -1,4 +0,0 @@ -where java -IF %ERRORLEVEL% EQU 0 (ECHO "JAVA IS INSTALLED") ELSE (choco install adoptopenjdk11) -where docker -IF %ERRORLEVEL% EQU 0 (ECHO "DOCKER IS INSTALLED") ELSE (choco install docker-desktop) diff --git a/scripts/install_choco.ps1 b/scripts/install_choco.ps1 deleted file mode 100644 index a42202c..0000000 --- a/scripts/install_choco.ps1 +++ /dev/null @@ -1,2 +0,0 @@ -Set-ExecutionPolicy AllSigned -Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) diff --git a/scripts/mac_or_linux/install-with-colima.sh b/scripts/mac_or_linux/install-with-colima.sh deleted file mode 100755 index d8c2b9a..0000000 --- a/scripts/mac_or_linux/install-with-colima.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/sh - -set -euo pipefail - -# batect dependencies -echo "Installing homebrew if it's not installed..." -which brew || /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)" - -echo "Installing Docker if it's not installed..." -which docker || brew install docker - -echo "Installing Colima if it's not installed..." -which colima || brew install colima - -echo "Installing java if it's not installed..." -which java -if [ $? -ne 0 ]; then - brew tap adoptopenjdk/openjdk - brew cask install adoptopenjdk11 -fi diff --git a/scripts/mac_or_linux/install-with-docker-desktop.sh b/scripts/mac_or_linux/install-with-docker-desktop.sh deleted file mode 100755 index 23ec747..0000000 --- a/scripts/mac_or_linux/install-with-docker-desktop.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/sh - -set -euo pipefail - -# batect dependencies -echo "Installing homebrew if it's not installed..." -which brew || /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)" - -echo "Installing docker desktop if it's not installed..." -which docker || brew install --cask docker - -echo "Installing java if it's not installed..." -which java -if [ $? -ne 0 ]; then - brew tap adoptopenjdk/openjdk - brew cask install adoptopenjdk11 -fi diff --git a/scripts/mac_or_linux/integration-test.sh b/scripts/mac_or_linux/integration-test.sh deleted file mode 100755 index b9e3184..0000000 --- a/scripts/mac_or_linux/integration-test.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -poetry run pytest tests/integration diff --git a/scripts/mac_or_linux/linting.sh b/scripts/mac_or_linux/linting.sh deleted file mode 100755 index b7b8de4..0000000 --- a/scripts/mac_or_linux/linting.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -echo "Running type checks" -poetry run mypy --ignore-missing-imports --disallow-untyped-calls --disallow-untyped-defs --disallow-incomplete-defs \ - data_transformations tests - -echo "Running lint checks" -poetry run pylint data_transformations tests diff --git a/scripts/mac_or_linux/run-colima-integration-test.sh b/scripts/mac_or_linux/run-colima-integration-test.sh deleted file mode 100755 index 3c327ae..0000000 --- a/scripts/mac_or_linux/run-colima-integration-test.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh - -set -euo pipefail - -./batect --docker-host=unix://$HOME/.colima/docker.sock integration-test diff --git a/scripts/mac_or_linux/run-colima-job.sh b/scripts/mac_or_linux/run-colima-job.sh deleted file mode 100755 index 8907689..0000000 --- a/scripts/mac_or_linux/run-colima-job.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh - -set -euo pipefail - -./batect --docker-host=unix://$HOME/.colima/docker.sock run-job diff --git a/scripts/mac_or_linux/run-colima-unit-test.sh b/scripts/mac_or_linux/run-colima-unit-test.sh deleted file mode 100755 index 4095b9d..0000000 --- a/scripts/mac_or_linux/run-colima-unit-test.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh - -set -euo pipefail - -./batect --docker-host=unix://$HOME/.colima/docker.sock unit-test diff --git a/scripts/mac_or_linux/run-docker-desktop-integration-test.sh b/scripts/mac_or_linux/run-docker-desktop-integration-test.sh deleted file mode 100755 index 6261c19..0000000 --- a/scripts/mac_or_linux/run-docker-desktop-integration-test.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh - -set -euo pipefail - -./batect integration-test diff --git a/scripts/mac_or_linux/run-docker-desktop-job.sh b/scripts/mac_or_linux/run-docker-desktop-job.sh deleted file mode 100755 index 996082f..0000000 --- a/scripts/mac_or_linux/run-docker-desktop-job.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh - -set -euo pipefail - -./batect run-job diff --git a/scripts/mac_or_linux/run-docker-desktop-unit-test.sh b/scripts/mac_or_linux/run-docker-desktop-unit-test.sh deleted file mode 100755 index f8878c8..0000000 --- a/scripts/mac_or_linux/run-docker-desktop-unit-test.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh - -set -euo pipefail - -./batect unit-test diff --git a/scripts/mac_or_linux/run-job.sh b/scripts/mac_or_linux/run-job.sh deleted file mode 100755 index f61ea37..0000000 --- a/scripts/mac_or_linux/run-job.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -poetry build - -jobName=$(echo "${JOB}" | awk '{ print tolower($1) }') - -if [[ "${jobName}" == "citibike_ingest" ]]; then - INPUT_FILE_PATH="./resources/citibike/citibike.csv" - JOB_ENTRY_POINT="jobs/citibike_ingest.py" - OUTPUT_PATH="./output_int" -elif [[ "${jobName}" == "citibike_distance_calculation" ]]; then - INPUT_FILE_PATH="./output_int" - JOB_ENTRY_POINT="jobs/citibike_distance_calculation.py" - OUTPUT_PATH="./output" -elif [[ "${jobName}" == "wordcount" ]]; then - INPUT_FILE_PATH="./resources/word_count/words.txt" - JOB_ENTRY_POINT="jobs/word_count.py" - OUTPUT_PATH="./output" -else - echo "Job name provided was : ${JOB} : failed" - echo "Job name deduced was : ${jobName} : failed" - echo "Please enter a valid job name (citibike_ingest, citibike_distance_calculation or wordcount)" - exit 1 -fi - -rm -rf $OUTPUT_PATH - - - -poetry run spark-submit \ - --master local \ - --py-files dist/data_transformations-*.whl \ - $JOB_ENTRY_POINT \ - $INPUT_FILE_PATH \ - $OUTPUT_PATH - diff --git a/scripts/mac_or_linux/start-colima.sh b/scripts/mac_or_linux/start-colima.sh deleted file mode 100755 index bac1283..0000000 --- a/scripts/mac_or_linux/start-colima.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh - -set -euo pipefail - -echo "Starting Colima" -colima start diff --git a/scripts/mac_or_linux/unit-test.sh b/scripts/mac_or_linux/unit-test.sh deleted file mode 100755 index 2164b32..0000000 --- a/scripts/mac_or_linux/unit-test.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -poetry run pytest tests/unit diff --git a/scripts/win/install_with_docker_desktop.ps1 b/scripts/win/install_with_docker_desktop.ps1 deleted file mode 100644 index f88176a..0000000 --- a/scripts/win/install_with_docker_desktop.ps1 +++ /dev/null @@ -1,23 +0,0 @@ -# WIP -Write-host "Please install java and docker desktop manually or use scripts/install.bat as an example. " -# ECHO "Checking if java is installed" -# -# "where java" | cmd -# Write-Host $LASTEXITCODE -# Write-Host 1 -# try{ -# -# Write-Host "JAVA IS INSTALLED" -# } -# catch{ -# choco install adoptopenjdk11 -# } -# -# -# where docker -# IF ($LASTEXITCODE -eq 0) { -# ECHO "DOCKER IS INSTALLED" -# } -# ELSE { -# choco install docker-desktop -# } diff --git a/scripts/win/linting.ps1 b/scripts/win/linting.ps1 deleted file mode 100755 index 6bce2b0..0000000 --- a/scripts/win/linting.ps1 +++ /dev/null @@ -1,5 +0,0 @@ -Write-Host "Running type checks" -poetry run mypy --ignore-missing-imports --disallow-untyped-calls --disallow-untyped-defs --disallow-incomplete-defs data_transformations tests - -Write-Host "Running lint checks" -poetry run pylint data_transformations tests diff --git a/scripts/win/run-docker-desktop-integration-test.ps1 b/scripts/win/run-docker-desktop-integration-test.ps1 deleted file mode 100755 index 8f3c5a1..0000000 --- a/scripts/win/run-docker-desktop-integration-test.ps1 +++ /dev/null @@ -1 +0,0 @@ -./batect integration-test diff --git a/scripts/win/run-docker-desktop-job.ps1 b/scripts/win/run-docker-desktop-job.ps1 deleted file mode 100755 index 9249a25..0000000 --- a/scripts/win/run-docker-desktop-job.ps1 +++ /dev/null @@ -1 +0,0 @@ -./batect run-job diff --git a/scripts/win/run-job.ps1 b/scripts/win/run-job.ps1 deleted file mode 100755 index b15ee73..0000000 --- a/scripts/win/run-job.ps1 +++ /dev/null @@ -1,41 +0,0 @@ -poetry build - -$JOB=[System.Environment]::GetEnvironmentVariable('JOB') -$jobName=$JOB.ToLower() - -switch($jobName) -{ - - citibike_ingest { - $INPUT_FILE_PATH="resources/citibike/citibike.csv" - $JOB_ENTRY_POINT="jobs/citibike_ingest.py" - $OUTPUT_PATH="./output_int" - Break - } - citibike_distance_calculation { - $INPUT_FILE_PATH="./output_int" - $JOB_ENTRY_POINT="jobs/citibike_distance_calculation.py" - $OUTPUT_PATH="./output" - Break - } - wordcount { - $INPUT_FILE_PATH="resources/word_count/words.txt" - $JOB_ENTRY_POINT="jobs/word_count.py" - $OUTPUT_PATH="./output" - Break - } - default { - Write-Host "Job name provided was : ${JOB} : failed" - Write-Host "Job name deduced was : ${jobName} : failed" - Write-Host "Please enter a valid job name (citibike_ingest, citibike_distance_calculation or wordcount)" - exit 1 - Break - } - - -} - - -rm -rf $OUTPUT_PATH - -poetry run spark-submit --master local --py-files dist/data_transformations-*.whl $JOB_ENTRY_POINT $INPUT_FILE_PATH $OUTPUT_PATH diff --git a/scripts/win/run-local-integration-test.ps1 b/scripts/win/run-local-integration-test.ps1 deleted file mode 100755 index c910832..0000000 --- a/scripts/win/run-local-integration-test.ps1 +++ /dev/null @@ -1 +0,0 @@ -poetry run pytest tests/integration diff --git a/scripts/win/run-local-unit-test.ps1 b/scripts/win/run-local-unit-test.ps1 deleted file mode 100755 index 5e2c6ac..0000000 --- a/scripts/win/run-local-unit-test.ps1 +++ /dev/null @@ -1 +0,0 @@ -poetry run pytest tests/unit diff --git a/scripts/win/write-test.ps1 b/scripts/win/write-test.ps1 deleted file mode 100644 index d230747..0000000 --- a/scripts/win/write-test.ps1 +++ /dev/null @@ -1 +0,0 @@ -Write-host $args[0] \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 From e28ddb4b765c608cd5a570c65ee6beb91038d261 Mon Sep 17 00:00:00 2001 From: Lauris Jullien Date: Mon, 17 Jun 2024 17:57:44 +0200 Subject: [PATCH 2/7] minor fixes| --- .github/workflows/local-setup-test.yaml | 1 - pyproject.toml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/local-setup-test.yaml b/.github/workflows/local-setup-test.yaml index 09e165c..23400a4 100644 --- a/.github/workflows/local-setup-test.yaml +++ b/.github/workflows/local-setup-test.yaml @@ -65,7 +65,6 @@ jobs: winutils.exe chmod 777 D:\a\dataengineer-transformations-python\dataengineer-transformations-python - name: Install Python Dependencies run: | - scripts\install.bat poetry install - name: Run local unit tests run: | diff --git a/pyproject.toml b/pyproject.toml index d005ffa..e96c428 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ numpy = "^1.20.2" pandas = "^2.0.0" pyarrow = "^12.0.0" # This is pinned to stay in sync with the version -# That gets downloaded in `Dockerfile` +# That gets downloaded in `.gitpod.Dockerfile` pyspark = "3.5.1" python = "~3.11" From bedd5979c257f1d4f1b7e55fb5752a1689f92365 Mon Sep 17 00:00:00 2001 From: Lauris Jullien <122110592+lauris-tw@users.noreply.github.com> Date: Tue, 18 Jun 2024 14:01:09 +0200 Subject: [PATCH 3/7] Update README.md Co-authored-by: Javi Molina --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0d05eb5..6e38295 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ TODO : Add a link to a spark Tutorial ### Local Setup -> 💡 If you don't manage to run the local setup, use the [gitpod](#gitpod-setup) one +> 💡 If you don't manage to run the local setup or you have restrictions to install software in your laptop, use the [gitpod](#gitpod-setup) one #### Pre-requisites From 7176f2ecd3e9dd80b57dec4faf0092421b70d251 Mon Sep 17 00:00:00 2001 From: Lauris Jullien <122110592+lauris-tw@users.noreply.github.com> Date: Tue, 18 Jun 2024 14:01:28 +0200 Subject: [PATCH 4/7] Update README.md Co-authored-by: Javi Molina --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 6e38295..cdb4211 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,7 @@ TODO : Add a link to a spark Tutorial Please make sure you have the following installed and can run them -- Python (3.11.X), - you can use for example [pyenv](https://github.com/pyenv/pyenv#installation) to manage your python versions locally +- Python (3.11.X), you can use for example [pyenv](https://github.com/pyenv/pyenv#installation) to manage your python versions locally - [Poetry](https://python-poetry.org/docs/#installation) - Java (11), you can use [sdkman](https://sdkman.io/) to install and manage java locally From c130c11cd7e6ba8006c3cee1a0fe904c164c3677 Mon Sep 17 00:00:00 2001 From: Lauris Jullien Date: Tue, 18 Jun 2024 14:26:04 +0200 Subject: [PATCH 5/7] Introduce atif's suggestions from #54 --- README.md | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index cdb4211..26a1f78 100644 --- a/README.md +++ b/README.md @@ -3,22 +3,19 @@ This coding challenge is a collection of _Python_ jobs that are supposed to extract, transform and load data. These jobs are using _PySpark_ to process larger volumes of data and are supposed to run on a _Spark_ cluster (via `spark-submit`). -## Before the interview +## Gearing Up for the Pairing Session **✅ Goals** -- **Get a working environment** - Either local ([local](#local-setup), or using gitpod) -- **Get an overview of the codebase and technologies involved** +1. **Get a working environment** + Either local ([local](#local-setup), or using [gitpod](#gitpod-setup)) +2. **Get a high-level understanding of the code and test dataset structure** +3. Have your preferred text editor or IDE setup and ready to go. **❌ Non-Goals** - solving the exercises / writing code - > The exercises will be given at the time of interview, and solved by pairing with the interviewer. - - + > ⚠️ The exercises will be given at the time of interview, and solved by pairing with the interviewer. ### Local Setup @@ -60,7 +57,7 @@ Remember to stop the vm and restart it just before the interview. ### Verify setup -The following commands should be running successfully +> All of the following commands should be running successfully #### Run unit tests @@ -85,6 +82,7 @@ poetry run pylint data_transformations tests ### Anything else? +All commands are passing? You are good to go! > ⚠️ do not try to solve the exercises ahead of the interview @@ -95,7 +93,7 @@ You are allowed to customize your environment (having the test in vscode directl There are two exercises in this repo: Word Count, and Citibike. -Currently, these exist as skeletons, and have some initial test cases which are defined but ignored. +Currently, these exist as skeletons, and have some **initial test cases** which are defined but some are skipped. The following section provides context over them. @@ -270,3 +268,10 @@ poetry build && poetry run spark-submit \ > ⚠️ do not try to solve the exercises ahead of the interview --- + +## Reading List + +If you are unfamiliar with some of the tools used here, we recommend some resources to get started + +- **pytest**: [official](https://docs.pytest.org/en/8.2.x/getting-started.html#get-started) +- **pyspark**: [official](https://spark.apache.org/docs/latest/api/python/index.html) and especially the [DataFrame quickstart](https://spark.apache.org/docs/latest/api/python/getting_started/quickstart_df.html) From 2928c8e7eac474b157b900ffbb7c2326ce7ebef1 Mon Sep 17 00:00:00 2001 From: Lauris Jullien Date: Tue, 18 Jun 2024 15:37:29 +0200 Subject: [PATCH 6/7] Simplify gitpod setup --- .gitpod.Dockerfile | 10 +++------- .gitpod.yml | 6 ++---- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/.gitpod.Dockerfile b/.gitpod.Dockerfile index f9cf755..863de24 100644 --- a/.gitpod.Dockerfile +++ b/.gitpod.Dockerfile @@ -5,13 +5,9 @@ USER root WORKDIR /opt RUN if [ "$(arch)" = "aarch64" ] ; then ARCHITECTURE="aarch64" ; else ARCHITECTURE="x64"; fi && \ wget -O OpenJDK.tar.gz https://github.com/AdoptOpenJDK/openjdk11-binaries/releases/download/jdk-11.0.11%2B9/OpenJDK11U-jdk_${ARCHITECTURE}_linux_hotspot_11.0.11_9.tar.gz && \ - wget -O scala.tgz https://downloads.lightbend.com/scala/2.13.5/scala-2.13.5.tgz && \ - wget -O spark-hadoop.tgz https://archive.apache.org/dist/spark/spark-3.5.1/spark-3.5.1-bin-hadoop3.tgz RUN tar xzf OpenJDK.tar.gz && \ - tar xvf scala.tgz && \ - tar xvf spark-hadoop.tgz -ENV PATH="/opt/jdk-11.0.11+9/bin:/opt/scala-2.13.5/bin:/opt/spark-3.5.1-bin-hadoop3/bin:$PATH" - +ENV JAVA_HOME="/opt/jdk-11.0.11+9" \ + PATH="/opt/jdk-11.0.11+9/bin:$PATH" #TODO : Change the user to non root user #USER 185 @@ -19,4 +15,4 @@ WORKDIR /app COPY ./pyproject.toml /app/pyproject.toml -RUN pyenv install 3.11.4 && pyenv global 3.11.4 \ No newline at end of file +RUN pyenv install 3.11.4 && pyenv global 3.11.4 && poetry env use "${HOME}/.pyenv/versions/3.11.4/bin/python3" \ No newline at end of file diff --git a/.gitpod.yml b/.gitpod.yml index 0744f2b..aab9f1f 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -7,9 +7,7 @@ image: tasks: - init: | - pyenv install 3.11.4 - pyenv shell 3.11.4 - poetry env use "${HOME}/.pyenv/versions/3.11.4/bin/python3" poetry install poetry env info - make tests + poetry run python -m pytest test/unit + poetry run python -m pytest tests/integration From 65a81aed8ca324cbc05f0cfe98a8c53cd85e7ee1 Mon Sep 17 00:00:00 2001 From: Lauris Jullien Date: Tue, 18 Jun 2024 15:44:18 +0200 Subject: [PATCH 7/7] Fix tests --- tests/integration/test_distance_transformer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/test_distance_transformer.py b/tests/integration/test_distance_transformer.py index e812618..5de4572 100644 --- a/tests/integration/test_distance_transformer.py +++ b/tests/integration/test_distance_transformer.py @@ -98,7 +98,7 @@ def test_should_maintain_all_data_it_reads(SPARK) -> None: @pytest.mark.skip def test_should_add_distance_column_with_calculated_distance(SPARK) -> None: - given_ingest_folder, given_transform_folder = __create_ingest_and_transform_folders() + given_ingest_folder, given_transform_folder = __create_ingest_and_transform_folders(SPARK) distance_transformer.run(SPARK, given_ingest_folder, given_transform_folder) actual_dataframe = SPARK.read.parquet(given_transform_folder)