From 41b3e9e86a492d15784137307c5fade4092764e0 Mon Sep 17 00:00:00 2001 From: Greg Lu Date: Fri, 26 Oct 2018 18:01:12 -0400 Subject: [PATCH] Implemented new project structure and release process --- .decrypt-keys.sh | 5 + .gitignore | 21 ++ .travis.yml | 62 ++++++ README.md | 10 +- VERSION | 1 + .../com/swoop/alchemy/test/SparkHelpers.scala | 15 ++ .../swoop/alchemy/utils/AnyExtensions.scala | 185 ++++++++++++++++++ .../alchemy/utils/AnyExtensionsTest.scala | 27 +++ build.sbt | 78 ++++++++ dev/release-process.md | 20 ++ docs/main/resources/site/images/favicon.png | Bin 0 -> 1439 bytes .../resources/site/images/navbar_brand.png | Bin 0 -> 1589 bytes .../resources/site/images/navbar_brand2x.png | Bin 0 -> 2376 bytes .../resources/site/images/sidebar_brand.png | Bin 0 -> 3593 bytes .../resources/site/images/sidebar_brand2x.png | Bin 0 -> 3318 bytes .../site/images/swoop-icon_130x130.png | Bin 0 -> 3318 bytes .../site/images/swoop-icon_80x80.png | Bin 0 -> 2376 bytes docs/main/resources/site/scripts/automenu.js | 102 ++++++++++ docs/main/resources/site/styles/overrides.css | 17 ++ docs/main/tut/docs.md | 9 + docs/main/tut/index.md | 53 +++++ project/build.properties | 1 + project/plugins.sbt | 11 ++ travis-deploy-key.enc | Bin 0 -> 3248 bytes 24 files changed, 613 insertions(+), 4 deletions(-) create mode 100644 .decrypt-keys.sh create mode 100644 .travis.yml create mode 100644 VERSION create mode 100644 alchemy-test/src/main/scala/com/swoop/alchemy/test/SparkHelpers.scala create mode 100644 alchemy/src/main/scala/com/swoop/alchemy/utils/AnyExtensions.scala create mode 100644 alchemy/src/test/scala/com/swoop/alchemy/utils/AnyExtensionsTest.scala create mode 100644 build.sbt create mode 100644 dev/release-process.md create mode 100644 docs/main/resources/site/images/favicon.png create mode 100644 docs/main/resources/site/images/navbar_brand.png create mode 100644 docs/main/resources/site/images/navbar_brand2x.png create mode 100644 docs/main/resources/site/images/sidebar_brand.png create mode 100644 docs/main/resources/site/images/sidebar_brand2x.png create mode 100644 docs/main/resources/site/images/swoop-icon_130x130.png create mode 100644 docs/main/resources/site/images/swoop-icon_80x80.png create mode 100644 docs/main/resources/site/scripts/automenu.js create mode 100644 docs/main/resources/site/styles/overrides.css create mode 100644 docs/main/tut/docs.md create mode 100644 docs/main/tut/index.md create mode 100644 project/build.properties create mode 100644 project/plugins.sbt create mode 100644 travis-deploy-key.enc diff --git a/.decrypt-keys.sh b/.decrypt-keys.sh new file mode 100644 index 0000000..247ac8a --- /dev/null +++ b/.decrypt-keys.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +openssl aes-256-cbc -K $encrypted_9df70f2e42de_key -iv $encrypted_9df70f2e42de_iv -in travis-deploy-key.enc -out travis-deploy-key -d +chmod 600 travis-deploy-key; +cp travis-deploy-key ~/.ssh/id_rsa; diff --git a/.gitignore b/.gitignore index 9c07d4ae..55fa90a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,23 @@ *.class *.log + +# sbt specific +.cache +.history +.lib/ +dist/* +target/ +lib_managed/ +src_managed/ +project/boot/ +project/plugins/project/ + +# Scala-IDE specific +.scala_dependencies +.worksheet + +#Markdown editing +.Ulysses-favorites.plist + +metastore_db/ +tmp/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..9b3a27f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,62 @@ +language: scala + +sudo: false + +scala: 2.11.8 + +addons: + apt: + packages: + - curl + +branches: + except: + - gh-pages + - /^v[0-9]/ + +cache: + directories: + - $HOME/.ivy2/cache + - $HOME/.sbt + +env: + global: + - secure: "dhDQWvkfLqVrcAOH1u8PRxTLM0B+DteVDe1X0zfMWE3Rfmoa7ZNsegvPDePWTyAEEnCDK+27buOSurWxt1wcOtu+7zwnPB5ZB5JPIIJItAtlipWY0o+tiZ046cysn9p9BY3eK5Up9LqaXkPCnvjF19tbHKVKTmTSrd/NGwin/FCdXoLebnG8YU5/+B6hAal92I5gVtKGsbvo6/q4V/8pOkZKjtIUPQg9wHANbefu5eR83CKINkOShUILtSKqzbF79LGttLN4bOCUr+/P54xpJ4GCfnGc6UEBZcQ3HKYjk8KLDnMr0r3oy8yr/RdJbq9MttCxJdbHjlu1Jak068DzUBZIH5QMTLJZdx61aGTmEuNVvItVXFVWOjIUz6vVgLAaKgA5BWe8fIMl5DSbCCs2rUAiaGwq8zuhzDsOeu1wrCKOqUxYrzzq8WFLiGDtD9RmomdXWqVqbw39/Jk4dRPSRJrtsIGGGxCfLV/gPbeAdNDwS+GWSMBEBQe+2f7cSpZcp7vCJ5j5bnTA/0HEqD1tMp1/C1cQ9I3X8HRwc5ivOW1CCOqJlOxBvmns4rjWBIEcRBVN9HbAR3nQQUwJ97q5T45tC9+L2MIlQjuKiYA6VIHweOnO25FMm2I+hAmWnWcHBt0s3IMx3S1yyZcMc7BJLRGsSBytjzb1gL+QhzdvtK0=" + - secure: "qddU9PPHqKQbMc/+pbX8h9Sy1hR2bjyvyY2isFOsemxi3XMhuPrV15yVju+SGTZklGhPITzbF/zgvVu5hvE5zcCKge/+ODVmcB+lQQNv1/2m2poP7CvT/Qvj1v4BvN6QXVF4/ZA+y/DEoCxV4h8r0uhw8slBj7PkFvGFALPDiaVwoqyiwExPhQJZX/O7UFipdTHCYk38mak3Q1JU5Vq9swBCrTZihWb1hmkyV+Gppa02loxaS/e5JS8UKc0I+kBbTCr8k+yzE9Pi4FqTO3HFYxuhegaDtrWS9gmtBZZXPViS1RtEemtzECUW0tG4xTuPw0EqaCM1Q9Qv4oRb1mtWLqdLytN1OaTW1ZqGlHBUuLZ3HBGsBQoe2NqOt4+g8+LfuDSkrtFGo7MzyikgAE797QkJtJXUNuqcfE5R1IIPpjxHkKlA5u77y2inSPxuA3iCGskbXy/YCWB8gZriKZKPrSXhpLZl9cpH70F4QTuBXDhkqPOVMwP6B7crGhFIzz1+CSWtxoJlERxZ7E2bYmWqySzw8BwvpuWk3ysrPEAAD5/gaWwohESlFgyAdRaexEqqQQG3/JagtxBInErLqcq3PUxp6U7mV63zgPjksmqQNgLaI3wBCMxdBJhl6Ejkaa+pBv+B40BPNczUSZW0vjtO7EDq1jSAhMLZhILQptFKZvE=" + - secure: "LrhD2u0lM+ZRJ6TvtP2BX3FlQbNf6iRGZ4uTGEGRkylIEAfk1BtRs2Kv9KhjVhlox630vzfSNZxf76yH252JMsqSQJI+3raJThF7tAfLDUK2tdIw8OdHpn940wI01ChpH+3uqKwBIYSLhyU8ryPSCHLeehQeayAvz8ClDgjMhw1OJLA62l26x9nJYEDyOetWtrlqG0N39Lg4Mb722X+jLMmYNOLZRVFb7CjLSaacQ4sRENA1PCQdA87qG/1/wnkJptF/TSXwbdfxr86qVv6n7gqJVqEDjeNABZ4GouX4c3SlJqAZqm26POBysvT7eO6okBOitsoIMM8WEO32tT8PVTRXY17NFnswCVJdarsZFCEv93f7KrRERTtCbHJRuV20LdLMCs9S7NVBnvudcYFT9tcq3dUprON4+9nMd4bGVdKZOuhwY2Xw9P3xur5//pKB9VKsa4Fx6kX7OYbA/dQcTEW2fuXUyQrVbcdfBuM/nkYeJYrya4wsrnzJzW7SqFszcohhJAjM3pklMxE8mlE+pK4N+th/0cxJoQfPyyCde9NEKKpjKZaQhbiuNNXsJCQjyuoLWDSGA/Yh/emJdYWQrjY+sLNRmCOQ4kjuNsVPnatuYUH7TQtX4V2HTmxgdApc0zsMpBGNk+inNvK6xIfTU8saiA5wwC4dZEdmnD7hZ2g=" + - SWOOP_VERSION_FILE=$TRAVIS_BUILD_DIR/VERSION + - SWOOP_RELEASE_BRANCH=release + - SWOOP_PROJECT_VERSION="$(grep -Po '\d+\.\d+' $SWOOP_VERSION_FILE).$TRAVIS_BUILD_NUMBER" + +before_install: + - > + if [ ${TRAVIS_PULL_REQUEST} = 'false' -a ${TRAVIS_BRANCH} = ${SWOOP_RELEASE_BRANCH} ]; then + bash ./.decrypt-keys.sh && + export PATH=${PATH}:./vendor/bundle && + gem update --system && + gem install sass && + gem install jekyll + fi + +# Set the version number with the Travis build number as the patch version +before_script: + - > + if [ ${TRAVIS_PULL_REQUEST} = 'false' -a ${TRAVIS_BRANCH} = ${SWOOP_RELEASE_BRANCH} ] + then echo "${SWOOP_PROJECT_VERSION}" | tee $SWOOP_VERSION_FILE + else echo "${SWOOP_PROJECT_VERSION}-SNAPSHOT" | tee $SWOOP_VERSION_FILE + fi + +after_success: + - echo "VERSION NUMBER = $(cat $SWOOP_VERSION_FILE)" + - > + [ ${TRAVIS_PULL_REQUEST} = 'false' -a ${TRAVIS_BRANCH} = ${SWOOP_RELEASE_BRANCH} ] && + echo "RELEASING: $(cat $SWOOP_VERSION_FILE)" && + sbt publish && + sbt publishMicrosite && + curl -H "Accept: application/vnd.github.v3+json" -H "Authorization: token ${GITHUB_TOKEN}" -X POST "https://api.github.com/repos/${TRAVIS_REPO_SLUG}/releases" -d '{ + "tag_name": "v'"${SWOOP_PROJECT_VERSION}"'", + "target_commitish": "'"${TRAVIS_COMMIT}"'", + "name": "v'"${SWOOP_PROJECT_VERSION}"'", + "body": "https://travis-ci.org/'"${TRAVIS_REPO_SLUG}"'/builds/'"${TRAVIS_BUILD_ID}"'", + "prerelease": false + }' diff --git a/README.md b/README.md index ba43053..02525e8 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,19 @@ # spark-alchemy -Spark Alchemy is a collection of open-source Spark tools & frameworks that have made the data engineering and +[![Download](https://api.bintray.com/packages/swoop-inc/maven/spark-alchemy/images/download.svg)](https://bintray.com/swoop-inc/maven/spark-alchemy/_latestVersion) + +Spark Alchemy is a collection of open-source Spark tools & frameworks that have made the data engineering and data science teams at [Swoop](https://www.swoop.com) highly productive in our demanding petabyte-scale environment with rich data (thousands of columns). -We are preparing to release `spark-alchemy`. Click Watch above to be notified when we do. +We are preparing to release `spark-alchemy`. Click Watch above to be notified when we do. Here is a preview of what we'd like to include here: -- Configuration Addressable Production (CAP), Automatic Lifecycle Management (ALM) and Just-in-time Dependency Resolution +- Configuration Addressable Production (CAP), Automatic Lifecycle Management (ALM) and Just-in-time Dependency Resolution (JDR) as outlined in our Spark+AI Summit talk [Unafraid of Change: Optimizing ETL, ML, and AI in Fast-Paced Environments](https://databricks.com/session/unafraid-of-change-optimizing-etl-ml-ai-in-fast-paced-environments). -- Our extensive set of [HyperLogLog](https://en.wikipedia.org/wiki/HyperLogLog) (HLL) functions that allow the saving of HLL sketches as binary columns for fast +- Our extensive set of [HyperLogLog](https://en.wikipedia.org/wiki/HyperLogLog) (HLL) functions that allow the saving of HLL sketches as binary columns for fast reaggregation as well as HLL interoperability with Postgres. (Spark has an HLL implementation but does not expose the binary HLL sketches, which makes its usefulness rather limited.) diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..4ecb664 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1.0-SNAPSHOT \ No newline at end of file diff --git a/alchemy-test/src/main/scala/com/swoop/alchemy/test/SparkHelpers.scala b/alchemy-test/src/main/scala/com/swoop/alchemy/test/SparkHelpers.scala new file mode 100644 index 0000000..e4dc034 --- /dev/null +++ b/alchemy-test/src/main/scala/com/swoop/alchemy/test/SparkHelpers.scala @@ -0,0 +1,15 @@ +package com.swoop.alchemy.test + +import org.apache.spark.sql.{Column, DataFrame} + +trait SparkHelpers { + + def stripExpressionIds(s: String): String = "#\\d+".r.unanchored.replaceAllIn(s, "") + + def simpleSql(column: Column): String = stripExpressionIds(column.expr.sql) + + def simplePlan(df: DataFrame): String = stripExpressionIds(df.queryExecution.logical.toString).stripLineEnd + +} + +object SparkHelpers extends SparkHelpers diff --git a/alchemy/src/main/scala/com/swoop/alchemy/utils/AnyExtensions.scala b/alchemy/src/main/scala/com/swoop/alchemy/utils/AnyExtensions.scala new file mode 100644 index 0000000..448d996 --- /dev/null +++ b/alchemy/src/main/scala/com/swoop/alchemy/utils/AnyExtensions.scala @@ -0,0 +1,185 @@ +package com.swoop.alchemy.utils + +/** + * Convenience methods for all types + */ +object AnyExtensions { + + /** Sugar for applying functions in a method chain. */ + implicit class TransformOps[A](val underlying: A) extends AnyVal { + + /** Applies a transformer function in a method chain. + * + * @param f function to apply + * @tparam B the return type + * @return the result of applying `f` on `underlying`. + */ + @inline def transform[B](f: A => B): B = + f(underlying) + + /** Conditionally applies a transformer function in a method chain. + * Use this instead of [[transformWhen()]] when the predicate requires the value of `underlying`. + * + * @param predicate predicate to evaluate to determine if the function should be applied + * @tparam B the return type of the function + * @return `underlying` if the predicate evaluates to `false` or the result of function application. + */ + @inline def transformIf[B <: A](predicate: A => Boolean)(f: A => B): A = + if (predicate(underlying)) + f(underlying) + else underlying + + /** Conditionally applies a transformer function in a method chain. + * Use this instead of [[transformIf()]] when the condition does not require the value of `underlying`. + * + * @param condition condition to evaluate to determine if the function should be applied + * @tparam B the return type of the function + * @return `underlying` if the expression evaluates to `false` or the result of function application. + */ + @inline def transformWhen[B <: A](condition: => Boolean)(f: A => B): A = + if (condition) + f(underlying) + else underlying + + } + + /** Sugar for creating side-effects in method chains. */ + implicit class TapOps[A](val underlying: A) extends AnyVal { + + /** Applies a function for its side-effect as part of a method chain. + * Inspired by Ruby's `Object#tap`. + * + * @param f side-effect function to call + * @tparam B the return type of the function; ignored + * @return `this` + */ + @inline def tap[B](f: A => B): A = { + f(underlying) + underlying + } + + /** Conditionally applies a function for its side-effect as part of a method chain. + * Use this instead of [[tapWhen()]] when the predicate requires the value of `underlying`. + * + * @param predicate predicate to evaluate to determine if the side-effect should be invoked + * @param f side-effect function to call + * @tparam B the return type of the function; ignored + * @return `this` + */ + @inline def tapIf[B](predicate: A => Boolean)(f: A => B): A = { + if (predicate(underlying)) + f(underlying) + underlying + } + + /** Conditionally applies a function for its side-effect as part of a method chain. + * Use this instead of [[tapIf()]] when the condition does not require the value of `underlying`. + * + * @param condition condition to evaluate to determine if the side-effect should be invoked + * @param f side-effect function to call + * @tparam B the return type of the function; ignored + * @return `this` + */ + @inline def tapWhen[B](condition: => Boolean)(f: A => B): A = { + if (condition) + f(underlying) + underlying + } + } + + /** Sugar for simple debugging/reporting by printing in a method chain. */ + implicit class PrintOps[A](val underlying: A) extends AnyVal { + + /** Taps and prints the object in a method chain. + * Shorthand for `.tap(println)`. + * + * @return `underlying` + */ + def tapp: A = + underlying.tap(println) + + /** Prints a value as a side effect. + * + * @param v the value to print + * @return `underlying` + */ + def print[B](v: B): A = + underlying.tap((_: A) => println(v)) + + /** Conditionally taps and prints the object in a method chain. + * Use this instead of [[printWhen()]] when the predicate requires the value of `underlying`. + * + * @param predicate predicate to evaluate to determine if the underlying value should be printed + * @return `this` + */ + def printIf(predicate: A => Boolean): A = + underlying.tapIf(predicate)(println) + + /** Conditionally prints a value as a side effect. + * Use this instead of [[printWhen()]] when the predicate requires the value of `underlying`. + * + * @param predicate predicate to evaluate to determine if the value should be printed + * @param v side-effect function to call + * @tparam B the value type; ignored + * @return `this` + */ + def printIf[B](predicate: A => Boolean, v: B): A = + underlying.tapIf(predicate)((_: A) => println(v)) + + /** Conditionally taps and prints the object in a method chain. + * Use this instead of [[printIf()]] when the condition does not require the value of `underlying`. + * + * @param condition condition to evaluate to determine if the underlying value should be printed + * @return `underlying` + */ + def printWhen(condition: => Boolean): A = + underlying.tapWhen(condition)(println) + + /** Conditionally prints a value as a side effect. + * Use this instead of [[printIf()]] when the condition does not require the value of `underlying`. + * + * @param condition condition to evaluate to determine if the value should be printed + * @tparam B the value type; ignored + * @return `underlying` + */ + def printWhen[B](condition: => Boolean, v: B): A = + underlying.tapWhen(condition)((_: A) => println(v)) + + } + + /** Sugar for conditionally raising exceptions as part of a method chain. */ + implicit class ThrowOps[A](val underlying: A) extends AnyVal { + + /** Raises an exception if a predicate is satisfied. + * Use this instead of [[throwWhen()]] when the predicate requires the value of `underlying`. + * + * @param predicate predicate to evaluate to determine if the exception should be thrown + * @param e expression that will return an exception + * @tparam B the exception type + * @return `underlying` if the predicate evaluates to `false`. + * @throws B + */ + def throwIf[B <: Throwable](predicate: A => Boolean)(e: => B): A = { + if (predicate(underlying)) + throw e + underlying + } + + /** Raises an exception if a condition is satisfied. + * Use this instead of [[throwIf()]] when the condition does not require the value of `underlying`. + * + * @param condition condition to evaluate to determine if the exception should be thrown + * @param e expression that will return an exception + * @tparam B the exception type + * @return `underlying` if the predicate evaluates to `false`. + * @throws B + */ + def throwWhen[B <: Throwable](condition: => Boolean, e: => B): A = { + if (condition) + throw e + underlying + } + + } + +} diff --git a/alchemy/src/test/scala/com/swoop/alchemy/utils/AnyExtensionsTest.scala b/alchemy/src/test/scala/com/swoop/alchemy/utils/AnyExtensionsTest.scala new file mode 100644 index 0000000..d56eb2b --- /dev/null +++ b/alchemy/src/test/scala/com/swoop/alchemy/utils/AnyExtensionsTest.scala @@ -0,0 +1,27 @@ +package com.swoop.alchemy.utils + +import org.scalatest.{BeforeAndAfterEach, FunSuite} + +class AnyExtensionsTest extends FunSuite with BeforeAndAfterEach { + + override def beforeEach() { + + } + + test("testTransformOps") { + + } + + test("testThrowOps") { + + } + + test("testTapOps") { + + } + + test("testPrintOps") { + + } + +} diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..29b0eec --- /dev/null +++ b/build.sbt @@ -0,0 +1,78 @@ +ThisBuild / organization := "com.swoop" +ThisBuild / version := scala.io.Source.fromFile("VERSION").mkString.stripLineEnd + +ThisBuild / scalaVersion := "2.11.8" +ThisBuild / crossScalaVersions := Seq("2.11.8") + +ThisBuild / javacOptions ++= Seq("-source", "1.8", "-target", "1.8") + +val scalaTest = "org.scalatest" %% "scalatest" % "3.0.5" + +val sparkVersion = "2.3.1" + +lazy val alchemy = (project in file(".")) + .aggregate(test) + .settings( + name := "spark-alchemy", + scalaSource in Compile := baseDirectory.value / "alchemy/src/main/scala", + scalaSource in Test := baseDirectory.value / "alchemy/src/test/scala", + resourceDirectory in Compile := baseDirectory.value / "alchemy/src/main/resources", + resourceDirectory in Test := baseDirectory.value / "alchemy/src/test/resources", + libraryDependencies ++= Seq( + scalaTest % Test withSources() + ) + ) + +lazy val test = (project in file("alchemy-test")) + .settings( + name := "spark-alchemy-test", + libraryDependencies ++= Seq( + "org.apache.spark" %% "spark-core" % sparkVersion % "provided" withSources(), + "org.apache.spark" %% "spark-sql" % sparkVersion % "provided" withSources() + excludeAll ExclusionRule(organization = "org.mortbay.jetty"), + "org.apache.spark" %% "spark-hive" % sparkVersion % "provided" withSources(), + scalaTest % Test withSources() + ) + ) + +enablePlugins(BuildInfoPlugin) +enablePlugins(GitVersioning, GitBranchPrompt) +enablePlugins(MicrositesPlugin) +enablePlugins(SiteScaladocPlugin) +enablePlugins(TutPlugin) + +// Speed up dependency resolution (experimental) +// @see https://www.scala-sbt.org/1.0/docs/Cached-Resolution.html +ThisBuild / updateOptions := updateOptions.value.withCachedResolution(true) + +// @see https://wiki.scala-lang.org/display/SW/Configuring+SBT+to+Generate+a+Scaladoc+Root+Page +scalacOptions in(Compile, doc) ++= Seq("-doc-root-content", baseDirectory.value + "/docs/root-doc.txt") +scalacOptions in(Compile, doc) ++= Seq("-groups", "-implicits") +javacOptions in(Compile, doc) ++= Seq("-notimestamp", "-linksource") +autoAPIMappings := true + +buildInfoPackage := "com.swoop.alchemy" + +tutSourceDirectory := baseDirectory.value / "docs" / "main" / "tut" +micrositeImgDirectory := baseDirectory.value / "docs" / "main" / "resources" / "site" / "images" +micrositeCssDirectory := baseDirectory.value / "docs" / "main" / "resources" / "site" / "styles" +micrositeJsDirectory := baseDirectory.value / "docs" / "main" / "resources" / "site" / "scripts" + +micrositeName := "Spark Alchemy" +micrositeDescription := "Useful extensions to Apache Spark" +micrositeAuthor := "Swoop" +micrositeHomepage := "https://www.swoop.com" +micrositeBaseUrl := "spark-alchemy" +micrositeDocumentationUrl := "/spark-alchemy/docs.html" +micrositeGithubOwner := "swoop-inc" +micrositeGithubRepo := "spark-alchemy" +micrositeHighlightTheme := "tomorrow" + +// SBT header settings +ThisBuild / organizationName := "Swoop, Inc" +ThisBuild / startYear := Some(2018) +ThisBuild / licenses += ("Apache-2.0", new URL("https://www.apache.org/licenses/LICENSE-2.0.txt")) + +// Bintray publishing +ThisBuild / bintrayOrganization := Some("swoop-inc") +ThisBuild / bintrayPackageLabels := Seq("apache", "spark", "apache-spark", "scala", "big-data", "spark-alchemy", "dataset", "swoop") diff --git a/dev/release-process.md b/dev/release-process.md new file mode 100644 index 0000000..6ccec62 --- /dev/null +++ b/dev/release-process.md @@ -0,0 +1,20 @@ +# Release Process + +1. Develop new code on feature branches. + +1. After development, testing, and code review, merge changes into the `master` branch. + +1. When ready to deploy a new release, merge and push changes from `master` to the `release` branch. Travis-CI will then: + + * Build the project + * Run tests + * Deploy artifacts to Bintray + * Publish the microsite to Github Pages + * Create a new release on the [Github Project Release Page](https://github.com/swoop-inc/spark-alchemy/releases) + +## Project Version Numbers + +* The `VERSION` file in the root of the project contains the version number that SBT will use for the `spark-alchemy` project. +* The format should follow [Semantic Versioning](https://semver.org/) with the patch number matching the Travis CI build number when deploying new releases. +* During deployment, Travis CI will read the MAJOR and MINOR version numbers from the `VERSION` file, but substitute the build number into the PATCH portion. In other words, if project developers wish to change the MAJOR or MINOR version numbers of the `spark-alchemy` project, they can simply change them in the `VERSION` file. +* During local development and when checked into Git, the version number defined in the `VERSION` file should end with the `-SNAPSHOT` string. diff --git a/docs/main/resources/site/images/favicon.png b/docs/main/resources/site/images/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..905aa690d3137a0ca8dc7d521454dc94b089cd4f GIT binary patch literal 1439 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyEa{HEjtmSN`?>!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+0817lTyPlzi}!T0HgqlnHS z@c;jRh927{VCdJB1o;I6V?(*(==*mEi{yE-uKf9ZK8{0T{?Bi{{Gu!WvA$lK8J@j7 z-t)r=M;5yS=M>gn(ox)J@G92wSrYTyx6K>=_N@QGtt!@fA%@*|^OIAlflRZGh)?=t z@rQQ#zy!(QU)>&pI+iH+5iaqg_R4-5=ULY^*;Ar_}|gI~tq3J^G>p;f`csMyA` zegRvE&8mvV?>DukR)t=v+LE>Od+pQy_jwH;n)d16oICgazS_x=o<;Iku2xG)>8?Kc zI-6|?JG<`M+)b(S_0V;?%LS-pGtcKx*P z1`l>A-1C2MU;(FtzsgKQ6CI%@WtOFGZyz>pESz^mT}W+SL|Db9KNbmEzCBAS4x9K+ z;0nz?*$=uSW{^ui}6>nW~ zc(v^*Jw4T!*ka2+zjzA{&)ss{I_T5diHm;h_GRpw|@N}!nl}!sR^?!Fu|ynxJHzuB$lLFB^RXvDF!10LvvjNBV7Zt z5Cc;yQ)4RwLu~^?D+7atjK?HVH00)|WTsW()^H)FZYfZMB*=!~{Irtt#G+IN$CUh} zR0Yr6#Prml)Wnp^!jq}MB8?%uDkP#LD6w3jpeR2rGbdG{q_QAYA+w+)nSr5V&f`x! z9ED*T8mIhEpYePe#K5e~t(VL#tSsz3S%g_w!KK0Ea0;{X<`9L`H?EvGa^{H45%$v! d9t*tm7+#4BmV9zDoeH#q!PC{xWt~$(69DJlkURhY literal 0 HcmV?d00001 diff --git a/docs/main/resources/site/images/navbar_brand.png b/docs/main/resources/site/images/navbar_brand.png new file mode 100644 index 0000000000000000000000000000000000000000..18bc128429851ec0b6991673b0bc8fccbd999ae3 GIT binary patch literal 1589 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 literal 0 HcmV?d00001 diff --git a/docs/main/resources/site/images/sidebar_brand.png b/docs/main/resources/site/images/sidebar_brand.png new file mode 100644 index 0000000000000000000000000000000000000000..d2a15d0b771a3067f12018bb5e01392485b1155b GIT binary patch literal 3593 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 literal 0 HcmV?d00001 diff --git a/docs/main/resources/site/images/sidebar_brand2x.png b/docs/main/resources/site/images/sidebar_brand2x.png new file mode 100644 index 0000000000000000000000000000000000000000..064ad1c615e74eaa089a2a0f3b3128fb1f4d1da1 GIT binary patch literal 3318 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 literal 0 HcmV?d00001 diff --git a/docs/main/resources/site/images/swoop-icon_130x130.png b/docs/main/resources/site/images/swoop-icon_130x130.png new file mode 100644 index 0000000000000000000000000000000000000000..064ad1c615e74eaa089a2a0f3b3128fb1f4d1da1 GIT binary patch literal 3318 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 literal 0 HcmV?d00001 diff --git a/docs/main/resources/site/images/swoop-icon_80x80.png b/docs/main/resources/site/images/swoop-icon_80x80.png new file mode 100644 index 0000000000000000000000000000000000000000..eeed4872f181557970ea08732f62b5ffacf0460a GIT binary patch literal 2376 zcmaJ@X;>3k7OeoH$RZ#hqG%DYktIomO%eza6P5sB52A<|k|G4mLM4!ZgAF1B%Bm=f z&|s^8in1y$G;NGPK=5_uO~Sk2(?P z@1?!oWIX@?+B9!!uyPGtdDp2czxjG|*Okj=u}8Q#L=YpEFcBCavjx#GNaHa%a4^hd zCnxm4t^lBF!3_-;htvItECCP8T*<*oc|s)`09@dmh* zR37hhwn!Wd|Ec$X5{p8Ug)lA{76}p&mU3`0<}0OyL<#~k#R4Q$AUM2wi-EBMu|O0n z5Q3BtM=TzsGg(~zO5`JgPAAg%A~BQCf@xF|TFHUsa@j-*fkN;gJMQtYBS3h(hZDhx zLLpO~oFF^M-gy_Tu|Ez338&`SwY<-xo^gE(7wpsgcS^%)-E{#eK zl|G$5;EOzU*EJ3Om95b0)*eyovRqdIgvY?ns38rg&UQzY?4gEjmFyRAE4l5kJ>{l; z+2p&F?~yxoHI0$cZUKUYT{D|cr1#ElZ#l74+M8q(@h?$P1>2?ft#!nQvBhUJ$x^>N zc}s7>&BHdsH71c(ko|DJ{-fWs))qc#wS4X8i;b{Nf3$mRTE}fnIwvA{I7z?z1qIl7 zKZJ-eTORi@kZ#e49-2R7;4IxR_p%c&^)Yx^HTFJ>Y**MiegAYlWrw+wQ3g;bJJNrn zzQ*H@izE^BtQwiucdVK)o0M6&zxFBX-Bh90mPum@v=_wHcxZXg88^QSmB3@SVxOYZ zX2bry4e;UuJMJnR8>A(ERR4clE{KJFn?rn|qI~iSDAo%Ot1qJ-EyV z9Y?An-fz3{7vGL>i0y^bIF%UyY?8H1AGegrebv>K-l}Y!HE2A#=8nx!rq)J>Vw^hq z$HQ^SQy&y>%>b#U>*Byzhd1NWx2n!4qccZJpa0Z{n%*)}<@y6|gQC4L_g_HyVr=6UblcKo$xe=D}}Gnffwk3BUp$IW<(&Nq)n zGv;STpBwfjVJsI3x_P$8t8H^ncw44ym4 zb=@wp#m5=H?KoOKeFTWCt-Wk1b+W#cpF3-zZGB%&8Xw50ID0BLMZ8e6S1a|HK7`sKS~K^C+~G=xtd((AwJpD~zE5~sp*4P>n>-=Ar`F&fa zUTxFusBj;BpOsVmBrGerr8Uv>tQt!G{SybNp{{*;N%(>ACn_qBizaO^=JdVEYo<1b z)TNWsjWeRkPd8l-(cYQ88Q(Bao^4E&(~zQr`f;V>=Mrw21TfO)>(z2(T8f^8YT%*# zS#im!q{+Lb;U#9Op0NvEX3}GCY~^yAZ`78buh-gib&h(u2~U>t)xsu=G~S&?o$CKi zp^1-M0@a^l5v}z3t-xZ+k8{=P!0qHY7wPo6bx7HEA%M79GLGWe!O zBtA+NN#dpBfZ(Jx^WW${Gb#&RBTw3%un=XFtHZw<_bqSg0^^WZ6@aKmJ&sh3_SQN7 zvNFX-yyUW}#%^tXRmu91)|PFLeASMgd^@-)jp0R%dD~PxL%$V$HMk(-*hF6Htg&}a zpH}gTU4^BP;ljku0D;hZ9AnU5jlIx{33|)R!hqdL|dd1 zw>(b)V6%g>4;0ZUstrMFhbE~-lEnmz*2n^up}R_g*_-&2k#s!eBs1?_ drH;pkbr9efLa#GfL_Yr9&^-L9^?ME-`wu=y&8GkW literal 0 HcmV?d00001 diff --git a/docs/main/resources/site/scripts/automenu.js b/docs/main/resources/site/scripts/automenu.js new file mode 100644 index 0000000..06bb705 --- /dev/null +++ b/docs/main/resources/site/scripts/automenu.js @@ -0,0 +1,102 @@ + +jQuery(document).ready(function() { + activeLinks(); + activeToggle(); + organizeContent(); +}); + +function organizeContent() { + var content = $('#content'); + var subcontent = $('
'); + content.prepend(subcontent); + content.find('h1').each(function(index) { + var section = $('
'); + subcontent.append(section); + var h1 = $(this); + var elements = h1.nextUntil('h1'); + var text = h1.text(); + var slug = slugify(text) + '-' + index; + addSectionToSidebar(text, slug); + section.append(makeSectionAnchor(h1, text, slug)); + if (elements.length > 0) { + elements.appendTo(section); + organizeSubSection(slug, elements); + } + }); + removeEmptyList(); +} + +function organizeSubSection(s, children) { + children.filter('h2').each(function(index, el) { + var h2 = $(this); + var text = h2.text(); + var slug = s + '-' + slugify(text) + '-' + index; + var a = makeSectionAnchor(h2, text, slug); + addSubSectionToSidebar(text, slug, s); + }); +} + +function makeSectionAnchor(h, text, slug) { + var a = $('').attr({ + 'class': 'anchor', + 'name': slug, + 'href': '#' + slug + }); + a.append(h.clone()); + h.replaceWith(a); + return a; +} + +function addSectionToSidebar(text, slug) { + var ul = $('
    ').addClass('sub_section'); + var a = $('' + text + ''); + a.find('.fa-angle-right').css('padding-top', '0.7em'); + var li = $('
  • '); + li.append(a).append(ul); + ul.hide(); + $('#sidebar').append(li); + a.click(function(event) { + $('#sidebar li').add('#sidebar a').removeClass('active'); + $('#sidebar .sub_section').not(ul).slideUp(); + ul.slideToggle('fast'); + li.add(a).toggleClass('active'); + }); +} + +function addSubSectionToSidebar(text, slug, s) { + var ul = $('#sidebar li.' + s + ' ul'); + var li = $('
  • ' + text + '
  • '); + ul.append(li); +} + +function removeEmptyList() { + $('#sidebar>li').not('.sidebar-brand').each(function(index, el) { + var li = $(this); + var children = li.find('li'); + if (children.size() == 0) { + li.find('span').remove(); + } + }); +} + +function slugify(text) { + return text.toString().toLowerCase() + .replace(/\s+/g, '-') // Replace spaces with - + .replace(/[^\w\-]+/g, '') // Remove all non-word chars + .replace(/\-\-+/g, '-') // Replace multiple - with single - + .replace(/^-+/, '') // Trim - from start of text + .replace(/-+$/, ''); // Trim - from end of text +} + +function activeToggle() { + $("#menu-toggle").click(function(e) { + e.preventDefault(); + $("#wrapper").toggleClass("toggled"); + }); +} + +function activeLinks() { + $('a[data-href]').each(function(index, el) { + $(this).attr('href', $(this).attr('data-href')); + }); +} diff --git a/docs/main/resources/site/styles/overrides.css b/docs/main/resources/site/styles/overrides.css new file mode 100644 index 0000000..2954ec7 --- /dev/null +++ b/docs/main/resources/site/styles/overrides.css @@ -0,0 +1,17 @@ +@media (min-width: 768px) { + .container { + width: 750px; } } +@media (min-width: 992px) { + .container { + width: 800px; } } +@media (min-width: 1200px) { + .container { + width: 800px; } } + +body { + font-size: 16px; +} + +a > code { + text-decoration: underline; +} diff --git a/docs/main/tut/docs.md b/docs/main/tut/docs.md new file mode 100644 index 0000000..60f207c --- /dev/null +++ b/docs/main/tut/docs.md @@ -0,0 +1,9 @@ +--- +layout: docs +--- + +# spark-alchemy by example + +```tut +import com.swoop.alchemy.utils.AnyExtensions +``` diff --git a/docs/main/tut/index.md b/docs/main/tut/index.md new file mode 100644 index 0000000..90642a9 --- /dev/null +++ b/docs/main/tut/index.md @@ -0,0 +1,53 @@ +--- +layout: home +title: "Home" +section: "Home" +--- + +## spark-alchemy + +Spark Alchemy is a collection of open-source Spark tools & frameworks that have made the data engineering and +data science teams at [Swoop](https://www.swoop.com) highly productive in our demanding petabyte-scale environment +with rich data (thousands of columns). + +We are preparing to release `spark-alchemy`. Click Watch above to be notified when we do. + +Here is a preview of what we'd like to include here: + +- Configuration Addressable Production (CAP), Automatic Lifecycle Management (ALM) and Just-in-time Dependency Resolution +(JDR) as outlined in our Spark+AI Summit talk [Unafraid of Change: Optimizing ETL, ML, and AI in Fast-Paced Environments](https://databricks.com/session/unafraid-of-change-optimizing-etl-ml-ai-in-fast-paced-environments). + +- Our extensive set of [HyperLogLog](https://en.wikipedia.org/wiki/HyperLogLog) (HLL) functions that allow the saving of HLL sketches as binary columns for fast +reaggregation as well as HLL interoperability with Postgres. (Spark has an HLL implementation but does not expose the binary HLL sketches, +which makes its usefulness rather limited.) + +- Hundreds of productivity-enhancing extensions to the core user-level data types: `Column`, `Dataset`, `SparkSession`, etc. + +- Data discovery and cleansing tools we use to ingest and clean up large amounts of dirty data from third parties. + +- Cross-cluster named lock manager, which simplifies data production by removing the need for workflow servers much of the time. + +- Versioned data source, which allows a new version to be written while the current version is being read. + +- `case class` code generation from Spark schema, with easy implementation customization. + +- Tools for deploying Spark ML pipelines to production. + +- Lots more, as we are constantly building up our internal toolset. + +All this is code we use on a daily basis at [Swoop](https://www.swoop.com) and [IPM.ai](https://www.ipm.ai). However, making the code +suited for external use and taking on the responsibility to manage it for the broader Spark community is not a task we take lightly. +We are reviewing/refactoring APIs based on what we've learned from using the code over months and years at Swoop and adjusting it for +Spark 2.4.x. The process we go through is as follows: + +1. Code we would like to consider for open-sourcing goes into an internal `spark-magic` library, which has no dependencies on Swoop-related +code, where it begins its "live use test" on multiple Spark clusters. + +2. Once we feel the APIs have baked enough, we review candidate code and move it to an internal/private version of `spark-alchemy` to check +for dependencies and interactions with other components. + +3. If all looks good, we promote the code to the public `spark-alchemy` (this repository). + +If you'd like to contribute to our open-source efforts, by joining our team or from your company, let us know at `spark-interest at swoop dot com`. + +For more Spark OSS work from Swoop, check out [spark-records](https://github.com/swoop-inc/spark-records). diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..6d44192 --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.2.4 diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..46896f5 --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,11 @@ +import sbt.Resolver + +scalaVersion := "2.12.5" + +addSbtPlugin("com.47deg" % "sbt-microsites" % "0.7.22") +addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.9.0") +addSbtPlugin("com.typesafe.sbt" % "sbt-ghpages" % "0.6.2") +addSbtPlugin("com.typesafe.sbt" % "sbt-git" % "1.0.0") +addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.0.0") +addSbtPlugin("org.foundweekends" % "sbt-bintray" % "0.5.4") +addSbtPlugin("org.tpolecat" % "tut-plugin" % "0.6.7") diff --git a/travis-deploy-key.enc b/travis-deploy-key.enc new file mode 100644 index 0000000000000000000000000000000000000000..64ca950c8624d61604012f3f912c5f794967b859 GIT binary patch literal 3248 zcmV;h3{UfIiNs~9uwQ2_Oky&VTLp&Ux;c@@LS=_{IXc>1^4^0z5>|+9MTI3M8KC#y zg#4#t3Xl3JaXIeCn|RPyycclLx_-vtXZ_;wU|k}{LEvGN(&F3QG~glZ>t2IxFPCie zI0p?n+jCwjI{9IM4B9JmTG3t#Hf1tppItu~482Xw|IfLy{x=*&Eh?xep*PF1M=1hW zFjs`55@=qNSgcJL0+VSY-4Z3Dw`|RcI+#b(%3T|c2g>Zhu&1}pQRu&u(N@3Gu{UV0 zrVQ!tH8c9@7-^UY+;yk5U#d!72gregSKKL?G=>)3v9R`>T4eGKU-7kNlq!H^ULz8h zGc0X#u7$e8yLSJz2#J}X7+X%37;I^s{t|suLd3jPFHx9*TmHGrVdSa`?wI!rOhiPWDq=*-!S6w3;uGRW=J%o?^SwT zso{_r(14i$6iyu@TX6kt`^uc8Q{uC_ST* zvdQH()om4Pt|ZbtSolWpgGO+Jz7#P=UYh&`Aj#FEV7}te7c9z(3bHrKuDm3GqD_PW zL|9TuvSQUnt?9rQvBUdLLAC7?QKKU$F(y;ytXo1pl`R`_p@gF8Mh9 z4eMOD{Zosn2s$anHmg8LY?&}pKfs`AP8B)8J|q4*To%v~ynPSxP_Kj4Z07;kqo(h6Ny4{=i-`<|%CU`bN z^BAAO zCvgA)5t=Jlx@Dh`Ek+iMlrfW%2JFPELd3Q53#*u5D1PE3KME6m=_pY#c|FwXS7f*I)GR(}sWd;0sC%rRsNZ@@ zVMMx!AVwDVoGWAJf=YJs;TJ2vV@>Z%4TsXK$GKUW+lrEV`0n55oVT}@8EGeBNym^( zj5AMZX4)jhq%4)mbI8i>Z#2TpL^aIeZ%T3!f}~HCtG1`d`&j98B#L{@xERy+yYGin zk~pnNP~HC9&FJP6l@&e%3qp*P3+}rp+BPzoI-akBEU1hYkYV;LT@`T4-bZsvITXP0 z(_=}LTwFL$Heo4A;6_3f1<(a40dO4}`XcE*em>@QA|Q6_Wyc$;Hb+GMfnL8XTz2Vg z!^wDg?sP|NlMK=?sLxA6%H-W-|F$ENs~O+t61%!>a^<%RKvXFsmUp3?) z%Y6NZVmCa^FM(70%OuX>*!@A)UeBU@r-PV*N48EXFg*_$-AC)c^(^uTM1l^iHPJnm z875EI4=5G`lIOq6^fv)K)qF#O9tg7~xAdg>oHLhnY@B}2utdoF7*s*HFVs`ITxNF? zEOWsVupuo1s3)TMOZKf~9n@N*=p(hXdKn7lc|^o7IuqL)^4`Tv-G8aBDQ$L zvtKez+u~B}Uuwyg`ce@#HxB&4pRfSpP+iu=vu+q%o>NiJ0s^${IB2cgW&T$x{@ijn znJ?XEs35&k(eyqj;_F${pRak{`ak+$J9t$_E~11dcuhvsg-i+ZRf{*Mh*EVNu7{lC zDK81u#26pQVk25pn<0nLG-T%JEx3aS`_@qv=Fjmg*Rsn^{*mkQbU3|+Hl?kYAUJ7p zVbQ3yrL8SyoL5QYesrR5Dcg$$l`n&50G&Kd18F;tl6&97H}DG@a>s+mpM5S`kT4c# zlxo#(96=4xBbiuwGUAUhgWEJ(DNp(608E8D^)RJ8?IAjm6shs8OQ0>md2k*BcCs** z8MEwQ?p~PJhy+JlgxQY><cZs+`Y{&sBu$%=keyvC32v1224Zd#R(jU{BxOiLfKGsbaUku_pLZi?9hJ2 z3%!{$Cvzt}`L2$bfod-$6JaP~FM#Fl=L-Z=YBJSdIsur;(SrY;pR;pa0$_x3b~M73 z14CH-g25czUslk*X#$*5knRYPiD&@Ey_`XOYvrbko~FI-h#@`4shPf9c5GGt z8`=jqB^%D8gM&ZsvZB`k#eZZ&8VP-{aDYc%C3;nT6tFd{KlpXBcfwSQFI1Ltd8$@U zK(LSBR{#e_@`t8jRlG6Qa|h@a}-By}km8V37R!l%Ye zNWC$Z#y=LKN0UEXz?fsR+J8a9Jbx%%9Is8G)XY!y^Y%!GGQam|uc#=D!QUJs?UMMD;=>yX?`f(9%977f~O9omLV5aYITk+Ng z3h0tsLIcUkoeu<4v7k!ocO`Af+kM)#ng#U65N}BZl-NKWZaP6L79iS;XE}ebC7YGQ3Jo*b*LaI(@RnPsZp70rk-+$UBmI zlEB%TVOdg}`Mv%;FGpRsVMXRIkl%8V0dIQmU~Vg!wTQhLSM{8hPLYNOBQ{7Bs1|4x zTEi!7 zFj;9nP9v#%%yR$!z|eQO`{QKb0+^1Y^}R6odpv5dvcCKq0B&Zh++ z@kdPW1{LTslYV@1SR+8De{*^t=&WcK6Jz0sm3%Yd3N9ge((8bX!K)#`!KR^q z6N_4yEH%Po&8nUN1z1iRjn`^#@#VEHQ$(<1ksG>%6z5UmX-C=3qrwkcJ;X73L<2r; i_gR^_Xt&ft1g0pK6kT*rQpoqHI>smL%>HWz?La7~uN=Jq literal 0 HcmV?d00001