LENS-1011: Implement python client for Lens
Project: http://git-wip-us.apache.org/repos/asf/lens/repo Commit: http://git-wip-us.apache.org/repos/asf/lens/commit/12b95da0 Tree: http://git-wip-us.apache.org/repos/asf/lens/tree/12b95da0 Diff: http://git-wip-us.apache.org/repos/asf/lens/diff/12b95da0 Branch: refs/heads/current-release-line Commit: 12b95da07df3957c851dd8af7d6d19b36368134f Parents: 01a561c Author: raju <[email protected]> Authored: Fri Jun 17 15:46:03 2016 +0800 Committer: raju <[email protected]> Committed: Fri Jun 17 15:46:03 2016 +0800 ---------------------------------------------------------------------- contrib/clients/pom.xml | 37 +++++ contrib/clients/python/.gitignore | 73 +++++++++ contrib/clients/python/README | 20 +++ contrib/clients/python/lens/__init__.py | 16 ++ contrib/clients/python/lens/client/__init__.py | 20 +++ contrib/clients/python/lens/client/log.py | 25 +++ contrib/clients/python/lens/client/main.py | 61 ++++++++ contrib/clients/python/lens/client/models.py | 64 ++++++++ contrib/clients/python/lens/client/query.py | 89 +++++++++++ contrib/clients/python/lens/client/utils.py | 62 ++++++++ contrib/clients/python/pom.xml | 110 +++++++++++++ contrib/clients/python/setup.cfg | 19 +++ contrib/clients/python/setup.py | 82 ++++++++++ contrib/clients/python/test/test_lensclient.py | 161 ++++++++++++++++++++ contrib/clients/python/tox.ini | 53 +++++++ contrib/pom.xml | 37 +++++ pom.xml | 5 + 17 files changed, 934 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/lens/blob/12b95da0/contrib/clients/pom.xml ---------------------------------------------------------------------- diff --git a/contrib/clients/pom.xml b/contrib/clients/pom.xml new file mode 100644 index 0000000..42a7fb0 --- /dev/null +++ b/contrib/clients/pom.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + 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. + +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <name>Lens Contributed Clients</name> + <parent> + <artifactId>lens-contrib</artifactId> + <groupId>org.apache.lens</groupId> + <version>2.6.0-beta-SNAPSHOT</version> + </parent> + + <artifactId>lens-contrib-clients</artifactId> + <packaging>pom</packaging> + <description>Lens Contrib clients jar</description> + <modules> + <module>python</module> + </modules> +</project> http://git-wip-us.apache.org/repos/asf/lens/blob/12b95da0/contrib/clients/python/.gitignore ---------------------------------------------------------------------- diff --git a/contrib/clients/python/.gitignore b/contrib/clients/python/.gitignore new file mode 100644 index 0000000..c4a5755 --- /dev/null +++ b/contrib/clients/python/.gitignore @@ -0,0 +1,73 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +build/ +dist/ +docs/_build/ +pip.egg-info/ +MANIFEST +.tox +*.egg +*.py[cod] +*~ +.coverage +coverage.xml + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + + +# Intellij +.idea/ http://git-wip-us.apache.org/repos/asf/lens/blob/12b95da0/contrib/clients/python/README ---------------------------------------------------------------------- diff --git a/contrib/clients/python/README b/contrib/clients/python/README new file mode 100644 index 0000000..732d3c9 --- /dev/null +++ b/contrib/clients/python/README @@ -0,0 +1,20 @@ +# lens-python-client +Lens Python Client + +## Installation +You can install like this: + + pip install ... + + +## Usage + if __name__ == '__main__': + with LensClient("http://lens.server.url/", "user.name", database="db") as client: + handle = client.queries.submit("cube select ...", query_name="My first query") + # Optionally wait for completion. + while not client.queries[handle].finished: + time.sleep(20) # sleep 20 seconds + print client.queries[handle].result_set_path + # listing queries: + for handle in client.queries(state='RUNNING'): + print client.queries[handle] http://git-wip-us.apache.org/repos/asf/lens/blob/12b95da0/contrib/clients/python/lens/__init__.py ---------------------------------------------------------------------- diff --git a/contrib/clients/python/lens/__init__.py b/contrib/clients/python/lens/__init__.py new file mode 100644 index 0000000..cce3aca --- /dev/null +++ b/contrib/clients/python/lens/__init__.py @@ -0,0 +1,16 @@ +# +# 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. +# http://git-wip-us.apache.org/repos/asf/lens/blob/12b95da0/contrib/clients/python/lens/client/__init__.py ---------------------------------------------------------------------- diff --git a/contrib/clients/python/lens/client/__init__.py b/contrib/clients/python/lens/client/__init__.py new file mode 100644 index 0000000..d59c5e9 --- /dev/null +++ b/contrib/clients/python/lens/client/__init__.py @@ -0,0 +1,20 @@ +# +# 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 .main import LensClient + +__all__ = ['LensClient'] +__version__ = "2.6.0" \ No newline at end of file http://git-wip-us.apache.org/repos/asf/lens/blob/12b95da0/contrib/clients/python/lens/client/log.py ---------------------------------------------------------------------- diff --git a/contrib/clients/python/lens/client/log.py b/contrib/clients/python/lens/client/log.py new file mode 100644 index 0000000..a10798f --- /dev/null +++ b/contrib/clients/python/lens/client/log.py @@ -0,0 +1,25 @@ +# +# 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. +# +import requests + + +class LensLogClient(object): + def __init__(self, base_url): + self.base_url = base_url + "logs/" + + def __getitem__(self, item): + return requests.get(self.base_url + str(item)).text http://git-wip-us.apache.org/repos/asf/lens/blob/12b95da0/contrib/clients/python/lens/client/main.py ---------------------------------------------------------------------- diff --git a/contrib/clients/python/lens/client/main.py b/contrib/clients/python/lens/client/main.py new file mode 100644 index 0000000..dc60497 --- /dev/null +++ b/contrib/clients/python/lens/client/main.py @@ -0,0 +1,61 @@ +# +# 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. +# +import os + +import requests +from six import string_types +from .log import LensLogClient +from .query import LensQueryClient +from .utils import conf_to_xml, xml_file_to_conf + + +class LensClient(object): + def __init__(self, base_url=None, username="", password="", database=None, conf=None): + if conf and isinstance(conf, string_types) and os.path.exists(conf): + if os.path.isdir(conf): + conf = os.path.join(conf, 'lens-client-site.xml') + if os.path.exists(conf): + conf = xml_file_to_conf(conf) + if not conf: + conf = {} + self.base_url = base_url or conf.get('lens.server.base.url', "http://0.0.0.0:9999/lensapi") + if self.base_url[-1] != '/': + self.base_url += "/" + username = username or conf.get('lens.client.user.name', "anonymous") + database = database or conf.get('lens.client.dbname') + self.open_session(username, password, database, conf) + self.queries = LensQueryClient(self.base_url, self._sessionid) + self.logs = LensLogClient(self.base_url) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close_session() + + def close_session(self): + if self._sessionid: + requests.delete(self.base_url + "session/", params={'sessionid': self._sessionid}) + self._sessionid = None + + def open_session(self, username, password, database, conf): + payload = [('username', username), ('password', password), ('sessionconf', conf_to_xml(conf))] + if database: + payload.append(('database', database)) + r = requests.post(self.base_url + "session/", files=payload, headers={'accept': 'application/xml'}) + r.raise_for_status() + self._sessionid = r.text http://git-wip-us.apache.org/repos/asf/lens/blob/12b95da0/contrib/clients/python/lens/client/models.py ---------------------------------------------------------------------- diff --git a/contrib/clients/python/lens/client/models.py b/contrib/clients/python/lens/client/models.py new file mode 100644 index 0000000..7451fe9 --- /dev/null +++ b/contrib/clients/python/lens/client/models.py @@ -0,0 +1,64 @@ +# +# 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. +# +import json + +from .utils import to_camel_case + + +class WrappedJson(dict): + _is_wrapper = False + + def __init__(self, seq=None, **kwargs): + super(WrappedJson, self).__init__(seq, **kwargs) + if len(self) == 1: + (self._wrapped_key, self._wrapped_value), = self.items() + self._is_wrapper = True + + def __getitem__(self, item): + for name in {item, to_camel_case(item)}: + if name in self: + return super(WrappedJson, self).__getitem__(name) + if self._is_wrapper: + if name in self._wrapped_value: + return self._wrapped_value[name] + raise Exception("Couldn't access " + str(item) + " in " + str(self)) + + def __getattr__(self, item): + try: + return self.__getitem__(item) + except: + pass + + def __str__(self): + if self._is_wrapper: + return str(self._wrapped_value) + return json.dumps(self, indent=2) + + def __repr__(self): + if self._is_wrapper: + return str(self._wrapped_value) + return super(WrappedJson, self).__repr__() + + def __eq__(self, other): + return super(WrappedJson, self).__eq__(other) or ( + self._is_wrapper and other._is_wrapper and str(self) == str(other)) + + +class LensQuery(WrappedJson): + @property + def finished(self): + return self.status.status in ('SUCCESSFUL', 'FAILED', 'CANCELED', 'CLOSED') http://git-wip-us.apache.org/repos/asf/lens/blob/12b95da0/contrib/clients/python/lens/client/query.py ---------------------------------------------------------------------- diff --git a/contrib/clients/python/lens/client/query.py b/contrib/clients/python/lens/client/query.py new file mode 100644 index 0000000..88ce719 --- /dev/null +++ b/contrib/clients/python/lens/client/query.py @@ -0,0 +1,89 @@ +# +# 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. +# + +import requests +import time +from six import string_types +from .models import WrappedJson, LensQuery +from .utils import conf_to_xml + + +class LensQueryClient(object): + def __init__(self, base_url, sessionid): + self._sessionid = sessionid + self.base_url = base_url + "queryapi/" + self.launched_queries = [] + self.finished_queries = {} + + def __call__(self, **filters): + filters['sessionid'] = self._sessionid + resp = requests.get(self.base_url + "queries/", params=filters, headers={'accept': 'application/json'}) + return self.sanitize_response(resp) + + def __getitem__(self, item): + if isinstance(item, string_types): + if item in self.finished_queries: + return self.finished_queries[item] + resp = requests.get(self.base_url + "queries/" + item, params={'sessionid': self._sessionid}, + headers={'accept': 'application/json'}) + resp.raise_for_status() + query = LensQuery(resp.json(object_hook=WrappedJson)) + if query.finished: + self.finished_queries[item] = query + return query + elif isinstance(item, WrappedJson): + if item._is_wrapper: + return self[item._wrapped_value] + raise Exception("Can't get query: " + str(item)) + + def submit(self, query, operation="execute", query_name=None, timeout=None, conf=None): + payload = [('sessionid', self._sessionid), ('query', query), ('operation', operation)] + if query_name: + payload.append(('queryName', query_name)) + if timeout: + payload.append(('timeoutmillis', timeout)) + payload.append(('conf', conf_to_xml(conf))) + resp = requests.post(self.base_url + "queries/", files=payload, headers={'accept': 'application/json'}) + query = self.sanitize_response(resp) + self.launched_queries.append(query) + return query + + def wait_till_finish(self, handle): + while not self[handle].finished: + time.sleep(1) + return self[handle] + + def sanitize_response(self, resp): + resp.raise_for_status() + try: + resp_json = resp.json(object_hook=WrappedJson) + if 'lensAPIResult' in resp_json: + resp_json = resp_json.lens_a_p_i_result + if 'error' in resp_json: + raise Exception(resp_json['error']) + if 'data' in resp_json: + data = resp_json.data + if len(data) == 2 and 'type' in data: + keys = list(data.keys()) + keys.remove('type') + return WrappedJson({data['type']: data[keys[0]]}) + return data + except: + resp_json = resp.json() + return resp_json + + http://git-wip-us.apache.org/repos/asf/lens/blob/12b95da0/contrib/clients/python/lens/client/utils.py ---------------------------------------------------------------------- diff --git a/contrib/clients/python/lens/client/utils.py b/contrib/clients/python/lens/client/utils.py new file mode 100644 index 0000000..16993ca --- /dev/null +++ b/contrib/clients/python/lens/client/utils.py @@ -0,0 +1,62 @@ +# +# 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. +# +import xml.etree.ElementTree as ET + +def to_camel_case(snake_str): + """ + Converts snake_case to camelCase. + :param snake_str: a string in snake case + :return: equivalent string in camel case + + >>> to_camel_case('a_b') + 'aB' + >>> to_camel_case('some_important_string') + 'someImportantString' + """ + + components = snake_str.split('_') + # We capitalize the first letter of each component except the first one + # with the 'title' method and join them together. + return components[0] + "".join(x.title() for x in components[1:]) + + +def conf_to_xml(conf): + """ + Converts LensConf given as dictionary to xml string + :param conf: a Dictionary + :return: LensConf xml string representation + >>> conf_to_xml(None) + '<conf></conf>' + >>> conf_to_xml({}) + '<conf></conf>' + >>> conf_to_xml({'a':'b'}) + '<conf><properties><entry><key>a</key><value>b</value></entry></properties></conf>' + """ + if conf and len(conf) != 0: + return "<conf><properties>" + \ + "".join("<entry><key>%s</key><value>%s</value></entry>" % (k, v) for k, v in conf.items()) + \ + "</properties></conf>" + return "<conf></conf>" + +def xml_file_to_conf(path): + """ + Reads file present at path as hadoop configuration file and converts that to a dictionary. + :param path: Path of xml + :return: configuration as dictionary + + """ + return {property.find('name').text: property.find('value').text for property in ET.parse(path).getroot()} http://git-wip-us.apache.org/repos/asf/lens/blob/12b95da0/contrib/clients/python/pom.xml ---------------------------------------------------------------------- diff --git a/contrib/clients/python/pom.xml b/contrib/clients/python/pom.xml new file mode 100644 index 0000000..79b198c --- /dev/null +++ b/contrib/clients/python/pom.xml @@ -0,0 +1,110 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + 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. + +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <name>Lens Python Client</name> + <parent> + <artifactId>lens-contrib-clients</artifactId> + <groupId>org.apache.lens</groupId> + <version>2.6.0-beta-SNAPSHOT</version> + </parent> + + <artifactId>lens-python-client</artifactId> + <packaging>pom</packaging> + <description>Lens python clients jar</description> + <build> + <plugins> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>exec-maven-plugin</artifactId> + <configuration> + <executable>python</executable> + <workingDirectory>${basedir}</workingDirectory> + <environmentVariables> + <VERSION>${project.version}</VERSION> + <!--<BUILD_NUMBER>${buildNumber}</BUILD_NUMBER>--> + <!--<TIMESTAMP>${timestamp}</TIMESTAMP>--> + </environmentVariables> + </configuration> + <executions> + <execution> + <id>setuptools clean</id> + <phase>clean</phase> + <goals> + <goal>exec</goal> + </goals> + <configuration> + <arguments> + <argument>setup.py</argument> + <argument>clean</argument> + <argument>--all</argument> + </arguments> + </configuration> + </execution> + <execution> + <id>setuptools install</id> + <phase>compile</phase> + <goals> + <goal>exec</goal> + </goals> + <configuration> + <arguments> + <argument>setup.py</argument> + <argument>develop</argument> + </arguments> + </configuration> + </execution> + <execution> + <id>setuptools test</id> + <phase>test</phase> + <goals> + <goal>exec</goal> + </goals> + <configuration> + <skip>${maven.test.skip}</skip> + <arguments> + <argument>setup.py</argument> + <argument>test</argument> + </arguments> + </configuration> + </execution> + <execution> + <id>setuptools deploy</id> + <phase>deploy</phase> + <goals> + <goal>exec</goal> + </goals> + <configuration> + <arguments> + <argument>setup.py</argument> + <argument>register</argument> + <argument>sdist</argument> + <argument>upload</argument> + </arguments> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> +</project> http://git-wip-us.apache.org/repos/asf/lens/blob/12b95da0/contrib/clients/python/setup.cfg ---------------------------------------------------------------------- diff --git a/contrib/clients/python/setup.cfg b/contrib/clients/python/setup.cfg new file mode 100644 index 0000000..5ad322b --- /dev/null +++ b/contrib/clients/python/setup.cfg @@ -0,0 +1,19 @@ +# +# 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. +# + +[metadata] +description-file = README http://git-wip-us.apache.org/repos/asf/lens/blob/12b95da0/contrib/clients/python/setup.py ---------------------------------------------------------------------- diff --git a/contrib/clients/python/setup.py b/contrib/clients/python/setup.py new file mode 100644 index 0000000..6147c90 --- /dev/null +++ b/contrib/clients/python/setup.py @@ -0,0 +1,82 @@ +# +# 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 __future__ import print_function + +import io +import os +import sys + +from setuptools import setup +from setuptools.command.test import test as TestCommand + +here = os.path.abspath(os.path.dirname(__file__)) + +def read(*filenames, **kwargs): + encoding = kwargs.get('encoding', 'utf-8') + sep = kwargs.get('sep', '\n') + buf = [] + for filename in filenames: + with io.open(filename, encoding=encoding) as f: + buf.append(f.read()) + return sep.join(buf) + +long_description = read('README') + +class Tox(TestCommand): + def finalize_options(self): + TestCommand.finalize_options(self) + self.test_args = [] + self.test_suite = True + def run_tests(self): + #import here, cause outside the eggs aren't loaded + import tox + errcode = tox.cmdline(self.test_args) + sys.exit(errcode) + +setup( + name='lens-client', + version="2.6.0", + url='http://github.com/apache/lens/', + license='Apache Software License', + author='Apache', + tests_require=['tox'], + install_requires=['requests>=2.9.1', 'six>=1.10.0'], + cmdclass={'test': Tox}, + author_email='[email protected]', + description='Python Lens Client', + long_description=long_description, + packages=['lens', 'lens.client'], + include_package_data=True, + platforms='any', + test_suite='lens.client.test.test_lensclient', + classifiers = [ + 'Programming Language :: Python', + 'Development Status :: 4 - Beta', + 'Natural Language :: English', + 'Environment :: Web Environment', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: Apache Software License', + 'Operating System :: OS Independent', + 'Topic :: Software Development :: Libraries :: Python Modules', + 'Topic :: Software Development :: Libraries :: Application Frameworks', + 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', + ], + extras_require={ + 'testing': ['pytest'], + } +) \ No newline at end of file http://git-wip-us.apache.org/repos/asf/lens/blob/12b95da0/contrib/clients/python/test/test_lensclient.py ---------------------------------------------------------------------- diff --git a/contrib/clients/python/test/test_lensclient.py b/contrib/clients/python/test/test_lensclient.py new file mode 100644 index 0000000..d49c55b --- /dev/null +++ b/contrib/clients/python/test/test_lensclient.py @@ -0,0 +1,161 @@ +# +# 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 __future__ import print_function + +import random +import string +from contextlib import contextmanager +from requests.exceptions import HTTPError + +import time + +import pytest +from lens.client import LensClient +from lens.client.models import WrappedJson +import subprocess +import os +import glob + +def check_output(command): + output = subprocess.check_output(command.split()) + if isinstance(output, bytes): # For Python 3. Python 2 directly gives string + output = output.decode("utf-8") + return output + +@contextmanager +def cwd(dir): + cur_dir = os.getcwd() + os.chdir(dir) + yield + os.chdir(cur_dir) + +def time_sorted_ls(path): + mtime = lambda f: os.stat(os.path.join(path, f)).st_mtime + return list(sorted(os.listdir(path), key=mtime)) + +def has_error(msg): + return any(x in msg for x in ('Error', 'error', 'Exception', 'exception')) + +def get_error(): + latest_out_file = list(name for name in time_sorted_ls('logs') if 'lensserver.out' in name)[-1] + print (latest_out_file) + with open(os.path.join('logs', latest_out_file)) as f: + return f.read() + +def select_query(path): + with open(path) as f: + for line in f: + if 'cube select' in line and 'sample_cube' in line: + return line + +class TestLensClient(object): + @classmethod + def setup_class(cls): + cls.db = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(10)) + try: + root = check_output("git rev-parse --show-toplevel") + except: + root = os.getenv("LENS_GIT_PATH") + print(root, root.strip()) + joined = os.path.join(root.strip(), 'lens-dist', 'target', '*bin', '*bin') + cls.base_path = glob.glob(joined)[0] + with cwd(cls.base_path): + with cwd('server'): + server_start_output = check_output("bin/lens-ctl restart") + assert "Started lens server" in server_start_output + assert os.path.exists('logs/server.pid') + time.sleep(1) + while not os.path.exists('logs/lensserver.log'): + error = get_error() + if has_error(error): + # Assert again with complete error + assert os.path.exists('logs/lensserver.log'), error + time.sleep(1) + error = get_error() + if has_error(error): + assert False, error + + with cwd('client'): + cls.candidate_query = select_query('examples/resources/cube-queries.sql') + with open('check_connection.sql', 'w') as f: + f.write('show databases') + for i in range(100): + try: + output = check_output('bin/lens-cli.sh --cmdfile check_connection.sql') + if not has_error(output): + break + except: + # Ignore error and retry + pass + time.sleep(1) + os.remove('check_connection.sql') + create_output = check_output('bin/run-examples.sh sample-metastore -db ' + cls.db) + if has_error(create_output): + raise Exception("Couldn't create sample metastore: " + create_output) + populate_output = check_output('bin/run-examples.sh populate-metastore -db ' + cls.db) + if has_error(populate_output): + raise Exception("Couldn't populate sample metastore: " + populate_output) + @classmethod + def teardown_class(cls): + # TODO: drop database + with cwd(cls.base_path): + with cwd('client'): + with open('drop_db.sql', 'w') as f: + f.write("drop database {db} --cascade".format(db=cls.db)) + drop_output = check_output('bin/lens-cli.sh --cmdfile drop_db.sql') + if has_error(drop_output): + raise Exception("Couldn't drop db") + os.remove('drop_db.sql') + with cwd('server'): + stop_output = check_output('bin/lens-ctl stop') + if has_error(stop_output): + raise("Error stopping server: " + stop_output) + + def get_client(self): + return LensClient(database = self.db, conf=os.path.join(self.base_path, 'client', 'conf')) + + def test_auto_close_session(self): + with self.get_client() as client: + pass + with pytest.raises(HTTPError) as e: + # Now any api should give 410 + client.queries(state='RUNNING') + assert e.value.response.status_code == 410 + + def test_wrong_query(self): + with self.get_client() as client: + with pytest.raises(HTTPError) as e: + client.queries.submit("blah") + assert e.value.response.status_code == 400; + assert 'Syntax Error' in e.value.response.json(object_hook=WrappedJson).lens_a_p_i_result.error.message + + def test_submit_query(self): + with self.get_client() as client: + handle = client.queries.submit(self.candidate_query) + # session not closed + assert client.queries[handle] + client.queries.wait_till_finish(handle) + client.close_session() + + def test_list_query(self): + with self.get_client() as client: + handle = client.queries.submit(self.candidate_query, query_name="Candidate Query") + finished_query = client.queries.wait_till_finish(handle) + assert client.queries[handle] == finished_query + queries = client.queries(state='SUCCESSFUL', fromDate=finished_query.submission_time - 1, toDate=finished_query.submission_time + 1) + assert handle in queries + http://git-wip-us.apache.org/repos/asf/lens/blob/12b95da0/contrib/clients/python/tox.ini ---------------------------------------------------------------------- diff --git a/contrib/clients/python/tox.ini b/contrib/clients/python/tox.ini new file mode 100644 index 0000000..c048e14 --- /dev/null +++ b/contrib/clients/python/tox.ini @@ -0,0 +1,53 @@ +# +# 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. +# + +[tox] +envlist=py27, py35 + +[testenv] +deps= + pytest + coverage + pytest-cov +setenv= + PYTHONWARNINGS=all +passenv = + HADOOP_HOME + HIVE_HOME + LENS_GIT_PATH + +[pytest] +addopts=--ignore setup.py --doctest-modules +norecursedirs=.tox .git + +[testenv:py27] +commands= + py.test + +[testenv:py35] +commands= + py.test + +[testenv:py27verbose] +basepython=python +commands= + py.test --cov=. --cov-report term + +[testenv:py35verbose] +basepython=python3.5 +commands= + py.test --cov=. --cov-report term \ No newline at end of file http://git-wip-us.apache.org/repos/asf/lens/blob/12b95da0/contrib/pom.xml ---------------------------------------------------------------------- diff --git a/contrib/pom.xml b/contrib/pom.xml new file mode 100644 index 0000000..0f792ab --- /dev/null +++ b/contrib/pom.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + 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. + +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <name>Lens Contrib</name> + <parent> + <artifactId>apache-lens</artifactId> + <groupId>org.apache.lens</groupId> + <version>2.6.0-beta-SNAPSHOT</version> + </parent> + + <artifactId>lens-contrib</artifactId> + <packaging>pom</packaging> + <description>Lens Contrib jar</description> + <modules> + <module>clients</module> + </modules> +</project> http://git-wip-us.apache.org/repos/asf/lens/blob/12b95da0/pom.xml ---------------------------------------------------------------------- diff --git a/pom.xml b/pom.xml index 9695126..f1b2289 100644 --- a/pom.xml +++ b/pom.xml @@ -657,6 +657,11 @@ <exclude>**/*.js</exclude> <exclude>**/*.properties</exclude> <exclude>**/*.json</exclude> + <!-- python client --> + <exclude>**/.tox/</exclude> + <exclude>**/.cache/</exclude> + <exclude>**/*.egg-info/**</exclude> + <exclude>**/*.egg</exclude> </excludes> </configuration> <executions>
