pip install for Python Druid API (#13938)

Broken test appears unrelated to this PR

* make druidapi pip installable

* include druidapi in prerequisites

* add license to setup.py

* updates from Paul's review

* note about editable install

* Apply suggestions from code review

Co-authored-by: 317brian <53799971+317brian@users.noreply.github.com>

* update install instructions

* found unrelated typos

* standardize install cmd with pip

---------

Co-authored-by: 317brian <53799971+317brian@users.noreply.github.com>
This commit is contained in:
Victoria Lim 2023-03-21 11:37:39 -07:00 committed by GitHub
parent 1c7a03a47b
commit ede9903ff4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 188 additions and 127 deletions

3
.gitignore vendored
View File

@ -30,3 +30,6 @@ integration-tests/gen-scripts/
*.hprof
**/.ipynb_checkpoints/
*.pyc
**/.ipython/
**/.jupyter/
**/.local/

View File

@ -46,7 +46,7 @@
"- The `requests` package for Python. For example, you can install it with the following command:\n",
"\n",
" ```bash\n",
" pip3 install requests\n",
" pip install requests\n",
" ````\n",
"\n",
"- JupyterLab (recommended) or Jupyter Notebook running on a non-default port. By default, Druid\n",

View File

@ -564,7 +564,7 @@
"id": "2654e72c",
"metadata": {},
"source": [
"Use the REST client if you need to make calls that are not yet wrapped by the Python API, or if you want to do something special. To illustrate the client, you can make some of the same calls as in the [Druid REST API notebook](api_tutorial.ipynb).\n",
"Use the REST client if you need to make calls that are not yet wrapped by the Python API, or if you want to do something special. To illustrate the client, you can make some of the same calls as in the [Druid REST API notebook](api-tutorial.ipynb).\n",
"\n",
"The REST API maintains the Druid host: you just provide the specifc URL tail. There are methods to get or post JSON results. For example, to get status information:"
]

View File

@ -1,9 +1,9 @@
# Jupyter Notebook tutorials for Druid
If you are reading this in Jupyter, switch over to the [- START HERE -](- START HERE -.ipynb]
If you are reading this in Jupyter, switch over to the [0-START-HERE](0-START-HERE.ipynb)
notebook instead.
<!-- This README, the "- START HERE -" notebook, and the tutorial-jupyter-index.md file in
<!-- This README, the "0-START-HERE" notebook, and the tutorial-jupyter-index.md file in
docs/tutorials share a lot of the same content. If you make a change in one place, update
the other too. -->
@ -39,7 +39,7 @@ Make sure you meet the following requirements before starting the Jupyter-based
- The `requests` package for Python. For example, you can install it with the following command:
```bash
pip3 install requests
pip install requests
```
- JupyterLab (recommended) or Jupyter Notebook running on a non-default port. By default, Druid
@ -49,9 +49,9 @@ Make sure you meet the following requirements before starting the Jupyter-based
```bash
# Install JupyterLab
pip3 install jupyterlab
pip install jupyterlab
# Install Jupyter Notebook
pip3 install notebook
pip install notebook
```
- Start Jupyter using either JupyterLab
```bash
@ -65,8 +65,15 @@ Make sure you meet the following requirements before starting the Jupyter-based
jupyter notebook --port 3001
```
- An available Druid instance. You can use the `micro-quickstart` configuration
described in [Quickstart](https://druid.apache.org/docs/latest/tutorials/index.html).
- The Python API client for Druid. Clone the Druid repo if you haven't already.
Go to your Druid source repo and install `druidapi` with the following commands:
```bash
cd examples/quickstart/jupyter-notebooks/druidapi
pip install .
```
- An available Druid instance. You can use the [quickstart deployment](https://druid.apache.org/docs/latest/tutorials/index.html).
The tutorials assume that you are using the quickstart, so no authentication or authorization
is expected unless explicitly mentioned.
@ -85,4 +92,4 @@ Make sure you meet the following requirements before starting the Jupyter-based
## Continue in Jupyter
Start Jupyter (see above) and navigate to the "- START HERE -" page for more information.
Start Jupyter (see above) and navigate to the "0-START-HERE" notebook for more information.

View File

@ -63,7 +63,7 @@
"Install the [Requests](https://requests.readthedocs.io/en/latest/) library for Python before you start. For example:\n",
"\n",
"```bash\n",
"pip3 install requests\n",
"pip install requests\n",
"```\n",
"\n",
"Please read the [Requests Quickstart](https://requests.readthedocs.io/en/latest/user/quickstart/) to gain a basic understanding of how Requests works.\n",

View File

@ -28,6 +28,9 @@ in any Python environment, but is optimized for use in Jupyter, providing a comp
environment which complements the UI-based Druid console. The primary use of `druidapi` at present
is to support the set of tutorial notebooks provided in the parent directory.
`druidapi` works against any version of Druid. Operations that make use of newer features obviously work
only against versions of Druid that support those features.
## Install
At present, the best way to use `druidapi` is to clone the Druid repo itself:
@ -36,21 +39,29 @@ At present, the best way to use `druidapi` is to clone the Druid repo itself:
git clone git@github.com:apache/druid.git
```
`druidapi` is located in `examples/quickstart/jupyter-notebooks/druidapi/`
`druidapi` is located in `examples/quickstart/jupyter-notebooks/druidapi/`.
From this directory, install the package and its dependencies with pip using the following command:
Eventually we would like to create a Python package that can be installed with `pip`. Contributions
in that area are welcome.
```
pip install .
```
Dependencies are listed in `requirements.txt`.
Note that there is a second level `druidapi` directory that contains the modules. Do not run
the install command in the subdirectory.
`druidapi` works against any version of Druid. Operations that exploit newer features obviously work
only against versions of Druid that support those features.
Verify your installation by checking that the following command runs in Python:
## Getting Started
```python
import druidapi
```
The import statement should not return anything if it runs successfully.
## Getting started
To use `druidapi`, first import the library, then connect to your cluster by providing the URL to your Router instance. The way that is done differs a bit between consumers.
### From a Tutorial Jupyter Notebook
### From a tutorial Jupyter notebook
The tutorial Jupyter notebooks in `examples/quickstart/jupyter-notebooks` reside in the same directory tree
as this library. We start the library using the Jupyter-oriented API which is able to render tables in
@ -70,40 +81,17 @@ druid = druidapi.jupyter_client(router_endpoint)
The `jupyter_client` call defines a number of CSS styles to aid in displaying tabular results. It also
provides a "display" client that renders information as HTML tables.
### From Any Other Juypter Notebook
If you create a Jupyter notebook outside of the `jupyter-notebooks` directory then you must tell Python where
to find the `druidapi` library. (This step is temporary until `druidapi` is properly packaged.)
First, set a variable to point to the location where you cloned the Druid git repo:
```python
druid_dev = '/path/to/Druid-repo'
```
Then, add the notebooks directory to Python's module search path:
```python
import sys
sys.path.append(druid_dev + '/examples/quickstart/jupyter-notebooks/')
```
Now you can import `druidapi` and create a client as shown in the previous section.
### From a Python Script
### From a Python script
`druidapi` works in any Python script. When run outside of a Jupyter notebook, the various "display"
commands revert to displaying a text (not HTML) format. The steps are similar to those above:
```python
druid_dev = '/path/to/Druid-repo'
import sys
sys.path.append(druid_dev + '/examples/quickstart/jupyter-notebooks/')
import druidapi
druid = druidapi.client(router_endpoint)
```
## Library Organization
## Library organization
`druidapi` organizes Druid REST operations into various "clients," each of which provides operations
for one of Druid's functional areas. Obtain a client from the `druid` client created above. For
@ -127,7 +115,7 @@ available as properties on the `druid` object created above.
* `display` - A set of convenience operations to display results as lightly formatted tables
in either HTML (for Jupyter notebooks) or text (for other Python scripts).
## Assumed Cluster Architecture
## Assumed cluster architecture
`druidapi` assumes that you run a standard Druid cluster with a Router in front of the other nodes.
This design works well for most Druid clusters:
@ -148,7 +136,7 @@ The one exception to this rule is if you want to perform a health check (i.e. th
on a service other than the Router. These checks are _not_ proxied by the Router: you must connect to
the target service directly.
## Status Operations
## Status operations
When working with tutorials, a local Druid cluster, or a Druid integration test cluster, it is common
to start your cluster then immediately start performing `druidapi` operations. However, because Druid
@ -183,7 +171,7 @@ extension is loaded:
status_client.properties['druid.extensions.loadList']
```
## Display Client
## Display client
When run in a Jupyter notebook, it is often handy to format results for display. A special display
client performs operations _and_ formats them for display as HTML tables within the notebook.
@ -204,7 +192,7 @@ The most common methods are:
The display client also has other methods to format data as a table, to display various kinds
of messages and so on.
## Interactive Queries
## Interactive queries
The original [`pydruid`](https://pythonhosted.org/pydruid/) library revolves around Druid
"native" queries. Most new applications now use SQL. `druidapi` provides two ways to run
@ -264,7 +252,7 @@ channel count
Within Jupyter, the results are formatted as an HTML table.
### Advanced Queries
### Advanced queries
In addition to the SQL text, Druid also lets you specify:
@ -350,7 +338,7 @@ resp.show()
In fact, the display client `sql()` method uses the `resp.show()` method internally, which in turn uses the
`rows` and `schema` properties.
### Run a Query and Return Results
### Run a query and return results
The above forms are handy for interactive use in a notebook. If you just need to run a query to use the results
in code, just do the following:
@ -366,7 +354,7 @@ sql = 'SELECT * FROM {}'
rows = sql_client.sql(sql, ['myTable'])
```
## MSQ Queries
## MSQ queries
The SQL client can also run an MSQ query. See the `sql-tutorial.ipynb` notebook for examples. First define the
query:
@ -408,7 +396,7 @@ while for Druid to load the resulting segments, so you must wait for the table t
sql_client.wait_until_ready('myTable')
```
## Datasource Operations
## Datasource operations
To get information about a datasource, prefer to query the `INFORMATION_SCHEMA` tables, or use the methods
in the display client. Use the datasource client for other operations.
@ -425,7 +413,7 @@ datasources.drop('myWiki', True)
The True argument asks for "if exists" semantics so you don't get an error if the datasource does not exist.
## REST Client
## REST client
The `druidapi` is based on a simple REST client which is itself based on the Requests library. If you
need to use Druid REST APIs not yet wrapped by this library, you can use the REST client directly.
@ -495,3 +483,28 @@ Druid has a large number of special constants: type names, options, etc. The con
from druidapi import consts
help(consts)
```
## Contributing
We encourage you to contribute to the `druidapi` package.
Set up an editable installation for development by running the following command
in a local clone of your `apache/druid` repo in
`examples/quickstart/jupyter-notebooks/druidapi/`:
```
pip install -e .
```
An editable installation allows you to implement and test changes iteratively
without having to reinstall the package with every change.
When you update the package, also increment the version field in `setup.py` following the
[PEP 440 semantic versioning scheme](https://peps.python.org/pep-0440/#semantic-versioning).
Use the following guidelines for incrementing the version number:
* Increment the third position for a patch or bug fix.
* Increment the second position for new features, such as adding new method wrappers.
* Increment the first position for major changes and changes that are not backwards compatible.
Submit your contribution by opening a pull request to the `apache/druid` GitHub repository.

View File

@ -13,14 +13,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from .druid import DruidClient
from druidapi.druid import DruidClient
def jupyter_client(endpoint) -> DruidClient:
'''
Create a Druid client configured to display results as HTML withing a Jupyter notebook.
Waits for the cluster to become ready to avoid intermitent problems when using Druid.
'''
from .html import HtmlDisplayClient
from druidapi.html_display import HtmlDisplayClient
druid = DruidClient(endpoint, HtmlDisplayClient())
druid.status.wait_until_ready()
return druid
@ -33,3 +33,4 @@ def client(endpoint) -> DruidClient:
that the cluster has not yet fully started.
'''
return DruidClient(endpoint)

View File

@ -14,8 +14,8 @@
# limitations under the License.
import requests
from .consts import COORD_BASE
from .rest import check_error
from druidapi.consts import COORD_BASE
from druidapi.rest import check_error
# Catalog (new feature in Druid 26)
CATALOG_BASE = COORD_BASE + '/catalog'
@ -30,10 +30,10 @@ class CatalogClient:
Client for the Druid catalog feature that provides metadata for tables,
including both datasources and external tables.
'''
def __init__(self, rest_client):
self.client = rest_client
def post_table(self, schema, table_name, table_spec, version=None, overwrite=None):
params = {}
if version:
@ -44,7 +44,7 @@ class CatalogClient:
def create(self, schema, table_name, table_spec):
self.post_table(schema, table_name, table_spec)
def table(self, schema, table_name):
return self.client.get_json(REQ_CAT_SCHEMA_TABLE, args=[schema, table_name])
@ -53,7 +53,7 @@ class CatalogClient:
if if_exists and r.status_code == requests.codes.not_found:
return
check_error(r)
def edit_table(self, schema, table_name, action):
return self.client.post_json(REQ_CAT_SCHEMA_TABLE_EDIT, action, args=[schema, table_name])

View File

@ -14,9 +14,9 @@
# limitations under the License.
import requests, time
from .consts import COORD_BASE
from .rest import check_error
from .util import dict_get
from druidapi.consts import COORD_BASE
from druidapi.rest import check_error
from druidapi.util import dict_get
REQ_DATASOURCES = COORD_BASE + '/datasources'
REQ_DATASOURCE = REQ_DATASOURCES + '/{}'
@ -32,18 +32,18 @@ class DatasourceClient:
See https://druid.apache.org/docs/latest/operations/api-reference.html#datasources
'''
def __init__(self, rest_client):
self.rest_client = rest_client
def drop(self, ds_name, if_exists=False):
'''
Drops a data source.
Marks as unused all segments belonging to a datasource.
Marks as unused all segments belonging to a datasource.
Marking all segments as unused is equivalent to dropping the table.
Parameters
----------
ds_name: str
@ -51,9 +51,9 @@ class DatasourceClient:
Returns
-------
Returns a map of the form
{"numChangedSegments": <number>} with the number of segments in the database whose
state has been changed (that is, the segments were marked as unused) as the result
Returns a map of the form
{"numChangedSegments": <number>} with the number of segments in the database whose
state has been changed (that is, the segments were marked as unused) as the result
of this API call.
Reference
@ -67,10 +67,10 @@ class DatasourceClient:
def load_status_req(self, ds_name, params=None):
return self.rest_client.get_json(REQ_DS_LOAD_STATUS, args=[ds_name], params=params)
def load_status(self, ds_name):
return self.load_status_req(ds_name, {
'forceMetadataRefresh': 'true',
'forceMetadataRefresh': 'true',
'interval': '1970-01-01/2999-01-01'})
def wait_until_ready(self, ds_name):

View File

@ -13,7 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from . import consts
from druidapi import consts
class DisplayClient:
'''
@ -41,7 +41,7 @@ class DisplayClient:
def new_table(self):
raise NotImplementedError()
def show_table(self, table):
raise NotImplementedError()
@ -114,7 +114,7 @@ class DisplayClient:
def table(self, table_name):
'''
Describe a table by returning the list of columns in the table.
Parameters
----------
table_name str
@ -122,7 +122,7 @@ class DisplayClient:
If the form is "table", then the 'druid' schema is assumed.
'''
self._druid.sql._schema_query(table_name).show(display=self)
def function(self, table_name):
'''
Retrieve the list of parameters for a partial external table defined in

View File

@ -13,12 +13,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from .rest import DruidRestClient
from .status import StatusClient
from .catalog import CatalogClient
from .sql import QueryClient
from .tasks import TaskClient
from .datasource import DatasourceClient
from druidapi.rest import DruidRestClient
from druidapi.status import StatusClient
from druidapi.catalog import CatalogClient
from druidapi.sql import QueryClient
from druidapi.tasks import TaskClient
from druidapi.datasource import DatasourceClient
class DruidClient:
'''
@ -36,7 +36,7 @@ class DruidClient:
if display_client:
self.display_client = display_client
else:
from .text import TextDisplayClient
from druidapi.text_display import TextDisplayClient
self.display_client = TextDisplayClient()
self.display_client._druid = self
@ -58,7 +58,7 @@ class DruidClient:
to learn what the code does so you can replicate it in your own client.
'''
self.rest_client.enable_trace(enable)
@property
def status(self) -> StatusClient:
'''
@ -67,7 +67,7 @@ class DruidClient:
if not self.status_client:
self.status_client = StatusClient(self.rest_client)
return self.status_client
def status_for(self, endpoint) -> StatusClient:
'''
Returns the status client for a Druid service.
@ -116,11 +116,11 @@ class DruidClient:
if not self.datasource_client:
self.datasource_client = DatasourceClient(self.rest_client)
return self.datasource_client
@property
def display(self):
return self.display_client
def close(self):
self.rest_client.close()
self.rest_client = None

View File

@ -15,8 +15,8 @@
from IPython.display import display, HTML
from html import escape
from .display import DisplayClient
from .base_table import BaseTable
from druidapi.display import DisplayClient
from druidapi.base_table import BaseTable
STYLES = '''
<style>
@ -124,7 +124,7 @@ class HtmlDisplayClient(DisplayClient):
if not initialized:
display(HTML(STYLES))
initialized = True
def text(self, msg):
html('<div class="druid">' + escape_for_html(msg) + '</div>')
@ -136,6 +136,6 @@ class HtmlDisplayClient(DisplayClient):
def new_table(self):
return HtmlTable()
def show_table(self, table):
self.text(table.format())

View File

@ -14,9 +14,9 @@
# limitations under the License.
import requests
from .util import dict_get
from druidapi.util import dict_get
from urllib.parse import quote
from .error import ClientError
from druidapi.error import ClientError
def check_error(response):
'''
@ -31,7 +31,7 @@ def check_error(response):
This method attempts to parse these variations. If the error response JSON
matches one of the known error formats, then raises a `ClientError` with the error
message. Otherise, raises a Requests library `HTTPError` for a generic error.
If the response includes a JSON payload, then the it is returned in the json field
If the response includes a JSON payload, then the it is returned in the json field
of the `HTTPError` object so that the client can perhaps decode it.
'''
code = response.status_code
@ -43,7 +43,7 @@ def check_error(response):
except Exception:
# If we can't get the JSON, raise a Requests error
response.raise_for_status()
# Druid JSON payload. Try to make sense of the error
msg = dict_get(json, 'errorMessage')
if not msg:
@ -51,8 +51,8 @@ def check_error(response):
if msg:
# We have an explanation from Druid. Raise a Client exception
raise ClientError(msg)
# Don't know what the Druid JSON is. Raise a Requetss exception, but
# Don't know what the Druid JSON is. Raise a Requests exception, but
# add on the JSON in the hopes that the caller can make use of it.
try:
response.raise_for_status()
@ -191,7 +191,7 @@ class DruidRestClient:
args: array[str], default = None
Arguments to include in the relative URL to replace {} markers.
headers: dict, default = None
Additional HTTP header fields to send in the request.
@ -225,7 +225,7 @@ class DruidRestClient:
args: array[str], default = None
Arguments to include in the relative URL to replace {} markers.
headers: dict, default = None
Additional HTTP header fields to send in the request.
@ -255,7 +255,7 @@ class DruidRestClient:
def delete_json(self, req, args=None, params=None, headers=None):
return self.delete(req, args=args, params=params, headers=headers).json()
def close(self):
'''
Close the session. Use in scripts and tests when the system will otherwise complain

View File

@ -14,9 +14,9 @@
# limitations under the License.
import time, requests
from . import consts
from .util import dict_get, split_table_name
from .error import DruidError, ClientError
from druidapi import consts
from druidapi.util import dict_get, split_table_name
from druidapi.error import DruidError, ClientError
REQ_SQL = consts.ROUTER_BASE + '/sql'
REQ_SQL_TASK = REQ_SQL + '/task'
@ -50,7 +50,7 @@ class SqlRequest:
else:
self.context.update(context)
return self
def add_context(self, key, value):
return self.with_context({key: value})
@ -93,7 +93,7 @@ class SqlRequest:
def request_headers(self, headers):
self.headers = headers
return self
def to_common_format(self):
self.header = False
self.sql_types = False
@ -122,7 +122,7 @@ class SqlRequest:
def run(self):
return self.query_client.sql_query(self)
def request_from_sql_query(query_client, sql_query):
try:
req = SqlRequest(query_client, sql_query['query'])
@ -273,7 +273,7 @@ class SqlQueryResult:
@property
def _druid(self):
return self.request.query_client.druid_client
@property
def result_format(self):
return self.request.result_format()
@ -384,7 +384,7 @@ class SqlQueryResult:
def _display(self, display):
return self._druid.display if not display else display
def show(self, non_null=False, display=None):
display = self._display(display)
if not self.ok:
@ -469,7 +469,7 @@ class QueryTaskResult:
def _druid(self):
return self._request.query_client.druid_client
def _tasks(self):
return self._druid().tasks
@ -613,7 +613,7 @@ class QueryTaskResult:
def _display(self, display):
return self._druid().display if not display else display
def show(self, non_null=False, display=None):
display = self._display(display)
if not self.done:
@ -629,7 +629,7 @@ class QueryTaskResult:
display.alert('Query returned no {}rows'.format("visible " if non_null else ''))
return
display.data_table(data, [c.name for c in self.schema])
class QueryClient:
def __init__(self, druid, rest_client=None):
@ -806,7 +806,7 @@ class QueryClient:
'''
Returns the schema of a table as an array of dictionaries of the
form {"Position": "<n>", "Name": "<name>", "Type": "<type>"}
Parameters
----------
table_name: str

View File

@ -13,7 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from .consts import OVERLORD_BASE
from druidapi.consts import OVERLORD_BASE
REQ_TASKS = OVERLORD_BASE + '/tasks'
REQ_POST_TASK = OVERLORD_BASE + '/task'
@ -29,7 +29,7 @@ class TaskClient:
See https://druid.apache.org/docs/latest/operations/api-reference.html#tasks
'''
def __init__(self, rest_client):
self.client = rest_client
@ -40,7 +40,7 @@ class TaskClient:
Parameters
----------
state: str, default = None
Filter list of tasks by task state. Valid options are "running",
Filter list of tasks by task state. Valid options are "running",
"complete", "waiting", and "pending". Constants are defined for
each of these in the `consts` file.
@ -134,7 +134,7 @@ class TaskClient:
def submit_task(self, payload):
'''
Submits a task to the Overlord.
Returns the `taskId` of the submitted task.
Parameters
@ -164,7 +164,7 @@ class TaskClient:
Returns
-------
The REST response.
Reference
---------
`POST /druid/indexer/v1/task/{taskId}/shutdown`
@ -183,7 +183,7 @@ class TaskClient:
Returns
-------
The REST response.
Reference
---------
`POST /druid/indexer/v1/datasources/{dataSource}/shutdownAllTasks`

View File

@ -13,8 +13,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from .display import DisplayClient
from .base_table import pad, BaseTable
from druidapi.display import DisplayClient
from druidapi.base_table import pad, BaseTable
alignments = ['', '^', '>']
@ -130,13 +130,13 @@ class TextTable(BaseTable):
self._rows = []
table_rows = self.formatter(self.compute_def(self._rows))
return '\n'.join(table_rows)
def format_rows(self, rows, min_width, max_width):
if not self._col_fmt:
return self.default_row_format(rows, min_width, max_width)
else:
return self.apply_row_formats(rows, max_width)
def default_row_format(self, rows, min_width, max_width):
new_rows = []
if min_width <= max_width:
@ -164,7 +164,7 @@ class TextDisplayClient(DisplayClient):
def __init__(self):
DisplayClient.__init__(self)
def text(self, msg):
print(msg)
@ -176,6 +176,6 @@ class TextDisplayClient(DisplayClient):
def new_table(self):
return TextTable()
def show_table(self, table):
print(table.format())

View File

@ -13,12 +13,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from .error import ClientError
from druidapi.error import ClientError
def dict_get(dict, key, default=None):
'''
Returns the value of key in the given dict, or the default value if
the key is not found.
the key is not found.
'''
if not dict:
return default

View File

@ -0,0 +1,37 @@
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You 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.
from setuptools import setup, find_packages
setup(
name='druidapi',
version='0.1.0',
description='Python API client for Apache Druid',
url='https://github.com/apache/druid/tree/master/examples/quickstart/jupyter-notebooks/druidapi',
author='Apache Druid project',
author_email='dev@druid.apache.org',
license='Apache License 2.0',
packages=find_packages(),
install_requires=['requests'],
classifiers=[
'Development Status :: 3 - Alpha',
'Intended Audience :: Developers',
'Intended Audience :: End Users/Desktop',
'License :: OSI Approved :: Apache Software License',
'Operating System :: OS Independent',
'Programming Language :: Python :: 3',
],
)