diff --git a/CMakeLists.txt b/CMakeLists.txt index 1a0ceff9aca7..00e0011752f6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -110,6 +110,24 @@ if(${VELOX_ENABLE_BENCHMARKS} OR ${VELOX_ENABLE_BENCHMARKS_BASIC}) set(VELOX_BUILD_TEST_UTILS ON) endif() +if(${VELOX_BUILD_PYTHON_PACKAGE}) + set(VELOX_BUILD_TESTING OFF) + set(VELOX_ENABLE_PRESTO_FUNCTIONS ON) + set(VELOX_ENABLE_DUCKDB OFF) + set(VELOX_ENABLE_EXPRESSION ON) + set(VELOX_ENABLE_PARSE OFF) + set(VELOX_ENABLE_EXEC OFF) + set(VELOX_ENABLE_AGGREGATES OFF) + set(VELOX_ENABLE_HIVE_CONNECTOR OFF) + set(VELOX_ENABLE_TPCH_CONNECTOR OFF) + set(VELOX_ENABLE_SPARK_FUNCTIONS OFF) + set(VELOX_ENABLE_EXAMPLES OFF) + set(VELOX_ENABLE_S3 OFF) + set(VELOX_ENABLE_SUBSTRAIT OFF) + set(VELOX_CODEGEN_SUPPORT OFF) + set(VELOX_ENABLE_BENCHMARKS_BASIC OFF) +endif() + if(VELOX_ENABLE_S3) # Set AWS_ROOT_DIR if you have a custom install location of AWS SDK CPP. if(AWSSDK_ROOT_DIR) @@ -291,10 +309,10 @@ if(CMAKE_SYSTEM_NAME MATCHES "Darwin") link_directories("${ICU_INCLUDE_DIRS}/../lib") endif() -if(VELOX_BUILD_PYTHON_PACKAGE) - message(STATUS "Adding pybind11") +if(${VELOX_BUILD_PYTHON_PACKAGE}) set(pybind11_SOURCE AUTO) resolve_dependency(pybind11 REQUIRED_VERSION 2.10.0) + add_subdirectory(pyvelox) endif() # Locate or build folly. diff --git a/Makefile b/Makefile index caca442c06d3..d0ff05e1fd23 100644 --- a/Makefile +++ b/Makefile @@ -62,6 +62,8 @@ CPU_TARGET ?= "avx" FUZZER_SEED ?= 123456 FUZZER_DURATION_SEC ?= 60 +PYTHON_EXECUTABLE ?= $(shell which python) + all: release #: Build the release version clean: #: Delete all build artifacts @@ -145,3 +147,12 @@ help: #: Show the help messages @cat $(firstword $(MAKEFILE_LIST)) | \ awk '/^[-a-z]+:/' | \ awk -F: '{ printf("%-20s %s\n", $$1, $$NF) }' + +python-clean: + DEBUG=1 ${PYTHON_EXECUTABLE} setup.py clean + +python-build: + DEBUG=1 ${PYTHON_EXECUTABLE} setup.py develop + +python-test: python-build + DEBUG=1 ${PYTHON_EXECUTABLE} -m unittest -v diff --git a/pyvelox/CMakeLists.txt b/pyvelox/CMakeLists.txt new file mode 100644 index 000000000000..dea18a5de8fc --- /dev/null +++ b/pyvelox/CMakeLists.txt @@ -0,0 +1,32 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +if(VELOX_BUILD_PYTHON_PACKAGE) + message("Creating pyvelox module") + include_directories(SYSTEM ${CMAKE_SOURCE_DIR}) + add_definitions(-DCREATE_PYVELOX_MODULE) + # Define our Python module: + pybind11_add_module(pyvelox MODULE pyvelox.cpp pyvelox.h) + + # Link with Velox: + target_link_libraries(pyvelox PRIVATE velox_type) + + install(TARGETS pyvelox LIBRARY DESTINATION .) +else() + # Torcharrow will not use pyvelox as an extension module for compatibility + # reasons. + message("Creating pyvelox library") + add_library(pyvelox pyvelox.cpp pyvelox.h) + target_link_libraries(pyvelox velox_type pybind11::module) +endif() diff --git a/pyvelox/README.md b/pyvelox/README.md new file mode 100644 index 000000000000..7f179127beac --- /dev/null +++ b/pyvelox/README.md @@ -0,0 +1,45 @@ +# PyVelox: Python bindings and extensions for Velox + +**This library is currently in Alpha stage and does not have a stable release. The API and implementation may change based on +user feedback or performance. Future changes may not be backward compatible. +If you have suggestions on the API or use cases you'd like to be covered, please open a +GitHub issue. We'd love to hear thoughts and feedback.** + + +## Prerequisites + +You will need Python 3.7 or later. Also, we highly recommend installing an [Miniconda](https://docs.conda.io/en/latest/miniconda.html#latest-miniconda-installer-links) environment. + +First, set up an environment. If you are using conda, create a conda environment: +``` +conda create --name pyveloxenv python=3.7 +conda activate pyveloxenv +``` + + +### From Source + +Currently PyVelox can only be built from source. You will need Python 3.7 or later and a C++17 compiler. + + +#### Install Dependencies + +On macOS + +[HomeBrew](https://brew.sh/) is required to install development tools on macOS. +Run the script referenced [here](https://github.com/facebookincubator/velox#setting-up-on-macos) to install all the mac specific dependencies. + +On Linux +Run the script referenced [here](https://github.com/facebookincubator/velox#setting-up-on-linux-ubuntu-2004-or-later) to install on linux. + + +#### Install PyVelox +For local development, you can build with debug mode: +``` +DEBUG=1 python setup.py develop +``` + +And run unit tests with +``` +python -m unittest -v +``` diff --git a/pyvelox/__init__.py b/pyvelox/__init__.py new file mode 100644 index 000000000000..8daf2005df70 --- /dev/null +++ b/pyvelox/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/pyvelox/pyvelox.cpp b/pyvelox/pyvelox.cpp new file mode 100644 index 000000000000..1fa4059d0fb3 --- /dev/null +++ b/pyvelox/pyvelox.cpp @@ -0,0 +1,40 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "pyvelox.h" // @manual + +namespace facebook::velox::py { +using namespace velox; +namespace py = pybind11; + +std::string serializeType(const std::shared_ptr& type) { + const auto& obj = type->serialize(); + return folly::json::serialize(obj, velox::getSerializationOptions()); +} + +#ifdef CREATE_PYVELOX_MODULE +PYBIND11_MODULE(pyvelox, m) { + m.doc() = R"pbdoc( + PyVelox native code module + ----------------------- + )pbdoc"; + + addVeloxBindings(m); + + m.attr("__version__") = "dev"; +} +#endif +} // namespace facebook::velox::py diff --git a/pyvelox/pyvelox.h b/pyvelox/pyvelox.h new file mode 100644 index 000000000000..433596994fdb --- /dev/null +++ b/pyvelox/pyvelox.h @@ -0,0 +1,171 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include +#include "folly/json.h" + +namespace facebook::velox::py { + +std::string serializeType(const std::shared_ptr& type); + +/// Adds Velox Python Bindings to the module m. +/// +/// This function adds the following bindings: +/// * velox::TypeKind enum +/// * velox::Type and its derived types +/// * Basic functions on Type and its derived types. +/// +/// @param m Module to add bindings too. +/// @param asLocalModule If true then these bindings are only visible inside +/// the module. Refer to +/// https://pybind11.readthedocs.io/en/stable/advanced/classes.html#module-local-class-bindings +/// for further details. +inline void addVeloxBindings(pybind11::module& m, bool asLocalModule = true) { + // Inlining these bindings since adding them to the cpp file results in a + // ASAN error. + using namespace velox; + namespace py = pybind11; + + // Add TypeKind enum. + py::enum_(m, "TypeKind", py::module_local(asLocalModule)) + .value("BOOLEAN", velox::TypeKind::BOOLEAN) + .value("TINYINT", velox::TypeKind::TINYINT) + .value("SMALLINT", velox::TypeKind::SMALLINT) + .value("INTEGER", velox::TypeKind::INTEGER) + .value("BIGINT", velox::TypeKind::BIGINT) + .value("REAL", velox::TypeKind::REAL) + .value("DOUBLE", velox::TypeKind::DOUBLE) + .value("VARCHAR", velox::TypeKind::VARCHAR) + .value("VARBINARY", velox::TypeKind::VARBINARY) + .value("TIMESTAMP", velox::TypeKind::TIMESTAMP) + .value("OPAQUE", velox::TypeKind::OPAQUE) + .value("ARRAY", velox::TypeKind::ARRAY) + .value("MAP", velox::TypeKind::MAP) + .value("ROW", velox::TypeKind::ROW) + .export_values(); + + // Create VeloxType bound to velox::Type. + py::class_> type( + m, "VeloxType", py::module_local(asLocalModule)); + + // Adding all the derived types of Type here. + py::class_> booleanType( + m, "BooleanType", py::module_local(asLocalModule)); + py::class_> integerType( + m, "IntegerType", py::module_local(asLocalModule)); + py::class_> bigintType( + m, "BigintType", py::module_local(asLocalModule)); + py::class_> smallintType( + m, "SmallintType", py::module_local(asLocalModule)); + py::class_> tinyintType( + m, "TinyintType", py::module_local(asLocalModule)); + py::class_> realType( + m, "RealType", py::module_local(asLocalModule)); + py::class_> doubleType( + m, "DoubleType", py::module_local(asLocalModule)); + py::class_> timestampType( + m, "TimestampType", py::module_local(asLocalModule)); + py::class_> varcharType( + m, "VarcharType", py::module_local(asLocalModule)); + py::class_> varbinaryType( + m, "VarbinaryType", py::module_local(asLocalModule)); + py::class_> arrayType( + m, "ArrayType", py::module_local(asLocalModule)); + py::class_> mapType( + m, "MapType", py::module_local(asLocalModule)); + py::class_> rowType( + m, "RowType", py::module_local(asLocalModule)); + py::class_> + fixedArrayType(m, "FixedSizeArrayType", py::module_local(asLocalModule)); + + // Basic operations on Type. + type.def("__str__", &Type::toString); + // Gcc doesnt support the below kind of templatization. +#if defined(__clang__) + // Adds equality and inequality comparison operators. + type.def(py::self == py::self); + type.def(py::self != py::self); +#endif + type.def( + "cpp_size_in_bytes", + &Type::cppSizeInBytes, + "Return the C++ size in bytes"); + type.def( + "is_fixed_width", + &Type::isFixedWidth, + "Check if the type is fixed width"); + type.def( + "is_primitive_type", + &Type::isPrimitiveType, + "Check if the type is a primitive type"); + type.def("kind", &Type::kind, "Returns the kind of the type"); + type.def("serialize", &serializeType, "Serializes the type as JSON"); + + booleanType.def(py::init()); + tinyintType.def(py::init()); + smallintType.def(py::init()); + integerType.def(py::init()); + bigintType.def(py::init()); + realType.def(py::init()); + doubleType.def(py::init()); + varcharType.def(py::init()); + varbinaryType.def(py::init()); + timestampType.def(py::init()); + arrayType.def(py::init>()); + arrayType.def( + "element_type", &ArrayType::elementType, "Return the element type"); + fixedArrayType.def(py::init()) + .def("element_type", &velox::FixedSizeArrayType::elementType) + .def("fixed_width", &velox::FixedSizeArrayType::fixedElementsWidth); + mapType.def(py::init, std::shared_ptr>()); + mapType.def("key_type", &MapType::keyType, "Return the key type"); + mapType.def("value_type", &MapType::valueType, "Return the value type"); + + rowType.def(py::init< + std::vector, + std::vector>>()); + rowType.def("size", &RowType::size, "Return the number of columns"); + rowType.def( + "child_at", + &RowType::childAt, + "Return the type of the column at a given index", + py::arg("idx")); + rowType.def( + "find_child", + [](const std::shared_ptr& type, const std::string& name) { + return type->findChild(name); + }, + "Return the type of the column with the given name", + py::arg("name")); + rowType.def( + "get_child_idx", + &RowType::getChildIdx, + "Return the index of the column with the given name", + py::arg("name")); + rowType.def( + "name_of", + &RowType::nameOf, + "Return the name of the column at the given index", + py::arg("idx")); + rowType.def("names", &RowType::names, "Return the names of the columns"); +} + +} // namespace facebook::velox::py diff --git a/pyvelox/test/__init__.py b/pyvelox/test/__init__.py new file mode 100644 index 000000000000..8daf2005df70 --- /dev/null +++ b/pyvelox/test/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/pyvelox/test/test_types.py b/pyvelox/test/test_types.py new file mode 100644 index 000000000000..ee8fb1892902 --- /dev/null +++ b/pyvelox/test/test_types.py @@ -0,0 +1,58 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pyvelox.pyvelox as pv +import unittest + + +class TestVeloxTypes(unittest.TestCase): + def test_types(self): + # Ensure we support all the basic types + self.assertTrue(isinstance(pv.BooleanType(), pv.VeloxType)) + self.assertTrue(isinstance(pv.IntegerType(), pv.VeloxType)) + self.assertTrue(isinstance(pv.BigintType(), pv.VeloxType)) + self.assertTrue(isinstance(pv.SmallintType(), pv.VeloxType)) + self.assertTrue(isinstance(pv.TinyintType(), pv.VeloxType)) + self.assertTrue(isinstance(pv.RealType(), pv.VeloxType)) + self.assertTrue(isinstance(pv.DoubleType(), pv.VeloxType)) + self.assertTrue(isinstance(pv.TimestampType(), pv.VeloxType)) + self.assertTrue(isinstance(pv.VarcharType(), pv.VeloxType)) + self.assertTrue(isinstance(pv.VarbinaryType(), pv.VeloxType)) + + # Complex types + self.assertTrue(isinstance(pv.ArrayType(pv.BooleanType()), pv.VeloxType)) + self.assertTrue( + isinstance(pv.MapType(pv.VarcharType(), pv.VarbinaryType()), pv.VeloxType) + ) + self.assertTrue( + isinstance(pv.RowType(["c0"], [pv.BooleanType()]), pv.VeloxType) + ) + + def test_complex_types(self): + arrayType = pv.ArrayType(pv.BigintType()) + self.assertEquals(arrayType.element_type(), pv.BigintType()) + + mapType = pv.MapType(pv.VarcharType(), pv.VarbinaryType()) + self.assertEquals(mapType.key_type(), pv.VarcharType()) + self.assertEquals(mapType.value_type(), pv.VarbinaryType()) + + rowType = pv.RowType( + ["c0", "c1", "c2"], [pv.BooleanType(), pv.BigintType(), pv.VarcharType()] + ) + self.assertEquals(rowType.size(), 3) + self.assertEquals(rowType.child_at(0), pv.BooleanType()) + self.assertEquals(rowType.find_child("c1"), pv.BigintType()) + self.assertEquals(rowType.get_child_idx("c1"), 1) + self.assertEquals(rowType.name_of(1), "c1") + self.assertEquals(rowType.names(), ["c0", "c1", "c2"]) diff --git a/setup.py b/setup.py new file mode 100644 index 000000000000..4aad12338b20 --- /dev/null +++ b/setup.py @@ -0,0 +1,186 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#!/usr/bin/env python +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +import distutils.command.build +import distutils.command.clean +import os +import re +import shutil +import subprocess +import sys +from pathlib import Path +from setuptools import Extension, find_packages, setup +from setuptools.command.build_ext import build_ext + +ROOT_DIR = Path(__file__).parent.resolve() + + +# Override build directory +class BuildCommand(distutils.command.build.build): + def initialize_options(self): + distutils.command.build.build.initialize_options(self) + self.build_base = "_build" + + +def _get_version(): + version = open("./version.txt").read().strip() + version = re.sub("#.*\n?", "", version, flags=re.MULTILINE) + sha = "Unknown" + try: + sha = ( + subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=str(ROOT_DIR)) + .decode("ascii") + .strip() + ) + except Exception: + pass + + if os.getenv("BUILD_VERSION"): + version = os.getenv("BUILD_VERSION") + elif sha != "Unknown": + version += "+" + sha[:7] + + return version, sha + + +def _export_version(version, sha): + version_path = ROOT_DIR / "pyvelox" / "version.py" + with open(version_path, "w") as f: + f.write("__version__ = '{}'\n".format(version)) + f.write("git_version = {}\n".format(repr(sha))) + + +VERSION, SHA = _get_version() +_export_version(VERSION, SHA) + +print("-- Building version " + VERSION) + + +class clean(distutils.command.clean.clean): + def run(self): + # Run default behavior first + distutils.command.clean.clean.run(self) + + # Remove pyvelox extension + for path in (ROOT_DIR / "pyvelox").glob("**/*.so"): + print(f"removing '{path}'") + path.unlink() + # Remove build directory + build_dirs = [ + ROOT_DIR / "_build", + ] + for path in build_dirs: + if path.exists(): + print(f"removing '{path}' (and everything under it)") + shutil.rmtree(str(path), ignore_errors=True) + + +# Based off of +# https://github.com/pytorch/audio/blob/2c8aad97fc8d7647ee8b2df2de9312cce0355ef6/build_tools/setup_helpers/extension.py#L46 +class CMakeBuild(build_ext): + def run(self): + try: + subprocess.check_output(["cmake", "--version"]) + except OSError: + raise RuntimeError("CMake is not available.") + super().run() + + def build_extension(self, ext): + extdir = os.path.abspath(os.path.dirname(self.get_ext_fullpath(ext.name))) + + # required for auto-detection of auxiliary "native" libs + if not extdir.endswith(os.path.sep): + extdir += os.path.sep + + if "DEBUG" in os.environ: + cfg = "Debug" if os.environ["DEBUG"] == "1" else "Release" + else: + cfg = "Debug" if self.debug else "Release" + + exec_path = sys.executable + + cmake_args = [ + f"-DCMAKE_BUILD_TYPE={cfg}", + f"-DCMAKE_INSTALL_PREFIX={extdir}", + "-DCMAKE_VERBOSE_MAKEFILE=ON", + "-DVELOX_BUILD_PYTHON_PACKAGE=ON", + f"-DPYTHON_EXECUTABLE={exec_path} ", + "-DVELOX_CODEGEN_SUPPORT=OFF", + "-DVELOX_BUILD_MINIMAL=ON", + ] + build_args = ["--target", "install"] + + # Default to Ninja + if "CMAKE_GENERATOR" not in os.environ: + cmake_args += ["-GNinja"] + + # Set CMAKE_BUILD_PARALLEL_LEVEL to control the parallel build level + # across all generators. + if "CMAKE_BUILD_PARALLEL_LEVEL" not in os.environ: + # self.parallel is a Python 3 only way to set parallel jobs by hand + # using -j in the build_ext call, not supported by pip or PyPA-build. + if hasattr(self, "parallel") and self.parallel: + # CMake 3.12+ only. + build_args += ["-j{}".format(self.parallel)] + + if not os.path.exists(self.build_temp): + os.makedirs(self.build_temp) + + subprocess.check_call( + ["cmake", str(ROOT_DIR)] + cmake_args, cwd=self.build_temp + ) + print(self.build_temp) + subprocess.check_call( + ["cmake", "--build", "."] + build_args, cwd=self.build_temp + ) + + +setup( + name="pyvelox", + version=VERSION, + description="Python bindings and extensions for Velox", + url="https://github.com/facebookincubator/velox", + author="Meta", + author_email="velox@fb.com", + license="BSD", + install_requires=[ + "cffi", + "typing", + "tabulate", + "typing-inspect", + ], + python_requires=">=3.7", + classifiers=[ + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: BSD License", + "Operating System :: POSIX :: Linux", + "Programming Language :: C++", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: Implementation :: CPython", + ], + packages=find_packages() + find_packages(where="./test"), + zip_safe=False, + ext_modules=[Extension(name="pyvelox.pyvelox", sources=[])], + cmdclass={"build_ext": CMakeBuild, "clean": clean, "build": BuildCommand}, +) diff --git a/version.txt b/version.txt new file mode 100644 index 000000000000..ed21fb8d629a --- /dev/null +++ b/version.txt @@ -0,0 +1,14 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +0.0.1a0