├── python_wrapper ├── requirements.txt ├── tests │ └── test_simple.py ├── susi_python │ ├── __init__.py │ ├── response_parser.py │ ├── models.py │ └── main.py ├── README.md ├── setup.py └── sample.py ├── .travis.yml ├── README.md ├── .bandit.yml └── LICENSE /python_wrapper/requirements.txt: -------------------------------------------------------------------------------- 1 | requests>=2.13.0 2 | youtube-dl 3 | geocoder 4 | -------------------------------------------------------------------------------- /python_wrapper/tests/test_simple.py: -------------------------------------------------------------------------------- 1 | import susi_python as susi 2 | 3 | 4 | # Check a simple reply 5 | def test_reply(): 6 | answer = susi.ask("hi") 7 | assert answer is not None 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.6" 4 | install: 5 | - pip install -U pytest-runner 6 | script: 7 | # for Python API wrapper 8 | - cd python_wrapper 9 | - python setup.py pytest 10 | -------------------------------------------------------------------------------- /python_wrapper/susi_python/__init__.py: -------------------------------------------------------------------------------- 1 | from .main import use_api_endpoint 2 | from .main import query 3 | from .main import ask 4 | from .main import answer_from_json 5 | from .main import sign_in 6 | from .main import sign_up 7 | from .main import forgot_password 8 | from .main import get_previous_responses 9 | from .main import update_location -------------------------------------------------------------------------------- /python_wrapper/README.md: -------------------------------------------------------------------------------- 1 | # Susi AI API Python Wrapper 2 | Currently the wrapper supports basic chat and auth functionalities. 3 | ## Usage 4 | For usage sample refer: [sample.py](https://github.com/fossasia/susi_api_wrapper/blob/master/python_wrapper/sample.py) 5 | 6 | ## Roadmap 7 | Implement all queries supported by Susi AI API like Maps,Charts etc. and provide an easy interface to access them in Python. 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wrappers for Susi AI REST API 2 | 3 | [](https://travis-ci.org/fossasia/susi_api_wrapper) 4 | 5 | This repository contains the wrappers for using Susi AI REST api in multiple languages 6 | for easy access by the developers who wish to develop their application utilizing Susi AI 7 | intelligence. 8 | 9 | ## Currently Included Languages 10 | - Python : specific guide [here](https://github.com/fossasia/susi_api_wrapper/tree/master/python_wrapper) 11 | 12 | ## Roadmap 13 | - Provide wrappers for languages currently being utilized by application developers for Susi 14 | - Improve support for existing languages 15 | -------------------------------------------------------------------------------- /.bandit.yml: -------------------------------------------------------------------------------- 1 | ### profile may optionally select or skip tests 2 | 3 | # (optional) list skipped tests here: 4 | skips: ['B605'] 5 | 6 | ### override settings - used to set settings for plugins to non-default values 7 | 8 | any_other_function_with_shell_equals_true: 9 | no_shell: [os.execl, os.execle, os.execlp, os.execlpe, os.execv, os.execve, 10 | os.execvp, os.execvpe, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe, 11 | os.spawnv, os.spawnve, os.spawnvp, os.spawnvpe, os.startfile] 12 | shell: [os.system, os.popen, os.popen2, os.popen3, os.popen4, 13 | popen2.popen2, popen2.popen3, popen2.popen4, popen2.Popen3, 14 | popen2.Popen4, commands.getoutput, commands.getstatusoutput] 15 | subprocess: [subprocess.Popen, subprocess.call, subprocess.check_call, 16 | subprocess.check_output, 17 | utils.execute, utils.execute_with_timeout] -------------------------------------------------------------------------------- /python_wrapper/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='susi_python', 5 | version="0.0.1", 6 | author='SUSI.AI', 7 | author_email='susiai@googlegroups.com', 8 | url='http://susi.ai', 9 | description='SUSI AI API Python Wrapper', 10 | long_description_markdown_filename='README.md', 11 | license='Apache', 12 | packages=['susi_python'], 13 | install_requires=['requests', 'youtube-dl', 'geocoder'], 14 | setup_requires=["setuptools-markdown"], 15 | classifiers=[ 16 | "Development Status :: 4 - Beta", 17 | "License :: OSI Approved :: Apache License", 18 | "Environment :: Console", 19 | "Intended Audience :: End Users/Desktop", 20 | "Natural Language :: English", 21 | "Operating System :: POSIX :: Linux", 22 | "Programming Language :: Python :: 3.4", 23 | "Programming Language :: Python :: 3.5", 24 | "Programming Language :: Python :: 3.6", 25 | "Programming Language :: Python :: 3.7", 26 | ] 27 | ) 28 | -------------------------------------------------------------------------------- /python_wrapper/sample.py: -------------------------------------------------------------------------------- 1 | import susi_python as susi 2 | import os 3 | 4 | ''' 5 | A sample application demonstrating use of Wrapper 6 | Currently, Wrapper supports basic chat and auth functionality 7 | ''' 8 | 9 | ''' 10 | Auth Examples 11 | ''' 12 | 13 | 14 | def sign_up(email, password): 15 | response = susi.sign_up(email, password) 16 | 17 | # message from server 18 | message = response.message 19 | print("Server's Message:" + message) 20 | 21 | 22 | def get_old_conversations(): 23 | response = susi.get_previous_responses() 24 | print(response) 25 | # handle response 26 | 27 | 28 | # To sign up a user by email and password 29 | # sign_up('EMAIL_HERE', 'PASSWORD_HERE') 30 | 31 | # To sign in a user by email and password 32 | # susi.sign_in('clever@gmail.com', 'Clever123') 33 | 34 | """ 35 | Use a custom api endpoint 36 | """ 37 | # susi.use_api_endpoint('
') 38 | 39 | ''' 40 | handle previous conversations 41 | ''' 42 | # get_old_conversations() 43 | 44 | ''' 45 | Chat Example 46 | ''' 47 | 48 | while True: 49 | print("You: ", end='') 50 | input_query = str(input()) 51 | reply = susi.ask(input_query) 52 | print(reply) 53 | if 'answer' in reply.keys(): 54 | print('Susi:' + reply['answer']) 55 | if 'identifier' in reply.keys(): 56 | classifier = reply['identifier'] 57 | print(classifier[:3]) 58 | if classifier[:3] == 'ytd': 59 | audio_url = result['identifier'] #bandit -s B605 60 | os.system('tizonia --youtube-audio-stream '+ audio_url) #nosec #pylint-disable type: ignore 61 | else : 62 | audio_url = reply['identifier'] # bandit -s B605 63 | os.system('play ' + audio_url[6:]) # nosec #pylint-disable type: ignore 64 | if 'table' in reply.keys(): 65 | table = reply['table'] 66 | for h in table.head: 67 | print('%s\t' % h, end='') 68 | print() 69 | for datum in table.data: 70 | for value in datum: 71 | print('%s\t' % value, end='') 72 | print() 73 | if 'map' in reply.keys(): 74 | mapObject = reply['map'] 75 | print("Map can be viewed at %s", mapObject.openStreetMapLink) 76 | if 'rss' in reply.keys(): 77 | rss = reply['rss'] 78 | for entity in rss['entities']: 79 | print('title: {0}\ndescription: {1}\nlink:{2}'. 80 | format(entity.title, entity.description, entity.link)) 81 | -------------------------------------------------------------------------------- /python_wrapper/susi_python/response_parser.py: -------------------------------------------------------------------------------- 1 | from .models import * 2 | 3 | 4 | def get_action(jsn): 5 | if jsn['type'] == 'answer': 6 | return AnswerAction(jsn['expression']) 7 | elif jsn['type'] == 'table': 8 | return TableAction(jsn['columns']) 9 | elif jsn['type'] == 'map': 10 | return MapAction(jsn['latitude'], jsn['longitude'], jsn['zoom']) 11 | elif jsn['type'] == 'anchor': 12 | return AnchorAction(jsn['link'], jsn['text']) 13 | elif jsn['type'] == 'rss': 14 | return RssAction(jsn['count'], jsn['title'], jsn['description'], jsn['link']) 15 | elif jsn['type'] == 'video_play': 16 | return VideoAction(jsn['identifier'], jsn['identifier_type']) 17 | elif jsn['type'] == 'stop': 18 | return StopAction() 19 | elif jsn['type'] == 'audio_play': 20 | return AudioAction(jsn['identifier'], jsn['identifier_type']) 21 | elif jsn['type'] == 'audio_volume': 22 | return VolumeAction(jsn['volume']) 23 | elif jsn['type'] == 'pause': 24 | return MediaAction(jsn['type']) 25 | elif jsn['type'] == 'restart': 26 | return MediaAction(jsn['type']) 27 | elif jsn['type'] == 'resume': 28 | return MediaAction(jsn['type']) 29 | else: 30 | return UnknownAction() 31 | 32 | 33 | def get_query_response(parsed_dict): 34 | ans = parsed_dict['answers'][0] 35 | 36 | data = [Datum(jsn) 37 | for jsn in ans['data']] 38 | 39 | metadata = Metadata(ans['metadata']) 40 | 41 | actions = [get_action(jsn) 42 | for jsn in ans['actions']] 43 | print(actions[::-1]) 44 | answer = Answer(data, metadata, actions[::-1]) 45 | 46 | try: 47 | identity_json = parsed_dict['session']['identity'] 48 | identity = Identity(identity_json) 49 | session = Session(identity) 50 | except KeyError: 51 | session = None 52 | 53 | return QueryResponse(answer,parsed_dict, session) 54 | 55 | 56 | def get_sign_in_response(parsed_dict): 57 | identity_json = parsed_dict['session']['identity'] 58 | identity = Identity(identity_json) 59 | session = Session(identity) 60 | return LoginResponse(parsed_dict, session) 61 | 62 | 63 | def get_memory_responses(parsed_dict): 64 | cognitions = parsed_dict['cognitions'] 65 | 66 | susi_responses = [] 67 | for cognition in cognitions: 68 | susi_responses.append(get_query_response(cognition)) 69 | 70 | return susi_responses 71 | 72 | 73 | def get_forgot_password_response(parsed_dict): 74 | return ForgotPasswordResponse(parsed_dict) 75 | 76 | 77 | def get_sign_up_response(parsed_dict): 78 | identity_json = parsed_dict['session']['identity'] 79 | identity = Identity(identity_json) 80 | session = Session(identity) 81 | return SignUpResponse(parsed_dict, session) 82 | -------------------------------------------------------------------------------- /python_wrapper/susi_python/models.py: -------------------------------------------------------------------------------- 1 | class QueryResponse: 2 | def __init__(self,answer, json, session): 3 | self.query = json['query'] 4 | self.count = json['count'] 5 | self.client_id = json['client_id'] 6 | self.query_date = json['query_date'] 7 | self.answer_time = json['answer_time'] 8 | self.session = session 9 | self.answer = answer 10 | 11 | def __repr__(self): 12 | return 'QueryResponse (query = %s , count = %s, client_id = %s, ' \ 13 | 'query_date = %s, answer_time = %s, session = %s, answer = %s )' % \ 14 | (self.query, self.count, self.client_id, self.query_date, self.answer_time, self.session, self.answer) 15 | 16 | 17 | class LoginResponse: 18 | def __init__(self, json, session): 19 | self.message = json['message'] 20 | self.session = session 21 | self.valid_seconds = json['valid_seconds'] 22 | self.access_token = json['access_token'] 23 | 24 | def __repr__(self): 25 | return 'LoginResponse: (message = %s, session = %s, valid_seconds = %s, access_token = %s )' % \ 26 | (self.message, self.session, self.valid_seconds, self.access_token) 27 | 28 | 29 | class SignUpResponse: 30 | def __init__(self, json, session): 31 | self.session = session 32 | self.message = json['message'] 33 | 34 | def __repr__(self): 35 | return 'SignUpResponse: (message = %s, session = %s' % \ 36 | (self.message, self.session) 37 | 38 | 39 | class ForgotPasswordResponse: 40 | def __init__(self, json): 41 | self.message = json['message'] 42 | 43 | def __repr__(self): 44 | return 'ForgotPasswordResponse: (message = %s)' % self.message 45 | 46 | 47 | class Answer: 48 | def __init__(self, data, metadata, actions): 49 | self.data = data 50 | self.metadata = metadata 51 | self.actions = actions 52 | 53 | def __repr__(self): 54 | return 'Answer: (data = %s, metadata = %s, actions = %s)' % \ 55 | (self.data, self.metadata, self.actions) 56 | 57 | 58 | class Datum: 59 | def __init__(self, json): 60 | # all properties of Datum are exposed as a dictionary rather than by field names 61 | self.values = json 62 | 63 | def __repr__(self): 64 | return 'Datum: (values = %s)' % self.values 65 | 66 | 67 | class Metadata: 68 | def __init__(self, json): 69 | self.count = json['count'] 70 | 71 | def __repr__(self): 72 | return 'Metadata: (count = %s)' % self.count 73 | 74 | 75 | class BaseAction: 76 | def __init__(self): 77 | pass 78 | 79 | 80 | class UnknownAction(BaseAction): 81 | def __init__(self): 82 | super().__init__() 83 | 84 | 85 | class AnswerAction(BaseAction): 86 | def __init__(self, expression): 87 | super().__init__() 88 | self.expression = expression 89 | 90 | 91 | class TableAction(BaseAction): 92 | def __init__(self, columns): 93 | super().__init__() 94 | # columns is a dictionary containing list of names of column to be displayed on client. 95 | self.columns = columns 96 | 97 | 98 | class MapAction(BaseAction): 99 | def __init__(self, latitude, longitude, zoom=None): 100 | super().__init__() 101 | self.latitude = latitude 102 | self.longitude = longitude 103 | if zoom is None: 104 | self.zoom = 13 105 | else: 106 | self.zoom = zoom 107 | 108 | 109 | class AnchorAction(BaseAction): 110 | def __init__(self, link, text): 111 | super().__init__() 112 | self.link = link 113 | self.text = text 114 | 115 | class VideoAction(BaseAction): 116 | def __init__(self, identifier , identifier_type): 117 | super().__init__() 118 | self.identifier = identifier 119 | self.identifier_type = identifier_type 120 | 121 | class RssAction(BaseAction): 122 | def __init__(self, count, title, description, link): 123 | super().__init__() 124 | self.count = count 125 | self.title = title 126 | self.description = description 127 | self.link = link 128 | 129 | class StopAction(BaseAction): 130 | def __init__(self): 131 | super().__init__() 132 | 133 | class AudioAction(BaseAction): 134 | def __init__(self, identifier , identifier_type): 135 | super().__init__() 136 | self.identifier = identifier 137 | self.identifier_type = identifier_type 138 | 139 | class MediaAction(BaseAction): 140 | def __init__(self, mediaAction): 141 | super().__init__() 142 | 143 | 144 | class VolumeAction(BaseAction): 145 | def __init__(self, volume): 146 | super().__init__() 147 | self.volume = volume 148 | 149 | class Session: 150 | def __init__(self, identity): 151 | self.identity = identity 152 | 153 | def __repr__(self): 154 | return 'Session: (identity = %s)' % self.identity 155 | 156 | 157 | class Identity: 158 | def __init__(self, json): 159 | self.name = json['name'] 160 | self.type = json['type'] 161 | self.anonymous = json['anonymous'] 162 | 163 | def __repr__(self): 164 | return 'Identity: (name = %s, type = %s, anonymous = %s)' % \ 165 | (self.name, self.type, self.anonymous) 166 | 167 | 168 | class Table: 169 | def __init__(self, columns, data): 170 | self.head = list(columns.values()) 171 | 172 | table_data = [] 173 | for datum in data: 174 | table_datum = [] 175 | for key in columns.keys(): 176 | table_datum.append(datum.values[key]) 177 | table_data.append(table_datum) 178 | 179 | self.data = table_data 180 | 181 | 182 | class Map: 183 | def __init__(self, longitude, latitude, zoom=None): 184 | self.longitude = longitude 185 | self.latitude = latitude 186 | if zoom is None: 187 | self.zoom = 13 188 | else: 189 | self.zoom = zoom 190 | self.openStreetMapLink = 'https://www.openstreetmap.org/#map=%s/%s/%s' % \ 191 | (zoom, latitude, longitude) 192 | 193 | 194 | class RssEntity: 195 | def __init__(self, title, description, link): 196 | self.title = title 197 | self.description = description 198 | self.link = link 199 | 200 | -------------------------------------------------------------------------------- /python_wrapper/susi_python/main.py: -------------------------------------------------------------------------------- 1 | import json 2 | import geocoder 3 | import requests 4 | import time 5 | import os 6 | 7 | from .response_parser import * 8 | from uuid import getnode as get_mac 9 | 10 | api_endpoint = 'https://api.susi.ai' 11 | 12 | access_token = None 13 | location = {'latitude': None, 'longitude': None, 'country_name': None, 'country_code': None} 14 | 15 | 16 | def check_local_server(): 17 | test_params = { 18 | 'q': 'Hello', 19 | 'timezoneOffset': int(time.timezone / 60) 20 | } 21 | try: 22 | chat_url = 'http://localhost:4000/susi/chat.json' 23 | if (requests.get(chat_url, test_params)): 24 | print('connected to local server') 25 | global api_endpoint 26 | api_endpoint = 'http://localhost:4000' 27 | except requests.exceptions.ConnectionError: 28 | print('local server is down') 29 | 30 | 31 | check_local_server() 32 | 33 | 34 | def use_api_endpoint(url): 35 | global api_endpoint 36 | api_endpoint = url 37 | 38 | 39 | def update_location(latitude, longitude, country_name, country_code): 40 | global location 41 | location['latitude'] = latitude 42 | location['longitude'] = longitude 43 | location['country_name'] = country_name 44 | location['country_code'] = country_code 45 | 46 | 47 | def query(query_string): 48 | params = { 49 | 'q': query_string, 50 | 'timezoneOffset': int(time.timezone/60), 51 | 'device_type': 'Smart Speaker' 52 | } 53 | if access_token is not None: 54 | params['access_token'] = access_token 55 | 56 | if location['latitude'] is not None and location['longitude'] is not None: 57 | params['latitude'] = location['latitude'] 58 | params['longitude'] = location['longitude'] 59 | 60 | if location['country_name'] is not None and location['country_code'] is not None: 61 | params['country_name'] = location['country_name'] 62 | params['country_code'] = location['country_code'] 63 | 64 | global api_endpoint 65 | chat_url = api_endpoint + "/susi/chat.json" 66 | try: 67 | api_response = requests.get(chat_url, params) 68 | except requests.exceptions.ConnectionError: 69 | if api_endpoint == 'http://localhost:4000' | api_endpoint == 'https://localhost:4000': 70 | api_endpoint = 'https://api.susi.ai' 71 | api_response = requests.get(chat_url, params) 72 | elif api_endpoint == 'http://api.susi.ai' | api_endpoint == 'https://api.susi.ai': 73 | api_endpoint = 'http://localhost:4000' 74 | api_response = requests.get(chat_url, params) 75 | 76 | response_json = api_response.json() 77 | parsed_res = get_query_response(response_json) 78 | return parsed_res 79 | 80 | 81 | def generate_result(response): 82 | result = dict() 83 | actions = response.answer.actions 84 | data = response.answer.data 85 | 86 | print(actions) 87 | 88 | for action in actions: 89 | if isinstance(action, AnswerAction): 90 | print(action) 91 | result['answer'] = action.expression 92 | elif isinstance(action, AudioAction): 93 | result['identifier'] = action.identifier 94 | elif isinstance(action, TableAction): 95 | result['table'] = Table(action.columns, data) 96 | elif isinstance(action, MapAction): 97 | result['map'] = Map(action.longitude, action.latitude, action.zoom) 98 | elif isinstance(action, AnchorAction): 99 | result['anchor'] = action 100 | elif isinstance(action, VideoAction): 101 | result['identifier'] = 'ytd-' + action.identifier 102 | elif isinstance(action, VolumeAction): 103 | result['volume'] = action.volume 104 | elif isinstance(action, RssAction): 105 | entities = get_rss_entities(data) 106 | count = action.count 107 | result['rss'] = {'entities': entities, 'count': count} 108 | elif isinstance(action, StopAction): 109 | result['stop'] = action 110 | break 111 | elif isinstance(action, MediaAction): 112 | result['media_action'] = action.type 113 | 114 | return result 115 | 116 | 117 | def ask(query_string): 118 | response = query(query_string) 119 | return generate_result(response) 120 | 121 | 122 | def answer_from_json(json_response): 123 | response_dict = json.loads(json_response) 124 | query_response = get_query_response(response_dict) 125 | return generate_result(query_response) 126 | 127 | 128 | def get_rss_entities(data): 129 | entities = [] 130 | for datum in data: 131 | values = datum.values 132 | entity = RssEntity( 133 | title=values['title'], 134 | description=values['description'], 135 | link=values['link'] 136 | ) 137 | entities.append(entity) 138 | return entities 139 | 140 | def add_device(access_token, room_name): 141 | 142 | get_device_info = api_endpoint + '/aaa/listUserSettings.json?' 143 | add_device_url = api_endpoint + '/aaa/addNewDevice.json?' 144 | mac = get_mac() 145 | macid = ':'.join(("%012X"%mac)[i:i+2] for i in range(0,12,2)) 146 | 147 | param1 = { 148 | 'access_token':access_token 149 | } 150 | 151 | # print(access_token) 152 | 153 | if access_token is not None: 154 | device_info_response = requests.get(get_device_info,param1) 155 | device_info = device_info_response.json() 156 | 157 | # print(device_info) 158 | 159 | if device_info is not None: 160 | device = device_info['devices'] # list of existing mac ids 161 | print(device) 162 | session = device_info['session'] # session info 163 | identity = session['identity'] 164 | name = identity['name'] 165 | curr_location = geocoder.ip('me') 166 | params2 = { 167 | 'macid': macid, 168 | 'name': name, 169 | 'room': str(room_name), 170 | 'latitude': curr_location.lat, 171 | 'longitude': curr_location.lng, 172 | 'access_token': access_token 173 | } 174 | 175 | for dev in device: 176 | if dev == macid: 177 | print('Device already configured') 178 | return 179 | else : 180 | adding_device = requests.post(add_device_url, params2) 181 | print(adding_device.url) 182 | 183 | def sign_in(email, password, room_name=None): 184 | global access_token 185 | params = { 186 | 'login': email, 187 | 'password': password 188 | } 189 | sign_in_url = api_endpoint + '/aaa/login.json?type=access-token' 190 | api_response = requests.get(sign_in_url, params) 191 | 192 | if api_response.status_code == 200: 193 | response_dict = api_response.json() 194 | parsed_response = get_sign_in_response(response_dict) 195 | access_token = parsed_response.access_token 196 | # print(access_token) 197 | if access_token is not None: 198 | add_device(access_token, room_name) 199 | else: 200 | access_token = None 201 | 202 | def sign_up(email, password): 203 | params = { 204 | 'signup': email, 205 | 'password': password 206 | } 207 | sign_up_url = api_endpoint + '/aaa/signup.json' 208 | api_response = requests.get(sign_up_url, params) 209 | parsed_dict = api_response.json() 210 | return get_sign_up_response(parsed_dict) 211 | 212 | 213 | def forgot_password(email): 214 | params = { 215 | 'forgotemail': email 216 | } 217 | forgot_password_url = api_endpoint + '/aaa/recoverpassword.json' 218 | api_response = requests.get(forgot_password_url, params) 219 | parsed_dict = api_response.json() 220 | return get_forgot_password_response(parsed_dict) 221 | 222 | 223 | def get_previous_responses(): 224 | memory_url = api_endpoint + '/susi/memory.json' 225 | api_response = requests.get(memory_url) 226 | parsed_dict = api_response.json() 227 | return get_memory_responses(parsed_dict) 228 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------