├── .gitignore ├── .gitlab-ci.yml ├── Algorithmia ├── CLI.py ├── __init__.py ├── __main__.py ├── acl.py ├── algo_response.py ├── algorithm.py ├── async_response.py ├── client.py ├── data.py ├── datadirectory.py ├── datafile.py ├── errors.py ├── handler.py ├── insights.py └── util.py ├── CODEOWNERS ├── LICENSE ├── README.md ├── Test ├── __init__.py ├── api │ ├── __init__.py │ ├── app.py │ └── self_signed_app.py ├── conftest.py ├── regular │ ├── CLI_test.py │ ├── __init__.py │ ├── acl_test.py │ ├── algo_failure_test.py │ ├── algo_test.py │ ├── client_test.py │ ├── datadirectory_test.py │ ├── datafile_test.py │ └── util_test.py ├── resources │ ├── cert.cert │ ├── cert.key │ ├── manifests │ │ └── example_manifest.json │ └── zip_directory │ │ ├── root.json │ │ └── subdirectory │ │ ├── __init__.py │ │ └── subdir.json └── self_signed │ ├── __init__.py │ ├── algo_failure_test.py │ └── algo_test.py ├── requirements.txt ├── requirements27.txt ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | #Eclipse 2 | .project 3 | venv 4 | venv_win 5 | TestFiles 6 | test.txt 7 | 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | env/ 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *,cover 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | 60 | # Sphinx documentation 61 | docs/_build/ 62 | 63 | # PyBuilder 64 | target/ 65 | .idea 66 | 67 | #pypirc 68 | pypirc 69 | 70 | #Visual Studio Code 71 | .vscode 72 | venv 73 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - test 3 | - python27 4 | - python37 5 | - python38 6 | - python39 7 | - deploy 8 | 9 | variables: 10 | PROJECT_NAME: algorithmia-python 11 | DOCKER_HOST: tcp://docker:2375/ 12 | DOCKER_DRIVER: overlay2 13 | RUNNING_ON_BUILD_SERVER: "true" 14 | TWINE_USERNAME: __token__ 15 | CLIENT_VERSION: $CI_COMMIT_TAG 16 | 17 | 18 | deploy: 19 | stage: deploy 20 | only: 21 | - tags 22 | image: python:3.7 23 | script: 24 | # - export CLIENT_VERSION=$(git describe --abbrev=0 2>/dev/null || echo '') 25 | - echo $CLIENT_VERSION 26 | - python -m pip install --upgrade pip 27 | - pip install wheel twine setuptools 28 | - python setup.py sdist bdist_wheel 29 | - python3 setup.py sdist bdist_wheel --universal 30 | - twine upload -r pypi dist/* 31 | 32 | test: 33 | stage: test 34 | script: 35 | - if [ -z $ALGORITHMIA_API_KEY ]; then echo "Algorithmia API key not defined"; exit 1; fi 36 | 37 | test:pylint:36: 38 | image: python:3.6 39 | script: 40 | - pip install pylint --quiet 41 | - pylint --ignored-classes=_socketobject *.py || echo $? 42 | 43 | test:pytest:36: 44 | image: python:3.6 45 | script: 46 | - pip install -r requirements.txt --quiet 47 | - pip install pytest --quiet 48 | - pytest 49 | 50 | python27: 51 | stage: python27 52 | image: python:2.7 53 | script: 54 | - pip install -r requirements27.txt --quiet 55 | - pip install pytest --quiet 56 | - pytest 57 | 58 | python37: 59 | stage: python37 60 | image: python:3.7 61 | script: 62 | - pip install -r requirements.txt --quiet 63 | - pip install pytest --quiet 64 | - pytest 65 | 66 | python38: 67 | stage: python38 68 | image: python:3.8 69 | script: 70 | - pip install -r requirements.txt --quiet 71 | - pip install pytest --quiet 72 | - pytest 73 | 74 | python39: 75 | stage: python39 76 | image: python:3.9 77 | script: 78 | - pip install -r requirements.txt --quiet 79 | - pip install pytest --quiet 80 | - pytest 81 | -------------------------------------------------------------------------------- /Algorithmia/CLI.py: -------------------------------------------------------------------------------- 1 | import Algorithmia 2 | import os 3 | from Algorithmia.errors import DataApiError, AlgorithmException 4 | from Algorithmia.algo_response import AlgoResponse 5 | import json, re, requests, six 6 | import toml 7 | import shutil 8 | 9 | class CLI: 10 | def __init__(self): 11 | self.client = Algorithmia.client() 12 | # algo auth 13 | def auth(self, apiaddress, apikey="", cacert="", profile="default", bearer=""): 14 | 15 | # store api key in local config file and read from it each time a client needs to be created 16 | key = self.getconfigfile() 17 | config = toml.load(key) 18 | 19 | if ('profiles' in config.keys()): 20 | if (profile in config['profiles'].keys()): 21 | config['profiles'][profile]['api_key'] = apikey 22 | config['profiles'][profile]['api_server'] = apiaddress 23 | config['profiles'][profile]['ca_cert'] = cacert 24 | config['profiles'][profile]['bearer_token'] = bearer 25 | 26 | else: 27 | config['profiles'][profile] = {'api_key':apikey,'api_server':apiaddress,'ca_cert':cacert,'bearer_token':bearer} 28 | else: 29 | config['profiles'] = {profile:{'api_key':apikey,'api_server':apiaddress,'ca_cert':cacert,'bearer_token':bearer }} 30 | 31 | with open(key, "w") as key: 32 | toml.dump(config,key) 33 | 34 | self.ls(path = None,client = CLI().getClient(profile)) 35 | 36 | # algo run run the the specified algo 37 | def runalgo(self, options, client): 38 | algo_input = None 39 | 40 | algo = client.algo(options.algo) 41 | url = client.apiAddress + algo.url 42 | result = None 43 | content = None 44 | 45 | algo.set_options(timeout=options.timeout, stdout=options.debug) 46 | 47 | # handle input type flags 48 | if (options.data != None): 49 | # data 50 | algo_input = options.data 51 | 52 | result = algo.pipe(algo_input) 53 | 54 | elif (options.text != None): 55 | # text 56 | algo_input = options.text 57 | key = self.getAPIkey(options.profile) 58 | content = 'text/plain' 59 | algo_input = algo_input.encode('utf-8') 60 | 61 | elif (options.json != None): 62 | # json 63 | algo_input = options.json 64 | key = self.getAPIkey(options.profile) 65 | content = 'application/json' 66 | 67 | elif (options.binary != None): 68 | # binary 69 | algo_input = bytes(options.binary) 70 | 71 | key = self.getAPIkey(options.profile) 72 | content = 'application/octet-stream' 73 | 74 | elif (options.data_file != None): 75 | # data file 76 | algo_input = open(options.data_file, "r").read() 77 | result = algo.pipe(algo_input) 78 | 79 | elif (options.text_file != None): 80 | # text file 81 | algo_input = open(options.text_file, "r").read() 82 | key = self.getAPIkey(options.profile) 83 | content = 'text/plain' 84 | algo_input = algo_input.encode('utf-8') 85 | 86 | elif (options.json_file != None): 87 | # json file 88 | # read json file and run algo with that input bypassing the auto detection of input type in pipe 89 | with open(options.json_file, "r") as f: 90 | algo_input = f.read() 91 | key = self.getAPIkey(options.profile) 92 | content = 'application/json' 93 | algo_input = json.dumps(algo_input).encode('utf-8') 94 | 95 | 96 | elif (options.binary_file != None): 97 | # binary file 98 | with open(options.binary_file, "rb") as f: 99 | algo_input = bytes(f.read()) 100 | key = self.getAPIkey(options.profile) 101 | content = 'application/octet-stream' 102 | 103 | 104 | else: 105 | output = "no valid input detected" 106 | 107 | if (content != None): 108 | result = AlgoResponse.create_algo_response(requests.post(url, data=algo_input, 109 | headers={'Authorization': key, 110 | 'Content-Type': content}, 111 | params=algo.query_parameters).json()) 112 | 113 | if (result != None): 114 | output = result.result 115 | 116 | # handle output flags 117 | 118 | # output to file if there is an output file specified 119 | if (options.output != None): 120 | outputFile = options.output 121 | try: 122 | if isinstance(result.result, bytearray) or isinstance(result.result, bytes): 123 | out = open(outputFile, "wb") 124 | out.write(result.result) 125 | out.close() 126 | else: 127 | out = open(outputFile, "w") 128 | out.write(result.result) 129 | out.close() 130 | output = "" 131 | 132 | except Exception as error: 133 | print(error) 134 | 135 | return output 136 | 137 | # algo mkdir 138 | def mkdir(self, path, client): 139 | # make a dir in data collection 140 | newDir = client.dir(path) 141 | 142 | if newDir.exists() is False: 143 | newDir.create() 144 | 145 | # algo rmdir 146 | def rmdir(self, path, client, force=False): 147 | # remove a dir in data collection 148 | 149 | Dir = client.dir(path) 150 | 151 | try: 152 | if Dir.exists(): 153 | Dir.delete(force) 154 | except Algorithmia.errors.DataApiError as e: 155 | print(e) 156 | 157 | def rm(self, path, client): 158 | 159 | # for f in path 160 | file = client.file(path) 161 | try: 162 | if file.exists(): 163 | file.delete() 164 | except Algorithmia.errors.DataApiError as e: 165 | print(e) 166 | 167 | # algo ls 168 | def ls(self, path, client, longlist=False): 169 | # by default list user's hosted data 170 | listing = "" 171 | if path is None: 172 | path = "data://" 173 | 174 | file = path.split('/') 175 | if file[-1] != '': 176 | # path is a file, list parent 177 | directory = path[:-len(file[-1])] 178 | f = client.dir(directory) 179 | 180 | response = client.getHelper(f.url, **{}) 181 | if response.status_code != 200: 182 | raise DataApiError("failed to get file info: " + str(response.content)) 183 | 184 | responseContent = response.content 185 | if isinstance(responseContent, six.binary_type): 186 | responseContent = responseContent.decode() 187 | 188 | content = json.loads(responseContent) 189 | 190 | if 'files' in content: 191 | f = client.file(path) 192 | for file_info in content['files']: 193 | if file_info['filename'] == file[-1]: 194 | f.set_attributes(file_info) 195 | 196 | if longlist: 197 | listing += f.last_modified.strftime("%Y-%m-%d %H:%M:%S") + ' ' 198 | listing += str(f.size) + ' ' 199 | listing += f.path + "\n" 200 | else: 201 | listing += f.path + "\n" 202 | else: 203 | # path is a directory 204 | if longlist: 205 | listingDir = client.dir(path) 206 | for f in listingDir.dirs(): 207 | listing += f.path + "/\n" 208 | for f in listingDir.files(): 209 | listing += f.last_modified.strftime("%Y-%m-%d %H:%M:%S") + ' ' 210 | listing += str(f.size) + ' ' 211 | listing += f.path + "\n" 212 | 213 | else: 214 | listingDir = client.dir(path) 215 | for f in listingDir.dirs(): 216 | listing += f.path + "/\n" 217 | for f in listingDir.files(): 218 | listing += f.path + "\n" 219 | 220 | return listing 221 | 222 | # algo cat 223 | def cat(self, path, client): 224 | result = "" 225 | for f in path: 226 | if '://' in f and not f.startswith("http"): 227 | if f[-1] == '*': 228 | path += ['data://' + file.path for file in client.dir(f[:len(f) - 2]).files()] 229 | else: 230 | file = client.file(f) 231 | 232 | if file.exists(): 233 | result += file.getString() 234 | else: 235 | result = "file does not exist " + f 236 | break 237 | else: 238 | print("operands must be a path to a remote data source data://") 239 | break 240 | 241 | return result 242 | 243 | # algo freeze 244 | def freezeAlgo(self, client, manifest_path="model_manifest.json"): 245 | client.freeze(manifest_path) 246 | 247 | # algo cp 248 | def cp(self, src, dest, client): 249 | 250 | if (src is None or dest is None): 251 | print("expected algo cp ") 252 | else: 253 | 254 | destLocation = client.file(dest) 255 | for f in src: 256 | 257 | # if dest is a directory apend the src name 258 | # if there are multiple src files only the final one will be copied if dest is not a directory 259 | destPath = dest 260 | 261 | path = dest.split('/') 262 | 263 | if (os.path.isdir(dest) or client.dir(dest).exists() and len(path) <= 5): 264 | if (dest[-1] == '/' and path[-1] == ''): 265 | destPath += client.file(f).getName() 266 | elif (len(path) == 4 or "data://" not in dest): 267 | destPath += '/' + client.file(f).getName() 268 | 269 | if (f[-1] == '*'): 270 | src += ['data://' + file.path for file in client.dir(f[:len(f) - 2]).files()] 271 | 272 | # if src is local and dest is remote 273 | elif ("data://" not in f and "data://" in dest): 274 | client.file(destPath).putFile(f) 275 | 276 | # if src and dest are remote 277 | elif ("data://" in f and "data://" in dest): 278 | file = client.file(f).getFile() 279 | filename = file.name 280 | file.close() 281 | 282 | client.file(destPath).putFile(filename) 283 | 284 | # if src is remote and dest is local 285 | elif ("data://" in f and "data://" not in dest): 286 | file = client.file(f).getFile() 287 | filename = file.name 288 | file.close() 289 | shutil.move(filename, destPath) 290 | else: 291 | print("at least one of the operands must be a path to a remote data source data://") 292 | 293 | def get_environment_by_language(self, language, client): 294 | response = client.get_environment(language) 295 | if "error" in response: 296 | return json.dumps(response) 297 | return json.dumps(response['environments'], indent=1) 298 | 299 | def list_languages(self, client): 300 | response = client.get_supported_languages() 301 | table = [] 302 | if "error" not in response: 303 | table.append("{:<25} {:<35}".format('Name', 'Description')) 304 | for lang in response: 305 | table.append("{:<25} {:<35}".format(lang['name'], lang['display_name'])) 306 | else: 307 | table.append(json.dumps(response)) 308 | return table 309 | 310 | def getBuildLogs(self, user, algo, client): 311 | api_response = client.algo(user + '/' + algo).get_builds() 312 | return json.dumps(api_response['results'], indent=1) 313 | 314 | 315 | def getconfigfile(self): 316 | if (os.name == "posix"): 317 | # if!windows 318 | # ~/.algorithmia/config 319 | # create the api key file if it does not exist 320 | keyPath = os.environ['HOME'] + "/.algorithmia/" 321 | 322 | elif (os.name == "nt"): 323 | # ifwindows 324 | # %LOCALAPPDATA%\Algorithmia\config 325 | # create the api key file if it does not exist 326 | keyPath = os.path.expandvars("%LOCALAPPDATA%\\Algorithmia\\") 327 | 328 | keyFile = "config" 329 | 330 | if (not os.path.exists(keyPath)): 331 | os.mkdir(keyPath) 332 | 333 | if (not os.path.exists(keyPath + keyFile)): 334 | with open(keyPath + keyFile, "w") as file: 335 | file.write("[profiles]\n") 336 | file.write("[profiles.default]\n") 337 | file.write("api_key = ''\n") 338 | file.write("api_server = ''\n") 339 | file.write("ca_cert = ''\n") 340 | file.write("bearer_token = ''\n") 341 | 342 | key = keyPath + keyFile 343 | 344 | return key 345 | 346 | def get_template(self, envid, dest, client): 347 | response = client.get_template(envid, dest) 348 | return response 349 | 350 | def getAPIkey(self, profile): 351 | key = self.getconfigfile() 352 | config_dict = toml.load(key) 353 | if 'profiles' in config_dict and profile in config_dict['profiles'] and \ 354 | config_dict['profiles'][profile]['api_key'] != "": 355 | return config_dict['profiles'][profile]['api_key'] 356 | else: 357 | return None 358 | 359 | def getBearerToken(self,profile): 360 | key = self.getconfigfile() 361 | config_dict = toml.load(key) 362 | if 'profiles' in config_dict and profile in config_dict['profiles'] and \ 363 | config_dict['profiles'][profile]['bearer_token'] != "": 364 | return config_dict['profiles'][profile]['bearer_token'] 365 | else: 366 | return None 367 | 368 | 369 | def getAPIaddress(self, profile): 370 | key = self.getconfigfile() 371 | config_dict = toml.load(key) 372 | 373 | if config_dict['profiles'][profile]['api_server'] != "": 374 | return config_dict['profiles'][profile]['api_server'] 375 | else: 376 | return None 377 | 378 | def getCert(self, profile): 379 | key = self.getconfigfile() 380 | config_dict = toml.load(key) 381 | if 'profiles' in config_dict and profile in config_dict['profiles'] and \ 382 | config_dict['profiles'][profile]['ca_cert'] != "": 383 | return config_dict['profiles'][profile]['ca_cert'] 384 | else: 385 | return None 386 | 387 | def getClient(self,profile): 388 | apiAddress = self.getAPIaddress(profile) 389 | apiKey = self.getAPIkey(profile) 390 | caCert = self.getCert(profile) 391 | bearer = None 392 | 393 | if apiKey is None: 394 | bearer = self.getBearerToken(profile) 395 | 396 | return Algorithmia.client(api_key=apiKey,api_address=apiAddress,ca_cert=caCert,bearer_token = bearer) 397 | -------------------------------------------------------------------------------- /Algorithmia/__init__.py: -------------------------------------------------------------------------------- 1 | 'Algorithmia API Client (python)' 2 | 3 | from Algorithmia.client import Client 4 | from Algorithmia.handler import Handler 5 | import sys 6 | 7 | import sys 8 | if sys.version_info[0] >= 3: 9 | from adk import ADK 10 | import os 11 | 12 | apiKey = None 13 | apiAddress = None 14 | 15 | # Get reference to an algorithm using a default client 16 | def algo(algoRef): 17 | # Return algorithm reference using default client 18 | return getDefaultClient().algo(algoRef) 19 | 20 | def file(dataUrl): 21 | return getDefaultClient().file(dataUrl) 22 | 23 | def dir(dataUrl): 24 | return getDefaultClient().dir(dataUrl) 25 | 26 | def client(api_key=None, api_address=None, ca_cert=None, bearer_token=None): 27 | return Client(api_key, api_address, ca_cert, bearer_token) 28 | 29 | def handler(apply_func, load_func=lambda: None): 30 | return Handler(apply_func, load_func) 31 | 32 | # The default client to use, assuming the user does not want to construct their own 33 | defaultClient = None 34 | 35 | # Used internally to get default client 36 | def getDefaultClient(): 37 | global defaultClient 38 | # Check for default client, and ensure default API key has not changed 39 | if defaultClient is None or defaultClient.apiKey is not apiKey: 40 | # Construct default client 41 | defaultClient = Client(apiKey) 42 | return defaultClient 43 | 44 | # Used internally to get default api client 45 | def getApiAddress(): 46 | global apiAddress 47 | if apiAddress is not None: 48 | # First check for user setting Algorithmia.apiAddress = "XXX" 49 | return apiAddress 50 | elif 'ALGORITHMIA_API' in os.environ: 51 | # Then check for system environment variable 52 | return os.environ['ALGORITHMIA_API'] 53 | else: 54 | # Else return default 55 | return "https://api.algorithmia.com" 56 | -------------------------------------------------------------------------------- /Algorithmia/__main__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import json 4 | sys.path = ['../'] + sys.path 5 | import Algorithmia 6 | import six 7 | from Algorithmia.CLI import CLI 8 | import argparse 9 | import re 10 | 11 | #bind input to raw input 12 | try: 13 | input = raw_input 14 | except NameError: 15 | pass 16 | #CLI app to allow a user to run algorithms and manage data collections 17 | 18 | usage = """CLI for interaction with Algorithmia\n 19 | Usage:\n 20 | algo [] [options] [...]\n 21 | algo [] [--help | --version]\n\n 22 | 23 | General commands include:\n 24 | auth configure authentication\n\n 25 | 26 | Algorithm commands include:\n 27 | run Runs an algorithm\n 28 | clone Clones an algorithm source\n\n 29 | 30 | Data commands include:\n 31 | ls list the contents of a data directory\n 32 | mkdir create a data directory\n 33 | rmdir remove a data directory\n 34 | rm remove a file from a data directory\n 35 | cp copy file(s) to or from a data directory\n 36 | cat concatenate and print file(s) in a data directory\n\n 37 | 38 | Global options:\n 39 | --help\n 40 | --profile \n\n 41 | """ 42 | 43 | def main(): 44 | parser = argparse.ArgumentParser('algo', description = "algo [] [options] [...] [--help] [--profile]") 45 | 46 | subparsers = parser.add_subparsers(help = 'sub cmd',dest = 'cmd') 47 | 48 | parser_auth = subparsers.add_parser('auth', help = 'save api key and api address for profile') 49 | parser_auth.add_argument('--profile', action = 'store', type = str, default = 'default') 50 | 51 | parser_clone = subparsers.add_parser('clone', help = 'clone clone the algorithm repository') 52 | parser_clone.add_argument('algo') 53 | parser_clone.add_argument('--profile', action = 'store', type = str, default = 'default') 54 | 55 | #parse options for the run command 56 | parser_run = subparsers.add_parser('run', help = 'algo run [input options] [output options] run an algorithm') 57 | 58 | parser_run.add_argument('algo') 59 | parser_run.add_argument('-d','--data', action = 'store', help = 'detect input type', default = None) 60 | parser_run.add_argument('-t','--text', action = 'store', help = 'treat input as text', default = None) 61 | parser_run.add_argument('-j','--json', action = 'store', help = 'treat input as json data', default = None) 62 | parser_run.add_argument('-b','--binary', action = 'store', help = 'treat input as binary data', default = None) 63 | parser_run.add_argument('-D','--data-file', action = 'store', help = 'specify a path to an input file', default = None) 64 | parser_run.add_argument('-T','--text-file', action = 'store', help = 'specify a path to a text file', default = None) 65 | parser_run.add_argument('-J','--json-file', action = 'store', help = 'specify a path to a json file', default = None) 66 | parser_run.add_argument('-B','--binary-file', action = 'store', help = 'specify a path to a binary file', default = None) 67 | parser_run.add_argument('--timeout', action = 'store',type = int, default = 300, help = 'specify a timeout (seconds)') 68 | parser_run.add_argument('--debug', action = 'store_true', help = 'print the stdout from the algo ') 69 | parser_run.add_argument('--profile', action = 'store', type = str, default = 'default') 70 | parser_run.add_argument('-o', '--output', action = 'store', default = None, type = str) 71 | 72 | #subparser for ls 73 | parser_ls = subparsers.add_parser('ls', help = 'ls [-l] [directory] list the contents of a directory', ) 74 | 75 | parser_ls.add_argument('-l', '--long', action = 'store_true') 76 | parser_ls.add_argument('path', nargs = '?', default = None) 77 | parser_ls.add_argument('--profile', action = 'store', type = str, default = 'default') 78 | 79 | #subparser for rm 80 | parser_rm = subparsers.add_parser('rm', help = 'rm remove a file', ) 81 | 82 | parser_rm.add_argument('path', nargs = '?', default = None) 83 | parser_rm.add_argument('--profile', action = 'store', type = str, default = 'default') 84 | 85 | #subparser for mkdir 86 | parser_mkdir = subparsers.add_parser('mkdir', help = 'mkdir create a directory') 87 | 88 | parser_mkdir.add_argument('path', help = 'directory to create') 89 | parser_mkdir.add_argument('--profile', action = 'store', type = str, default = 'default') 90 | 91 | #subparser for rmdir 92 | parser_rmdir = subparsers.add_parser('rmdir', help = 'rmdir [-f] remove a directory') 93 | 94 | parser_rmdir.add_argument('-f', '--force', action = 'store_true', help = 'force directory removal if it is not empty') 95 | parser_rmdir.add_argument('path', help = 'directory to remove') 96 | parser_rmdir.add_argument('--profile', action = 'store', type = str, default = 'default') 97 | 98 | #subparser for cp 99 | parser_cp = subparsers.add_parser('cp', help = 'cp copy file(s) to the destination',) 100 | 101 | parser_cp.add_argument('src', nargs = '*', type = str, help = 'file(s) to be copied') 102 | parser_cp.add_argument('dest', help = 'destination for file(s) to be copied to') 103 | parser_cp.add_argument('--profile', action = 'store', type = str, default = 'default') 104 | 105 | #sub parser for cat 106 | parser_cat = subparsers.add_parser('cat', help = 'cat concatenate and print file(s)') 107 | 108 | parser_cat.add_argument('path', nargs = '*', help = 'file(s) to concatenate and print') 109 | parser_cat.add_argument('--profile', action = 'store', type = str, default = 'default') 110 | 111 | #sub parser for getting environment template 112 | parser_template = subparsers.add_parser('template', help='template downloads an environment template to the destination') 113 | parser_template.add_argument('envid',help='environment specification id') 114 | parser_template.add_argument('dest',help='destination for template download') 115 | 116 | #sub parser for getting environment by language name 117 | parser_env = subparsers.add_parser('environment', help = 'environment gets environment info by language') 118 | parser_env.add_argument('language', help='supported language name') 119 | 120 | 121 | #sub parser for listing languages 122 | subparsers.add_parser('languages', help = 'lists supported languages') 123 | 124 | #sub parser for builds 125 | parser_builds = subparsers.add_parser('builds', help = 'builds gets build logs for algorithm') 126 | parser_builds.add_argument('user') 127 | parser_builds.add_argument('algo',help='algorithm name') 128 | 129 | #sub parser for help 130 | 131 | subparsers.add_parser('help') 132 | parser.add_argument('--profile', action = 'store', type = str, default = 'default') 133 | 134 | #sub parser for freeze 135 | subparsers.add_parser('freeze', help="freezes a model_manifest.json file into a model_manifest.json.freeze") 136 | 137 | args = parser.parse_args() 138 | 139 | 140 | #run auth before trying to create a client 141 | if args.cmd == 'auth': 142 | 143 | print("Configuring authentication for profile: " + args.profile) 144 | 145 | APIaddress = input("enter API address [https://api.algorithmia.com]: ") 146 | APIkey = input("enter API key: ") 147 | CACert = input('(optional) enter path to custom CA certificate: ') 148 | 149 | if APIaddress == "" or not APIaddress.startswith("https://api."): 150 | print("invalid API address") 151 | else: 152 | if len(APIkey) == 28 and APIkey.startswith("sim"): 153 | CLI().auth(apikey=APIkey, apiaddress=APIaddress, cacert=CACert, profile=args.profile) 154 | else: 155 | jwt = re.compile(r"^([a-zA-Z0-9_=]+)\.([a-zA-Z0-9_=]+)\.([a-zA-Z0-9_\-\+\/=]*)") 156 | Bearer = input("enter JWT token: ") 157 | if jwt.match(Bearer): 158 | CLI().auth(apikey=APIkey, bearer=Bearer, apiaddress=APIaddress, cacert=CACert, profile=args.profile) 159 | else: 160 | print("invalid authentication") 161 | 162 | 163 | 164 | if args.cmd == 'help': 165 | parser.parse_args(['-h']) 166 | 167 | #create a client with the appropreate api address and key 168 | client = CLI().getClient(args.profile) 169 | 170 | if args.cmd == 'run': 171 | 172 | print(CLI().runalgo(args, client)) 173 | 174 | elif args.cmd == 'clone': 175 | 176 | algo_name = args.algo 177 | 178 | print("cloning src for " + algo_name) 179 | 180 | if CLI().getAPIaddress(args.profile) == None: 181 | exitcode = os.system("git clone https://git.algorithmia.com/git/"+algo_name+".git") 182 | else: 183 | #replace https://api. with https://git. 184 | exitcode = os.system("git clone " + (CLI().getAPIaddress(args.profile).replace("//api.", "//git."))+"/git/"+algo_name+".git") 185 | 186 | if exitcode != 0: 187 | print("failed to clone\nis git installed?") 188 | 189 | elif args.cmd == 'ls': 190 | print(CLI().ls(args.path, client, args.long)) 191 | 192 | elif args.cmd == 'mkdir': 193 | CLI().mkdir(args.path, client) 194 | 195 | elif args.cmd == 'rmdir': 196 | CLI().rmdir(args.path, client, args.force) 197 | 198 | elif args.cmd == 'rm': 199 | CLI().rm(args.path, client) 200 | 201 | elif args.cmd == 'cp': 202 | CLI().cp(args.src,args.dest, client) 203 | 204 | elif args.cmd == 'cat': 205 | print(CLI().cat(args.path, client)) 206 | 207 | elif args.cmd == 'languages': 208 | response = CLI().list_languages(client) 209 | for line in response: 210 | print(line) 211 | 212 | elif args.cmd == 'template': 213 | CLI().get_template(args.envid,args.dest,client) 214 | 215 | elif args.cmd == 'environment': 216 | response = CLI().get_environment_by_language(args.language,client) 217 | print(response) 218 | 219 | elif args.cmd == 'builds': 220 | print(CLI().getBuildLogs(args.user, args.algo, client)) 221 | 222 | elif args.cmd == "freeze": 223 | print(CLI().freezeAlgo(client)) 224 | 225 | else: 226 | parser.parse_args(['-h']) 227 | 228 | 229 | 230 | 231 | 232 | 233 | if __name__ == '__main__': 234 | #main() 235 | main() -------------------------------------------------------------------------------- /Algorithmia/acl.py: -------------------------------------------------------------------------------- 1 | class Acl(object): 2 | def __init__(self, read_acl): 3 | self.read_acl = read_acl 4 | 5 | @staticmethod 6 | def from_acl_response(acl_response): 7 | '''Takes JSON response from API and converts to ACL object''' 8 | if 'read' in acl_response: 9 | read_acl = AclType.from_acl_response(acl_response['read']) 10 | return Acl(read_acl) 11 | else: 12 | raise ValueError('Response does not contain read ACL') 13 | 14 | def to_api_param(self): 15 | read_acl_string = self.read_acl.acl_string 16 | if read_acl_string is None: 17 | return {'read':[]} 18 | return {'read':[read_acl_string]} 19 | 20 | class AclInner(object): 21 | def __init__(self, pseudonym, acl_string): 22 | self.pseudonym = pseudonym 23 | self.acl_string = acl_string 24 | 25 | def __repr__(self): 26 | return 'AclType(pseudonym=%s,acl_string=%s)' % (self.pseudonym, self.acl_string) 27 | 28 | class AclType(object): 29 | public = AclInner('public','user://*') 30 | my_algos = AclInner('my_algos','algo://.my/*') 31 | private = AclInner('private',None) # Really is an empty list 32 | default = my_algos 33 | 34 | types = (public, my_algos, private) 35 | 36 | @staticmethod 37 | def from_acl_response(acl_list): 38 | if len(acl_list) == 0: 39 | return AclType.private 40 | else: 41 | acl_string = acl_list[0] 42 | for t in AclType.types: 43 | if t.acl_string == acl_string: 44 | return t 45 | else: 46 | raise ValueError('Invalid acl string %s' % (acl_list[0])) 47 | 48 | class ReadAcl(object): 49 | public = Acl(AclType.public) 50 | private = Acl(AclType.private) 51 | my_algos = Acl(AclType.my_algos) 52 | -------------------------------------------------------------------------------- /Algorithmia/algo_response.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from Algorithmia.errors import raiseAlgoApiError 3 | from Algorithmia.async_response import AsyncResponse 4 | import sys 5 | 6 | 7 | class AlgoResponse(object): 8 | def __init__(self, result, metadata): 9 | self.result = result 10 | self.metadata = metadata 11 | 12 | def __unicode__(self): 13 | return 'AlgoResponse(result=%s,metadata=%s)' % (self.result, self.metadata) 14 | 15 | def __repr__(self): 16 | if sys.version_info[0] >= 3: 17 | return self.__unicode__() 18 | else: 19 | return self.__unicode__().encode('utf-8') 20 | 21 | @staticmethod 22 | def create_algo_response(response): 23 | 24 | # Check if request is async 25 | if 'async_protocol' in response and 'request_id' in response: 26 | return AsyncResponse(response) 27 | # Parse response JSON, if it's indeed JSON 28 | elif 'error' in response or 'metadata' not in response: 29 | # Failure 30 | raise raiseAlgoApiError(response) 31 | else: 32 | metadata = Metadata(response['metadata']) 33 | # Success, check content_type 34 | if response['metadata']['content_type'] == 'binary': 35 | # Decode Base64 encoded binary file 36 | return AlgoResponse(base64.b64decode(response['result']), metadata) 37 | elif response['metadata']['content_type'] == 'void': 38 | return AlgoResponse(None, metadata) 39 | else: 40 | return AlgoResponse(response['result'], metadata) 41 | 42 | class Metadata(object): 43 | def __init__(self, metadata): 44 | self.content_type = metadata['content_type'] 45 | self.duration = metadata['duration'] 46 | self.stdout = None 47 | if 'stdout' in metadata: 48 | self.stdout = metadata['stdout'] 49 | self.full_metadata = metadata 50 | 51 | def __getitem__(self, key): 52 | return self.__dict__[key] 53 | 54 | def __repr__(self): 55 | return "Metadata(content_type='%s',duration=%s,stdout=%s)" % (self.content_type, self.duration, self.stdout) 56 | -------------------------------------------------------------------------------- /Algorithmia/algorithm.py: -------------------------------------------------------------------------------- 1 | 'Algorithmia Algorithm API Client (python)' 2 | 3 | import json 4 | import re 5 | from Algorithmia.async_response import AsyncResponse 6 | from Algorithmia.algo_response import AlgoResponse 7 | from Algorithmia.errors import ApiError, ApiInternalError, raiseAlgoApiError, AlgorithmException 8 | from enum import Enum 9 | from algorithmia_api_client.rest import ApiException 10 | from algorithmia_api_client import CreateRequest, UpdateRequest, VersionRequest, Details, Settings, SettingsMandatory, \ 11 | SettingsPublish, \ 12 | CreateRequestVersionInfo, VersionInfo, VersionInfoPublish 13 | 14 | OutputType = Enum('OutputType', 'default raw void') 15 | 16 | 17 | class Algorithm(object): 18 | def __init__(self, client, algoRef): 19 | # Parse algoRef 20 | algoRegex = re.compile(r"(?:algo://|/|)(\w+/.+)") 21 | m = algoRegex.match(algoRef) 22 | if m is not None: 23 | self.client = client 24 | self.path = m.group(1) 25 | self.username = self.path.split("/")[0] 26 | self.algoname = self.path.split("/")[1] 27 | if len(self.path.split("/")) > 2: 28 | self.version = self.path.split("/")[2] 29 | self.url = '/v1/algo/' + self.path 30 | self.query_parameters = {} 31 | self.output_type = OutputType.default 32 | else: 33 | raise ValueError('Invalid algorithm URI: ' + algoRef) 34 | 35 | def set_options(self, timeout=300, stdout=False, output=OutputType.default, **query_parameters): 36 | self.query_parameters = {'timeout': timeout, 'stdout': stdout} 37 | self.output_type = output 38 | self.query_parameters.update(query_parameters) 39 | return self 40 | 41 | def get_algorithm_id(self): 42 | url = '/v1/algorithms/' + self.username + '/' + self.algoname 43 | print(url) 44 | api_response = self.client.getJsonHelper(url) 45 | if 'id' in api_response: 46 | return api_response['id'] 47 | else: 48 | raise Exception("field 'id' not found in response: ", api_response) 49 | 50 | 51 | def get_secrets(self): 52 | algorithm_id = self.get_algorithm_id() 53 | url = "/v1/algorithms/" + algorithm_id + "/secrets" 54 | api_response = self.client.getJsonHelper(url) 55 | return api_response 56 | 57 | 58 | def set_secret(self, short_name, secret_key, secret_value, description=None): 59 | algorithm_id = self.get_algorithm_id() 60 | url = "/v1/algorithms/" + algorithm_id + "/secrets" 61 | secret_providers = self.client.get_secret_providers() 62 | provider_id = secret_providers[0]['id'] 63 | 64 | create_parameters = { 65 | "owner_type": "algorithm", 66 | "owner_id": algorithm_id, 67 | "short_name": short_name, 68 | "provider_id": provider_id, 69 | "secret_key": secret_key, 70 | "secret_value": secret_value, 71 | } 72 | if description: 73 | create_parameters['description'] = description 74 | else: 75 | create_parameters['description'] = " " 76 | 77 | print(create_parameters) 78 | api_response = self.client.postJsonHelper(url, create_parameters, parse_response_as_json=True) 79 | return api_response 80 | 81 | 82 | # Create a new algorithm 83 | def create(self, details, settings, version_info=None, source=None, scmsCredentials=None): 84 | url = "/v1/algorithms/" + self.username 85 | create_parameters = {"name": self.algoname, "details": details, "settings": settings} 86 | if version_info: 87 | create_parameters['version_info'] = version_info 88 | if source: 89 | create_parameters['source'] = source 90 | if scmsCredentials: 91 | create_parameters['scmsCredentials'] = scmsCredentials 92 | 93 | api_response = self.client.postJsonHelper(url, create_parameters, parse_response_as_json=True) 94 | return api_response 95 | 96 | # Update the settings in an algorithm 97 | def update(self, details={}, settings={}, version_info={}, source={}, scmsCredentials={}): 98 | url = "/v1/algorithms/" + self.username + "/" + self.algoname 99 | update_parameters = {"details": details, "settings": settings, 100 | "version_info": version_info, "source": source, "scmsCredentials": scmsCredentials} 101 | api_response = self.client.putHelper(url, update_parameters) 102 | return api_response 103 | 104 | # Publish an algorithm 105 | def publish(self, details={}, settings={}, version_info={}, source={}, scmsCredentials={}): 106 | url = "/v1/algorithms/" + self.username + "/" + self.algoname + "/versions" 107 | publish_parameters = {"details": details, "settings": settings, 108 | "version_info": version_info, "source": source, "scmsCredentials": scmsCredentials} 109 | api_response = self.client.postJsonHelper(url, publish_parameters, parse_response_as_json=True, retry=True) 110 | return api_response 111 | 112 | def get_builds(self, limit=56, marker=None): 113 | kwargs = {"limit": limit, "marker": marker} 114 | url = "/v1/algorithms/" + self.username + "/" + self.algoname + '/builds' 115 | response = self.client.getJsonHelper(url, **kwargs) 116 | return response 117 | 118 | def get_build(self, build_id): 119 | # Get the build object for a given build_id 120 | # The build status can have one of the following value: succeeded, failed, in-progress 121 | url = '/v1/algorithms/' + self.username + '/' + self.algoname + '/builds/' + build_id 122 | response = self.client.getJsonHelper(url) 123 | return response 124 | 125 | def get_build_logs(self, build_id): 126 | # Get the algorithm build logs for a given build_id 127 | url = '/v1/algorithms/' + self.username + '/' + self.algoname + '/builds/' + build_id + '/logs' 128 | response = self.client.getJsonHelper(url) 129 | return response 130 | 131 | def get_scm_status(self): 132 | url = '/v1/algorithms/' + self.username + '/' + self.algoname + '/scm/status' 133 | response = self.client.getJsonHelper(url) 134 | return response 135 | 136 | # Get info on an algorithm 137 | def info(self, algo_hash=None): 138 | # Get Algorithm 139 | if algo_hash: 140 | url = '/v1/algorithms/' + self.username + '/' + self.algoname + '/versions/' + algo_hash 141 | else: 142 | url = '/v1/algorithms/' + self.username + '/' + self.algoname + '/versions' 143 | response = self.client.getJsonHelper(url) 144 | return response 145 | 146 | # Check if an Algorithm exists 147 | def exists(self): 148 | try: 149 | url = '/v1/algorithms/' + self.username + '/' + self.algoname 150 | _ = self.client.getJsonHelper(url) 151 | return True 152 | except AlgorithmException as e: 153 | if "404" in str(e) or "No such algorithm" in str(e): 154 | return False 155 | elif "403" in str(e): 156 | raise Exception("unable to check exists on algorithms you don't own.") 157 | else: 158 | raise e 159 | 160 | # Get all versions of the algorithm, with the given filters 161 | def versions(self, limit=None, marker=None, published=None, callable=None): 162 | kwargs = {} 163 | bools = ["True", "False"] 164 | if limit: 165 | kwargs["limit"] = limit 166 | if marker: 167 | kwargs["marker"] = marker 168 | if published: 169 | p = published 170 | kwargs["published"] = str(p).lower() if str(p) in bools else p 171 | if callable: 172 | c = callable 173 | kwargs["callable"] = str(c).lower() if str(c) in bools else c 174 | # Get Algorithm versions 175 | url = '/v1/algorithms/' + self.username + '/' + self.algoname + '/versions' 176 | response = self.client.getJsonHelper(url, **kwargs) 177 | return response 178 | 179 | # Compile an algorithm 180 | def compile(self): 181 | # Compile algorithm 182 | url = '/v1/algorithms/' + self.username + '/' + self.algoname + '/compile' 183 | response = self.client.postJsonHelper(url, {}, parse_response_as_json=True, retry=True) 184 | return response 185 | 186 | # Pipe an input into this algorithm 187 | def pipe(self, input1): 188 | 189 | if self.output_type == OutputType.raw: 190 | return self._postRawOutput(input1) 191 | elif self.output_type == OutputType.void: 192 | return self._postVoidOutput(input1) 193 | else: 194 | return AlgoResponse.create_algo_response( 195 | self.client.postJsonHelper(self.url, input1, **self.query_parameters)) 196 | 197 | def _postRawOutput(self, input1): 198 | # Don't parse response as json 199 | self.query_parameters['output'] = 'raw' 200 | response = self.client.postJsonHelper(self.url, input1, parse_response_as_json=False, **self.query_parameters) 201 | # Check HTTP code and throw error as needed 202 | if response.status_code == 400: 203 | # Bad request 204 | raise ApiError(response.text) 205 | elif response.status_code == 500: 206 | raise ApiInternalError(response.text) 207 | else: 208 | return response.text 209 | 210 | def _postVoidOutput(self, input1): 211 | self.query_parameters['output'] = 'void' 212 | responseJson = self.client.postJsonHelper(self.url, input1, **self.query_parameters) 213 | if 'error' in responseJson: 214 | raise ApiError(responseJson['error']['message']) 215 | else: 216 | return AsyncResponse(responseJson) 217 | -------------------------------------------------------------------------------- /Algorithmia/async_response.py: -------------------------------------------------------------------------------- 1 | class AsyncResponse(object): 2 | ''' 3 | Response from the API for an asynchronous request (output=void) 4 | ''' 5 | def __init__(self, server_response): 6 | self.async_protocol = server_response['async'] 7 | self.request_id = server_response['request_id'] 8 | 9 | def __repr__(self): 10 | return 'AsyncResponse(async_protocol=%s, request_id=%s)' % (self.async_protocol, self.request_id) -------------------------------------------------------------------------------- /Algorithmia/client.py: -------------------------------------------------------------------------------- 1 | 'Algorithmia API Client (python)' 2 | 3 | import Algorithmia 4 | from Algorithmia.insights import Insights 5 | from Algorithmia.errors import raiseAlgoApiError 6 | from Algorithmia.algorithm import Algorithm 7 | from Algorithmia.datafile import DataFile, LocalDataFile, AdvancedDataFile 8 | from Algorithmia.datadirectory import DataDirectory, LocalDataDirectory, AdvancedDataDirectory 9 | from algorithmia_api_client import Configuration, DefaultApi, ApiClient 10 | from Algorithmia.util import md5_for_file, md5_for_str 11 | from tempfile import mkstemp 12 | import atexit 13 | import json, re, requests, six, certifi 14 | import tarfile 15 | import os 16 | from time import time 17 | 18 | 19 | class Client(object): 20 | 'Algorithmia Common Library' 21 | 22 | handle, ca_cert = None, None 23 | apiKey = None 24 | apiAddress = None 25 | requestSession = None 26 | bearerToken = None 27 | 28 | def __init__(self, apiKey=None, apiAddress=None, caCert=None, bearerToken=None): 29 | # Override apiKey with environment variable 30 | config = None 31 | self.requestSession = requests.Session() 32 | if apiKey is None and 'ALGORITHMIA_API_KEY' in os.environ: 33 | apiKey = os.environ['ALGORITHMIA_API_KEY'] 34 | elif bearerToken is None and 'ALGORITHMIA_BEARER_TOKEN' in os.environ: 35 | bearerToken = os.environ['ALGORITHMIA_BEARER_TOKEN'] 36 | 37 | self.bearerToken = bearerToken 38 | self.apiKey = apiKey 39 | if apiAddress is not None: 40 | self.apiAddress = apiAddress 41 | else: 42 | self.apiAddress = Algorithmia.getApiAddress() 43 | if caCert == False: 44 | self.requestSession.verify = False 45 | self.requestSession.trust_env = False 46 | config = Configuration(use_ssl=False) 47 | elif caCert is None and 'REQUESTS_CA_BUNDLE' in os.environ: 48 | caCert = os.environ.get('REQUESTS_CA_BUNDLE') 49 | self.catCerts(caCert) 50 | self.requestSession.verify = self.ca_cert 51 | elif caCert is not None and 'REQUESTS_CA_BUNDLE' not in os.environ: 52 | self.catCerts(caCert) 53 | self.requestSession.verify = self.ca_cert 54 | elif caCert is not None and 'REQUESTS_CA_BUNDLE' in os.environ: 55 | # if both are available, use the one supplied in the constructor. I assume that a user supplying a cert in initialization wants to use that one. 56 | self.catCerts(caCert) 57 | self.requestSession.verify = self.ca_cert 58 | 59 | if not config: 60 | config = Configuration() 61 | 62 | config.api_key['Authorization'] = self.apiKey 63 | config.host = "{}/v1".format(self.apiAddress) 64 | self.manageApi = DefaultApi(ApiClient(config)) 65 | 66 | def algo(self, algoRef): 67 | return Algorithm(self, algoRef) 68 | 69 | def username(self): 70 | username = next(self.dir("").list()).path 71 | return username 72 | 73 | def scms(self): 74 | url = "/v1/scms" 75 | response = self.getJsonHelper(url) 76 | return response 77 | 78 | def file(self, dataUrl, cleanup=False): 79 | if dataUrl.startswith('file://'): 80 | return LocalDataFile(self, dataUrl) 81 | else: 82 | return AdvancedDataFile(self, dataUrl, cleanup) 83 | 84 | def dir(self, dataUrl): 85 | if dataUrl.startswith('file://'): 86 | return LocalDataDirectory(self, dataUrl) 87 | else: 88 | return AdvancedDataDirectory(self, dataUrl) 89 | 90 | def create_user(self, requestString): 91 | url = "/v1/users" 92 | response = self.postJsonHelper(url, input_object=requestString) 93 | return response 94 | 95 | def get_org_types(self): 96 | url = "/v1/organization/types" 97 | response = self.getHelper(url) 98 | return json.loads(response.content.decode("utf-8")) 99 | 100 | def create_org(self, requestString): 101 | url = "/v1/organizations" 102 | type = requestString["type_id"] 103 | 104 | id, error = self.convert_type_id(type) 105 | requestString["type_id"] = id 106 | 107 | response = self.postJsonHelper(url=url, input_object=requestString) 108 | if (error != "") and (response["error"] is not None): 109 | response["error"]["message"] = error 110 | 111 | return response 112 | 113 | def get_org(self, org_name): 114 | url = "/v1/organizations/" + org_name 115 | response = self.getHelper(url) 116 | return json.loads(response.content.decode("utf-8")) 117 | 118 | def edit_org(self, org_name, requestString): 119 | url = "/v1/organizations/" + org_name 120 | type = requestString["type_id"] 121 | 122 | id, error = self.convert_type_id(type) 123 | requestString["type_id"] = id 124 | 125 | data = json.dumps(requestString).encode('utf-8') 126 | response = self.putHelper(url, data) 127 | 128 | if (error != "") and (response["error"] is not None): 129 | response["error"]["message"] = error 130 | 131 | return response 132 | 133 | def invite_to_org(self, orgname, username): 134 | url = "/v1/organizations/" + orgname + "/members/" + username 135 | response = self.putHelper(url, data={}) 136 | return response 137 | 138 | def get_template(self, envid, dest, save_tar=False): 139 | url = "/v1/algorithm-environments/edge/environment-specifications/" + envid + "/template" 140 | filename = "template.tar.gz" 141 | 142 | if not os.path.exists(dest): 143 | os.makedirs(dest) 144 | 145 | filepath = os.path.join(dest, filename) 146 | response = self.getStreamHelper(url) 147 | 148 | if response.ok: 149 | with open(filepath, 'wb') as f: 150 | for chunk in response.iter_content(chunk_size=1024 * 8): 151 | if chunk: 152 | f.write(chunk) 153 | f.flush() 154 | os.fsync(f.fileno()) 155 | 156 | tar = tarfile.open(filepath, "r:gz") 157 | tar.extractall(dest) 158 | tar.close() 159 | 160 | if not save_tar: 161 | try: 162 | os.remove(filepath) 163 | except OSError as e: 164 | print(e) 165 | return response 166 | else: 167 | return json.loads(response.content.decode("utf-8")) 168 | 169 | def get_environment(self, language): 170 | url = "/v1/algorithm-environments/edge/languages/" + language + "/environments" 171 | response = self.getHelper(url) 172 | return response.json() 173 | 174 | def get_supported_languages(self): 175 | url = "/v1/algorithm-environments/edge/languages" 176 | response = self.getHelper(url) 177 | return response.json() 178 | 179 | def get_secret_providers(self): 180 | url = "/v1/secret-provider" 181 | api_response = self.getJsonHelper(url) 182 | return api_response 183 | 184 | def get_organization_errors(self, org_name): 185 | """Gets the errors for the organization. 186 | 187 | Args: 188 | self (Client): The instance of the Client class. 189 | org_name (str): The identifier for the organization. 190 | 191 | Returns: 192 | Any: A JSON-encoded response from the API. 193 | """ 194 | 195 | url = '/v1/organizations/%s/errors' % org_name 196 | response = self.getHelper(url) 197 | return response.json() 198 | 199 | def get_user_errors(self, user_id): 200 | """Gets the errors for a specific user. 201 | 202 | Args: 203 | self (Client): The instance of the Client class. 204 | user_id (str): The identifier for the user. 205 | 206 | Returns: 207 | Any: A JSON-encoded response from the API. 208 | """ 209 | 210 | url = '/v1/users/%s/errors' % user_id 211 | response = self.getHelper(url) 212 | return response.json() 213 | 214 | def get_algorithm_errors(self, algorithm_id): 215 | """Gets the errors for a specific algorithm. 216 | 217 | Args: 218 | self (Client): The instance of the Client class. 219 | algorithm_id (str): The identifier for the algorithm. 220 | 221 | Returns: 222 | Any: A JSON-encoded response from the API. 223 | """ 224 | 225 | url = '/v1/algorithms/%s/errors' % algorithm_id 226 | return self.getJsonHelper(url) 227 | 228 | # Used to send insight data to Algorithm Queue Reader in cluster 229 | def report_insights(self, insights): 230 | return Insights(insights) 231 | 232 | # Used internally to post json to the api and parse json response 233 | def postJsonHelper(self, url, input_object, parse_response_as_json=True, retry=False, **query_parameters): 234 | headers = {} 235 | if self.apiKey is not None: 236 | headers['Authorization'] = self.apiKey 237 | elif self.bearerToken is not None: 238 | headers['Authorization'] = 'Bearer ' + self.bearerToken 239 | 240 | input_json = None 241 | if input_object is None: 242 | input_json = json.dumps(None).encode('utf-8') 243 | headers['Content-Type'] = 'application/json' 244 | elif isinstance(input_object, six.string_types): 245 | input_json = input_object.encode('utf-8') 246 | headers['Content-Type'] = 'text/plain' 247 | elif isinstance(input_object, bytearray) or isinstance(input_object, bytes): 248 | input_json = bytes(input_object) 249 | headers['Content-Type'] = 'application/octet-stream' 250 | else: 251 | input_json = json.dumps(input_object).encode('utf-8') 252 | headers['Content-Type'] = 'application/json' 253 | 254 | response = self.requestSession.post(self.apiAddress + url, data=input_json, headers=headers, 255 | params=query_parameters) 256 | if 200 <= response.status_code <= 299: 257 | if parse_response_as_json: 258 | response = response.json() 259 | if 'error' in response: 260 | raise raiseAlgoApiError(response) 261 | else: 262 | return response 263 | else: 264 | return response 265 | elif retry: 266 | return self.postJsonHelper(url, input_object, parse_response_as_json, False, **query_parameters) 267 | else: 268 | raise raiseAlgoApiError(response) 269 | 270 | # Used internally to http get a file 271 | def getHelper(self, url, **query_parameters): 272 | headers = {} 273 | if self.apiKey is not None: 274 | headers['Authorization'] = self.apiKey 275 | elif self.bearerToken is not None: 276 | headers['Authorization'] = 'Bearer ' + self.bearerToken 277 | return self.requestSession.get(self.apiAddress + url, headers=headers, params=query_parameters) 278 | 279 | def getJsonHelper(self, url, **query_parameters): 280 | headers = {} 281 | if self.apiKey is not None: 282 | headers['Authorization'] = self.apiKey 283 | elif self.bearerToken is not None: 284 | headers['Authorization'] = 'Bearer ' + self.bearerToken 285 | response = self.requestSession.get(self.apiAddress + url, headers=headers, params=query_parameters) 286 | if 200 <= response.status_code <= 299: 287 | response = response.json() 288 | if 'error' in response: 289 | raise raiseAlgoApiError(response) 290 | else: 291 | return response 292 | else: 293 | if response.content is not None: 294 | response = response.json() 295 | raise raiseAlgoApiError(response) 296 | 297 | def getStreamHelper(self, url, **query_parameters): 298 | headers = {} 299 | if self.apiKey is not None: 300 | headers['Authorization'] = self.apiKey 301 | elif self.bearerToken is not None: 302 | headers['Authorization'] = 'Bearer ' + self.bearerToken 303 | return self.requestSession.get(self.apiAddress + url, headers=headers, params=query_parameters, stream=True) 304 | 305 | def patchHelper(self, url, params): 306 | headers = {'content-type': 'application/json'} 307 | if self.apiKey is not None: 308 | headers['Authorization'] = self.apiKey 309 | elif self.bearerToken is not None: 310 | headers['Authorization'] = 'Bearer ' + self.bearerToken 311 | return self.requestSession.patch(self.apiAddress + url, headers=headers, data=json.dumps(params)) 312 | 313 | # Used internally to get http head result 314 | def headHelper(self, url): 315 | headers = {} 316 | if self.apiKey is not None: 317 | headers['Authorization'] = self.apiKey 318 | elif self.bearerToken is not None: 319 | headers['Authorization'] = 'Bearer ' + self.bearerToken 320 | return self.requestSession.head(self.apiAddress + url, headers=headers) 321 | 322 | # Used internally to http put a file 323 | def putHelper(self, url, data): 324 | headers = {} 325 | if self.apiKey is not None: 326 | headers['Authorization'] = self.apiKey 327 | elif self.bearerToken is not None: 328 | headers['Authorization'] = 'Bearer ' + self.bearerToken 329 | if isJson(data): 330 | headers['Content-Type'] = 'application/json' 331 | response = self.requestSession.put(self.apiAddress + url, data=data, headers=headers) 332 | if response._content == b'': 333 | return response 334 | if 200 <= response.status_code <= 299: 335 | response = response.json() 336 | if 'error' in response: 337 | raise raiseAlgoApiError(response) 338 | else: 339 | return response 340 | else: 341 | raise raiseAlgoApiError(response) 342 | 343 | # Used internally to http delete a file 344 | def deleteHelper(self, url): 345 | headers = {} 346 | if self.apiKey is not None: 347 | headers['Authorization'] = self.apiKey 348 | elif self.bearerToken is not None: 349 | headers['Authorization'] = 'Bearer ' + self.bearerToken 350 | response = self.requestSession.delete(self.apiAddress + url, headers=headers) 351 | if response.reason == "No Content": 352 | return response 353 | return response.json() 354 | 355 | # Used internally to concatonate given custom cert with built in certificate store. 356 | def catCerts(self, customCert): 357 | self.handle, self.ca_cert = mkstemp(suffix=".pem") 358 | # wrapped all in the with context handler to prevent unclosed files 359 | with open(customCert, 'r') as custom_cert, \ 360 | open(self.ca_cert, 'w') as ca, \ 361 | open(certifi.where(), 'r') as cert: 362 | new_cert = custom_cert.read() + cert.read() 363 | ca.write(new_cert) 364 | atexit.register(self.exit_handler) 365 | 366 | # User internally to convert type id name to uuid 367 | def convert_type_id(self, type): 368 | id = "" 369 | error = "" 370 | types = self.get_org_types() 371 | for enumtype in types: 372 | if type == enumtype["name"]: 373 | id = enumtype["id"] 374 | error = "" 375 | break 376 | else: 377 | error = "invalid type_id" 378 | 379 | return (id, error) 380 | 381 | # Used internally to clean up temporary files 382 | def exit_handler(self): 383 | try: 384 | os.close(self.handle) 385 | os.unlink(self.ca_cert) 386 | except OSError as e: 387 | print(e) 388 | 389 | # Used by CI/CD automation for freezing model manifest files, and by the CLI for manual freezing 390 | def freeze(self, manifest_path, manifest_output_dir="."): 391 | if os.path.exists(manifest_path): 392 | with open(manifest_path, 'r') as f: 393 | manifest_file = json.load(f) 394 | manifest_file['timestamp'] = str(time()) 395 | required_files = manifest_file['required_files'] 396 | optional_files = manifest_file['optional_files'] 397 | for i in range(len(required_files)): 398 | uri = required_files[i]['source_uri'] 399 | local_file = self.file(uri).getFile(as_path=True) 400 | md5_checksum = md5_for_file(local_file) 401 | required_files[i]['md5_checksum'] = md5_checksum 402 | for i in range(len(optional_files)): 403 | uri = required_files[i]['source_uri'] 404 | local_file = self.file(uri).getFile(as_path=True) 405 | md5_checksum = md5_for_file(local_file) 406 | required_files[i]['md5_checksum'] = md5_checksum 407 | lock_md5_checksum = md5_for_str(str(manifest_file)) 408 | manifest_file['lock_checksum'] = lock_md5_checksum 409 | with open(manifest_output_dir + '/' + 'model_manifest.json.freeze', 'w') as f: 410 | json.dump(manifest_file, f) 411 | else: 412 | print("Expected to find a model_manifest.json file, none was discovered in working directory") 413 | 414 | 415 | def isJson(myjson): 416 | try: 417 | json_object = json.loads(myjson) 418 | except (ValueError, TypeError) as e: 419 | return False 420 | 421 | return True 422 | -------------------------------------------------------------------------------- /Algorithmia/data.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | DataObjectType = Enum('DataObjectType','file directory') 4 | 5 | class DataObject(object): 6 | def __init__(self, data_object_type): 7 | self.data_object_type = data_object_type 8 | 9 | def is_file(self): 10 | '''Returns whether object is a file''' 11 | return self.data_object_type is DataObjectType.file 12 | 13 | def is_dir(self): 14 | '''Returns whether object is a directory''' 15 | return self.data_object_type is DataObjectType.directory 16 | 17 | def get_type(self): 18 | '''Returns type of this DataObject''' 19 | return self.data_object_type 20 | 21 | def set_attributes(self): 22 | '''Sets attributes about the directory after querying the Data API''' 23 | raise NotImplementedError -------------------------------------------------------------------------------- /Algorithmia/datadirectory.py: -------------------------------------------------------------------------------- 1 | 'Algorithmia Data API Client (python)' 2 | 3 | import json 4 | import re 5 | import os 6 | import six 7 | import tempfile 8 | import Algorithmia 9 | 10 | from Algorithmia.datafile import DataFile, AdvancedDataFile, LocalDataFile 11 | from Algorithmia.data import DataObject, DataObjectType 12 | from Algorithmia.errors import DataApiError 13 | from Algorithmia.util import getParentAndBase, pathJoin 14 | from Algorithmia.acl import Acl 15 | 16 | 17 | class DataDirectory(DataObject): 18 | def __init__(self, client, dataUrl): 19 | super(DataDirectory, self).__init__(DataObjectType.directory) 20 | self.client = client 21 | # Parse dataUrl 22 | self.path = re.sub(r'^data://|^/', '', dataUrl) 23 | self.url = DataDirectory._getUrl(self.path) 24 | 25 | @staticmethod 26 | def _getUrl(path): 27 | return '/v1/data/' + path 28 | 29 | def set_attributes(self, response_json): 30 | # Nothing to set for now 31 | pass 32 | 33 | def getName(self): 34 | _, name = getParentAndBase(self.path) 35 | return name 36 | 37 | def exists(self): 38 | # Heading a directory apparently isn't a valid operation 39 | response = self.client.getHelper(self.url) 40 | return (response.status_code == 200) 41 | 42 | def create(self, acl=None): 43 | '''Creates a directory, optionally include Acl argument to set permissions''' 44 | parent, name = getParentAndBase(self.path) 45 | json = {'name': name} 46 | if acl is not None: 47 | json['acl'] = acl.to_api_param() 48 | response = self.client.postJsonHelper(DataDirectory._getUrl(parent), json, False) 49 | if (response.status_code != 200): 50 | raise DataApiError("Directory creation failed: " + str(response.content)) 51 | 52 | def delete(self, force=False): 53 | # Delete from data api 54 | url = self.url 55 | if force: 56 | url += '?force=true' 57 | 58 | result = self.client.deleteHelper(url) 59 | if 'error' in result: 60 | raise DataApiError(result['error']['message']) 61 | else: 62 | return True 63 | 64 | def file(self, name): 65 | return DataFile(self.client, pathJoin(self.path, name)) 66 | 67 | def files(self): 68 | return self._get_directory_iterator(DataObjectType.file) 69 | 70 | def dir(self, name): 71 | return DataDirectory(self.client, pathJoin(self.path, name)) 72 | 73 | def dirs(self): 74 | return self._get_directory_iterator(DataObjectType.directory) 75 | 76 | def getDir(self): 77 | directory = tempfile.mkdtemp() 78 | for file in self.files(): 79 | correct_filename = file.getName() 80 | correct_file_path = os.path.join(directory, correct_filename) 81 | local_file = file.getFile(as_path=True) 82 | os.rename(local_file, correct_file_path) 83 | return directory 84 | 85 | def list(self): 86 | return self._get_directory_iterator() 87 | 88 | def get_permissions(self): 89 | ''' 90 | Returns permissions for this directory or None if it's a special collection such as 91 | .session or .algo 92 | ''' 93 | response = self.client.getHelper(self.url, acl='true') 94 | if response.status_code != 200: 95 | raise DataApiError('Unable to get permissions:' + str(response.content)) 96 | content = response.json() 97 | if 'acl' in content: 98 | return Acl.from_acl_response(content['acl']) 99 | else: 100 | return None 101 | 102 | def update_permissions(self, acl): 103 | params = {'acl': acl.to_api_param()} 104 | response = self.client.patchHelper(self.url, params) 105 | if response.status_code != 200: 106 | raise DataApiError('Unable to update permissions: ' + response.json()['error']['message']) 107 | return True 108 | 109 | def _get_directory_iterator(self, type_filter=None): 110 | marker = None 111 | first = True 112 | while first or (marker is not None and len(marker) > 0): 113 | first = False 114 | url = self.url 115 | query_params = {} 116 | if marker: 117 | query_params['marker'] = marker 118 | response = self.client.getHelper(url, **query_params) 119 | if response.status_code != 200: 120 | raise DataApiError("Directory iteration failed: " + str(response.content)) 121 | 122 | responseContent = response.content 123 | if isinstance(responseContent, six.binary_type): 124 | responseContent = responseContent.decode() 125 | 126 | content = json.loads(responseContent) 127 | if 'marker' in content: 128 | marker = content['marker'] 129 | else: 130 | marker = None 131 | 132 | if type_filter is DataObjectType.directory or type_filter is None: 133 | for d in self._iterate_directories(content): 134 | yield d 135 | if type_filter is DataObjectType.file or type_filter is None: 136 | for f in self._iterate_files(content): 137 | yield f 138 | 139 | def _iterate_directories(self, content): 140 | directories = [] 141 | if 'folders' in content: 142 | for dir_info in content['folders']: 143 | d = DataDirectory(self.client, pathJoin(self.path, dir_info['name'])) 144 | d.set_attributes(dir_info) 145 | directories.append(d) 146 | return directories 147 | 148 | def _iterate_files(self, content): 149 | files = [] 150 | if 'files' in content: 151 | for file_info in content['files']: 152 | f = DataFile(self.client, pathJoin(self.path, file_info['filename'])) 153 | f.set_attributes(file_info) 154 | files.append(f) 155 | return files 156 | 157 | 158 | class LocalDataDirectory(): 159 | def __init__(self, client, dataUrl): 160 | self.client = client 161 | # Parse dataUrl 162 | self.path = dataUrl.replace('file://', '') 163 | 164 | def set_attributes(self, response_json): 165 | raise NotImplementedError 166 | 167 | def getName(self): 168 | raise NotImplementedError 169 | 170 | def exists(self): 171 | return os.path.isdir(self.path) 172 | 173 | def create(self): 174 | os.mkdir(self.path) 175 | 176 | def delete(self, force=False): 177 | os.rmdir(self.path) 178 | 179 | def file(self, name): 180 | return LocalDataFile(self.client, 'file://' + pathJoin(self.path, name)) 181 | 182 | def dir(self, name): 183 | raise NotImplementedError 184 | 185 | def list(self): 186 | for x in os.listdir(self.path): yield x 187 | 188 | def dirs(self, content): 189 | for x in os.listdir(self.path): 190 | if os.path.isdir(self.path + '/' + x): yield x 191 | 192 | def files(self, content): 193 | for x in os.listdir(self.path): 194 | if os.path.isfile(self.path + '/' + x): 195 | yield x 196 | 197 | 198 | class AdvancedDataDirectory(DataDirectory): 199 | def __init__(self, client, dataUrl): 200 | super(AdvancedDataDirectory, self).__init__(client, dataUrl) 201 | 202 | def file(self, name, cleanup=True): 203 | return AdvancedDataFile(self.client, pathJoin(self.path, name), cleanup) 204 | -------------------------------------------------------------------------------- /Algorithmia/datafile.py: -------------------------------------------------------------------------------- 1 | 'Algorithmia Data API Client (python)' 2 | 3 | import re 4 | import json 5 | import six 6 | import tempfile 7 | from datetime import datetime 8 | import os.path 9 | import pkgutil 10 | import zipfile 11 | 12 | from Algorithmia.util import getParentAndBase 13 | from Algorithmia.data import DataObject, DataObjectType 14 | from Algorithmia.errors import DataApiError, raiseDataApiError 15 | from io import RawIOBase 16 | 17 | 18 | class DataFile(DataObject): 19 | def __init__(self, client, dataUrl): 20 | super(DataFile, self).__init__(DataObjectType.file) 21 | self.client = client 22 | # Parse dataUrl 23 | self.path = re.sub(r'^data://|^/', '', dataUrl) 24 | self.url = '/v1/data/' + self.path 25 | self.last_modified = None 26 | self.size = None 27 | 28 | def set_attributes(self, attributes): 29 | self.last_modified = datetime.strptime(attributes['last_modified'], '%Y-%m-%dT%H:%M:%S.%fZ') 30 | self.size = attributes['size'] 31 | 32 | # Deprecated: 33 | def get(self): 34 | return self.client.getHelper(self.url) 35 | 36 | # Get file from the data api 37 | def getFile(self, as_path=False): 38 | exists, error = self.existsWithError() 39 | if not exists: 40 | raise DataApiError('unable to get file {} - {}'.format(self.path, error)) 41 | # Make HTTP get request 42 | response = self.client.getHelper(self.url) 43 | with tempfile.NamedTemporaryFile(delete=False) as f: 44 | for block in response.iter_content(1024): 45 | if not block: 46 | break 47 | f.write(block) 48 | f.flush() 49 | if as_path: 50 | return f.name 51 | else: 52 | return open(f.name) 53 | 54 | def getAsZip(self): 55 | """Download/decompress file/directory and return path to file/directory. 56 | 57 | Expects the `DataFile` object to contain a data API path pointing to a file/directory compressed with a zip-based compression algorithm. 58 | Either returns the directory or a path to the file, depending on whether a directory or file was zipped. 59 | """ 60 | local_file_path = self.getFile(as_path=True) 61 | directory_path = tempfile.mkdtemp() 62 | with zipfile.ZipFile(local_file_path, 'r') as ziph: 63 | ziph.extractall(directory_path) 64 | if len(ziph.namelist()) > 1: 65 | output_path = directory_path 66 | else: 67 | filename = ziph.namelist()[0] 68 | output_path = os.path.join(directory_path, filename) 69 | return output_path 70 | 71 | def getName(self): 72 | _, name = getParentAndBase(self.path) 73 | return name 74 | 75 | def getBytes(self): 76 | exists, error = self.existsWithError() 77 | if not exists: 78 | raise DataApiError('unable to get file {} - {}'.format(self.path, error)) 79 | # Make HTTP get request 80 | return self.client.getHelper(self.url).content 81 | 82 | def getString(self): 83 | exists, error = self.existsWithError() 84 | if not exists: 85 | raise DataApiError('unable to get file {} - {}'.format(self.path, error)) 86 | # Make HTTP get request 87 | return self.client.getHelper(self.url).text 88 | 89 | def getJson(self): 90 | exists, error = self.existsWithError() 91 | if not exists: 92 | raise DataApiError('unable to get file {} - {}'.format(self.path, error)) 93 | # Make HTTP get request 94 | return self.client.getHelper(self.url).json() 95 | 96 | def getNumpy(self): 97 | exists, error = self.existsWithError() 98 | if not exists: 99 | raise DataApiError('unable to get file {} - {}'.format(self.path, error)) 100 | np_loader = pkgutil.find_loader('numpy') 101 | if np_loader is not None: 102 | import numpy as np 103 | payload = self.client.getHelper(self.url).json() 104 | return np.array(payload) 105 | else: 106 | raise DataApiError("Attempted to .getNumpy() file without numpy available, please install numpy.") 107 | 108 | def exists(self): 109 | # In order to not break backward compatability keeping this method to only return 110 | # a boolean 111 | exists, error = self.existsWithError() 112 | return exists 113 | 114 | def existsWithError(self): 115 | response = self.client.headHelper(self.url) 116 | if 'X-Error-Message' in response.headers: 117 | error = response.headers['X-Error-Message'] 118 | else: 119 | error = response.text 120 | return (response.status_code == 200, error) 121 | 122 | def put(self, data): 123 | # Post to data api 124 | 125 | # First turn the data to bytes if we can 126 | if isinstance(data, six.string_types) and not isinstance(data, six.binary_type): 127 | data = bytes(data.encode()) 128 | if isinstance(data, six.binary_type): 129 | result = self.client.putHelper(self.url, data) 130 | if 'error' in result: 131 | raise raiseDataApiError(result) 132 | else: 133 | return self 134 | else: 135 | raise TypeError("Must put strings or binary data. Use putJson instead") 136 | 137 | def putJson(self, data): 138 | # Post to data api 139 | jsonElement = json.dumps(data) 140 | result = self.client.putHelper(self.url, jsonElement) 141 | if 'error' in result: 142 | raise raiseDataApiError(result) 143 | else: 144 | return self 145 | 146 | def putFile(self, path): 147 | # Post file to data api 148 | with open(path, 'rb') as f: 149 | result = self.client.putHelper(self.url, f) 150 | if 'error' in result: 151 | raise raiseDataApiError(result) 152 | else: 153 | return self 154 | 155 | def putNumpy(self, array): 156 | # Post numpy array as json payload 157 | np_loader = pkgutil.find_loader('numpy') 158 | if np_loader is not None: 159 | import numpy as np 160 | encoded_array = array.tolist() 161 | self.putJson(encoded_array) 162 | return self 163 | else: 164 | raise DataApiError("Attempted to .putNumpy() a file without numpy available, please install numpy.") 165 | 166 | def putAsZip(self, path): 167 | """Zip file/directory and upload to data API location defined by `DataFile` object. 168 | 169 | Accepts either a single file or a directory containing other files and directories. 170 | """ 171 | temp = tempfile.NamedTemporaryFile(delete=False).name 172 | if os.path.isdir(path): 173 | with zipfile.ZipFile(temp, 'w') as ziph: 174 | for root, dirs, files in os.walk(path): 175 | for file in files: 176 | f_path = os.path.join(root, file) 177 | arc_path = os.path.relpath(os.path.join(root, file), path) 178 | ziph.write(f_path, arc_path) 179 | else: 180 | with zipfile.ZipFile(temp, 'w') as ziph: 181 | ziph.write(path) 182 | return self.putFile(temp) 183 | 184 | def delete(self): 185 | # Delete from data api 186 | result = self.client.deleteHelper(self.url) 187 | if 'error' in result: 188 | raise raiseDataApiError(result) 189 | else: 190 | return True 191 | 192 | 193 | class LocalDataFile(): 194 | def __init__(self, client, filePath): 195 | self.client = client 196 | # Parse dataUrl 197 | self.path = filePath.replace('file://', '') 198 | self.url = '/v1/data/' + self.path 199 | self.last_modified = None 200 | self.size = None 201 | 202 | def set_attributes(self, attributes): 203 | self.last_modified = datetime.strptime(attributes['last_modified'], '%Y-%m-%dT%H:%M:%S.%fZ') 204 | self.size = attributes['size'] 205 | 206 | # Get file from the data api 207 | def getFile(self): 208 | exists, error = self.existsWithError() 209 | if not exists: 210 | raise DataApiError('unable to get file {} - {}'.format(self.path, error)) 211 | return open(self.path) 212 | 213 | def getName(self): 214 | _, name = getParentAndBase(self.path) 215 | return name 216 | 217 | def getBytes(self): 218 | exists, error = self.existsWithError() 219 | if not exists: 220 | raise DataApiError('unable to get file {} - {}'.format(self.path, error)) 221 | f = open(self.path, 'rb') 222 | bts = f.read() 223 | f.close() 224 | return bts 225 | 226 | def getString(self): 227 | exists, error = self.existsWithError() 228 | if not exists: 229 | raise DataApiError('unable to get file {} - {}'.format(self.path, error)) 230 | with open(self.path, 'r') as f: return f.read() 231 | 232 | def getJson(self): 233 | exists, error = self.existsWithError() 234 | if not exists: 235 | raise DataApiError('unable to get file {} - {}'.format(self.path, error)) 236 | return json.loads(open(self.path, 'r').read()) 237 | 238 | def exists(self): 239 | return self.existsWithError()[0] 240 | 241 | def existsWithError(self): 242 | return os.path.isfile(self.path), '' 243 | 244 | def put(self, data): 245 | # First turn the data to bytes if we can 246 | if isinstance(data, six.string_types) and not isinstance(data, six.binary_type): 247 | data = bytes(data.encode()) 248 | with open(self.path, 'wb') as f: f.write(data) 249 | return self 250 | 251 | def putJson(self, data): 252 | # Post to data api 253 | jsonElement = json.dumps(data) 254 | result = localPutHelper(self.path, jsonElement) 255 | if 'error' in result: 256 | raise raiseDataApiError(result) 257 | else: 258 | return self 259 | 260 | def putFile(self, path): 261 | result = localPutHelper(path, self.path) 262 | if 'error' in result: 263 | raise raiseDataApiError(result) 264 | else: 265 | return self 266 | 267 | def delete(self): 268 | try: 269 | os.remove(self.path) 270 | return True 271 | except: 272 | raise DataApiError('Failed to delete local file ' + self.path) 273 | 274 | 275 | def localPutHelper(path, contents): 276 | try: 277 | with open(path, 'wb') as f: 278 | f.write(contents) 279 | return dict(status='success') 280 | except Exception as e: 281 | return dict(error=str(e)) 282 | 283 | 284 | class AdvancedDataFile(DataFile, RawIOBase): 285 | def __init__(self, client, dataUrl, cleanup=True): 286 | super(AdvancedDataFile, self).__init__(client, dataUrl) 287 | self.cleanup = cleanup 288 | self.local_file = None 289 | 290 | def __del__(self): 291 | if self.local_file: 292 | filepath = self.local_file.name 293 | self.local_file.close() 294 | if self.cleanup: 295 | os.remove(filepath) 296 | 297 | def readable(self): 298 | return True 299 | 300 | def seekable(self): 301 | return True 302 | 303 | def writable(self): 304 | return False 305 | 306 | def read(self, __size=None): 307 | if not self.local_file: 308 | self.local_file = self.getFile() 309 | output = self.local_file.read() 310 | elif __size: 311 | output = self.local_file.read(__size) 312 | else: 313 | output = self.local_file.read() 314 | return output 315 | 316 | def readline(self, __size=None): 317 | if not self.local_file: 318 | self.local_file = self.getFile() 319 | with self.local_file as f: 320 | if __size: 321 | output = f.readline(__size) 322 | else: 323 | output = f.readline() 324 | return output 325 | 326 | def readlines(self, __hint=None): 327 | if not self.local_file: 328 | self.local_file = self.getFile() 329 | if __hint: 330 | output = self.local_file.readlines(__hint) 331 | else: 332 | output = self.local_file.readlines() 333 | return output 334 | 335 | def tell(self): 336 | if not self.local_file: 337 | self.local_file = self.getFile() 338 | output = self.local_file.tell() 339 | return output 340 | 341 | def seek(self, __offset, __whence=None): 342 | if not self.local_file: 343 | self.local_file = self.getFile() 344 | if __whence: 345 | output = self.local_file.seek(__offset, __whence) 346 | else: 347 | output = self.local_file.seek(__offset) 348 | return output 349 | -------------------------------------------------------------------------------- /Algorithmia/errors.py: -------------------------------------------------------------------------------- 1 | class ApiError(Exception): 2 | '''General error from the Algorithmia API''' 3 | pass 4 | 5 | class ApiInternalError(ApiError): 6 | '''Error representing a server error, typically a 5xx status code''' 7 | pass 8 | 9 | class DataApiError(ApiError): 10 | '''Error returned from the Algorithmia data API''' 11 | pass 12 | 13 | class AlgorithmException(ApiError): 14 | '''Base algorithm error exception''' 15 | def __init__(self, message, stack_trace=None, error_type=None): 16 | self.message = message 17 | self.error_type = error_type 18 | self.stack_trace = stack_trace 19 | def __str__(self): 20 | if self.stack_trace: 21 | return repr(self.message + "\n" + self.stack_trace) 22 | else: 23 | return repr(self.message) 24 | 25 | 26 | def raiseDataApiError(result): 27 | if 'error' in result: 28 | if 'message' in result['error']: 29 | return DataApiError(result['error']['message']) 30 | else: 31 | return DataApiError(result['error']) 32 | 33 | 34 | def raiseAlgoApiError(result): 35 | if 'error' in result: 36 | if 'message' in result['error']: 37 | message = result['error']['message'] 38 | else: 39 | message = result['error'] 40 | if 'error_type' in result['error']: 41 | err_type = result['error']['error_type'] 42 | else: 43 | err_type = None 44 | if 'stack_trace' in result['error']: 45 | stacktrace = result['error']['stack_trace'] 46 | else: 47 | stacktrace = None 48 | return AlgorithmException(message=message, stack_trace=stacktrace, error_type=err_type) 49 | else: 50 | return Exception("Non-Algorithm related Failure: " + str(result)) 51 | -------------------------------------------------------------------------------- /Algorithmia/handler.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import json 4 | import base64 5 | import traceback 6 | import inspect 7 | import six 8 | 9 | 10 | class Handler(object): 11 | 12 | def __init__(self, apply_func, load_func): 13 | """ 14 | Creates the handler object 15 | :param apply_func: A required function that can have an arity of 1-2, depending on if loading occurs 16 | :param load_func: An optional supplier function used if load time events are required, has an arity of 0. 17 | """ 18 | self.FIFO_PATH = "/tmp/algoout" 19 | apply_args, _, _, apply_defaults = inspect.getargspec(apply_func) 20 | load_args, _, _, _ = inspect.getargspec(load_func) 21 | if len(load_args) > 0: 22 | raise Exception("load function must not have parameters") 23 | if len(apply_args) > 2 or len(apply_args) == 0: 24 | raise Exception("apply function may have between 1 and 2 parameters, not {}".format(len(apply_args))) 25 | self.apply_func = apply_func 26 | self.load_func = load_func 27 | self.load_result = None 28 | 29 | def load(self): 30 | self.load_result = self.load_func() 31 | print('PIPE_INIT_COMPLETE') 32 | sys.stdout.flush() 33 | 34 | def format_data(self, request): 35 | if request['content_type'] in ['text', 'json']: 36 | data = request['data'] 37 | elif request['content_type'] == 'binary': 38 | data = self.wrap_binary_data(request['data']) 39 | else: 40 | raise Exception("Invalid content_type: {}".format(request['content_type'])) 41 | return data 42 | 43 | def is_binary(self, arg): 44 | if six.PY3: 45 | return isinstance(arg, base64.bytes_types) 46 | 47 | return isinstance(arg, bytearray) 48 | 49 | def wrap_binary_data(self, data): 50 | if six.PY3: 51 | return bytes(data) 52 | else: 53 | return bytearray(data) 54 | 55 | def format_response(self, response): 56 | if self.is_binary(response): 57 | content_type = 'binary' 58 | response = base64.b64encode(response) 59 | if not isinstance(response, six.string_types): 60 | response = str(response, 'utf-8') 61 | elif isinstance(response, six.string_types) or isinstance(response, six.text_type): 62 | content_type = 'text' 63 | else: 64 | content_type = 'json' 65 | response_string = json.dumps({ 66 | 'result': response, 67 | 'metadata': { 68 | 'content_type': content_type 69 | } 70 | }) 71 | return response_string 72 | 73 | def write_to_pipe(self, data_string): 74 | if os.name == "posix": 75 | with open(self.FIFO_PATH, 'w') as f: 76 | f.write(data_string) 77 | f.write('\n') 78 | sys.stdout.flush() 79 | if os.name == "nt": 80 | sys.stdin = data_string 81 | 82 | def serve(self): 83 | try: 84 | self.load() 85 | except Exception as e: 86 | if hasattr(e, 'error_type'): 87 | error_type = e.error_type 88 | else: 89 | error_type = 'AlgorithmError' 90 | load_error_string = json.dumps({ 91 | 'error': { 92 | 'message': str(e), 93 | 'stacktrace': traceback.format_exc(), 94 | 'error_type': error_type 95 | } 96 | }) 97 | return self.write_to_pipe(load_error_string) 98 | for line in sys.stdin: 99 | try: 100 | request = json.loads(line) 101 | formatted_input = self.format_data(request) 102 | if self.load_result: 103 | apply_result = self.apply_func(formatted_input, self.load_result) 104 | else: 105 | apply_result = self.apply_func(formatted_input) 106 | response_string = self.format_response(apply_result) 107 | except Exception as e: 108 | if hasattr(e, 'error_type'): 109 | error_type = e.error_type 110 | else: 111 | error_type = 'AlgorithmError' 112 | response_string = json.dumps({ 113 | 'error': { 114 | 'message': str(e), 115 | 'stacktrace': traceback.format_exc(), 116 | 'error_type': error_type 117 | } 118 | }) 119 | finally: 120 | self.write_to_pipe(response_string) -------------------------------------------------------------------------------- /Algorithmia/insights.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import os 4 | 5 | class Insights: 6 | # Example of correct insights: 7 | # {"aKey":"aValue","aKey2":"aValue2"} 8 | def __init__(self, insights): 9 | headers = {} 10 | headers['Content-Type'] = 'application/json' 11 | AQR_URL = os.getenv('ALGORITHMIA_API') or "http://localhost:9000" 12 | insight_payload=[{"insight_key": key, "insight_value": insights[key]} for key in insights.keys()] 13 | 14 | requests.post(AQR_URL+"/v1/insights", data=json.dumps(insight_payload).encode('utf-8'), headers=headers) 15 | -------------------------------------------------------------------------------- /Algorithmia/util.py: -------------------------------------------------------------------------------- 1 | import re 2 | import hashlib 3 | 4 | FNAME_MATCH = re.compile(r'/([^/]+)$') # From the last slash to the end of the string 5 | PREFIX = re.compile(r'([^:]+://)(/)?(.+)') # Check for a prefix like data:// 6 | 7 | 8 | def getParentAndBase(path): 9 | match = PREFIX.match(path) 10 | if match is None: 11 | if path.endswith('/'): 12 | stripped_path = path[:-1] 13 | else: 14 | stripped_path = path 15 | base = FNAME_MATCH.search(stripped_path) 16 | if base is None: 17 | raise ValueError('Invalid path') 18 | parent = FNAME_MATCH.sub('', stripped_path) 19 | return parent, base.group(1) 20 | else: 21 | prefix, leading_slash, uri = match.groups() 22 | parts = uri.split('/') 23 | parent_path = '/'.join(parts[:-1]) 24 | 25 | if leading_slash is not None: 26 | parent_path = '{prefix}/{uri}'.format(prefix=prefix, uri='/'.join(parts[:-1])) 27 | else: 28 | parent_path = '{prefix}{uri}'.format(prefix=prefix, uri='/'.join(parts[:-1])) 29 | return parent_path, parts[-1] 30 | 31 | 32 | def pathJoin(parent, base): 33 | if parent.endswith('/'): 34 | return parent + base 35 | return parent + '/' + base 36 | 37 | 38 | def md5_for_file(fname): 39 | hash_md5 = hashlib.md5() 40 | with open(fname, "rb") as f: 41 | for chunk in iter(lambda: f.read(4096), b""): 42 | hash_md5.update(chunk) 43 | return str(hash_md5.hexdigest()) 44 | 45 | 46 | def md5_for_str(content): 47 | hash_md5 = hashlib.md5() 48 | hash_md5.update(content.encode()) 49 | return str(hash_md5.hexdigest()) 50 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # https://github.blog/2017-07-06-introducing-code-owners/ 2 | 3 | * @algorithmiaio/developer-technologies-contractors 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Algorithmia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Algorithmia Common Library (python) 2 | =================================== 3 | 4 | Python client library for accessing the Algorithmia API 5 | For API documentation, see the [PythonDocs](https://algorithmia.com/docs/lang/python) 6 | 7 | [![PyPI](https://img.shields.io/pypi/v/algorithmia.svg?maxAge=600)]() 8 | 9 | ## Algorithm Development Kit 10 | This package contains the [algorithmia-adk](https://github.com/algorithmiaio/algorithmia-adk-python) development kit, simply add `from Algorithmia import ADK` into your workflow to access it. 11 | 12 | ## Install from PyPi 13 | 14 | The official Algorithmia python client is [available on PyPi](https://pypi.python.org/pypi/algorithmia). 15 | Install it with pip: 16 | 17 | ```bash 18 | pip install algorithmia 19 | ``` 20 | 21 | ## Install from source 22 | 23 | Build algorithmia client wheel: 24 | 25 | ```bash 26 | python setup.py bdist_wheel 27 | ``` 28 | 29 | Install a wheel manually: 30 | 31 | ```bash 32 | pip install --user --upgrade dist/algorithmia-*.whl 33 | ``` 34 | #install locally for testing 35 | 36 | from directory containing setup.py and the Algorithmia directory: 37 | ```bash 38 | pip3 install ./ 39 | ``` 40 | #add CLI script to PATH 41 | 42 | to use the CLI it may be nessesary to add the install location to the PATH enviroment variable 43 | ```bash 44 | export PATH = $HOME/.local/bin:$PATH 45 | ``` 46 | 47 | ## Authentication 48 | 49 | First, create an Algorithmia client and authenticate with your API key: 50 | 51 | ```python 52 | import Algorithmia 53 | 54 | apiKey = '{{Your API key here}}' 55 | client = Algorithmia.client(apiKey) 56 | ``` 57 | 58 | Now you're ready to call algorithms. 59 | 60 | ## Calling algorithms 61 | 62 | The following examples of calling algorithms are organized by type of input/output which vary between algorithms. 63 | 64 | Note: a single algorithm may have different input and output types, or accept multiple types of input, 65 | so consult the algorithm's description for usage examples specific to that algorithm. 66 | 67 | ### Text input/output 68 | 69 | Call an algorithm with text input by simply passing a string into its `pipe` method. 70 | If the algorithm output is text, then the `result` field of the response will be a string. 71 | 72 | ```python 73 | algo = client.algo('demo/Hello/0.1.1') 74 | response = algo.pipe("HAL 9000") 75 | print(response.result) # Hello, world! 76 | print(response.metadata) # Metadata(content_type='text',duration=0.0002127) 77 | print(response.metadata.duration) # 0.0002127 78 | ``` 79 | 80 | ### JSON input/output 81 | 82 | Call an algorithm with JSON input by simply passing in a type that can be serialized to JSON: 83 | most notably python dicts and arrays. 84 | For algorithms that return JSON, the `result` field of the response will be the appropriate 85 | deserialized type. 86 | 87 | ```python 88 | algo = client.algo('WebPredict/ListAnagrams/0.1.0') 89 | result = algo.pipe(["transformer", "terraforms", "retransform"]).result 90 | # -> ["transformer","retransform"] 91 | ``` 92 | 93 | ### Binary input/output 94 | 95 | Call an algorithm with Binary input by passing a byte array into the `pipe` method. 96 | Similarly, if the algorithm response is binary data, then the `result` field of the response 97 | will be a byte array. 98 | 99 | ```python 100 | input = bytearray(open("/path/to/bender.png", "rb").read()) 101 | result = client.algo("opencv/SmartThumbnail/0.1").pipe(input).result 102 | # -> [binary byte sequence] 103 | ``` 104 | 105 | ### Error handling 106 | 107 | API errors and Algorithm exceptions will result in calls to `pipe` throwing an `AlgoException`: 108 | 109 | ```python 110 | client.algo('util/whoopsWrongAlgo').pipe('Hello, world!') 111 | # Algorithmia.algo_response.AlgoException: algorithm algo://util/whoopsWrongAlgo not found 112 | ``` 113 | 114 | ### Request options 115 | 116 | The client exposes options that can configure algorithm requests. 117 | This includes support for changing the timeout or indicating that the API should include stdout in the response. 118 | 119 | ```python 120 | from Algorithmia.algorithm import OutputType 121 | response = client.algo('util/echo').set_options(timeout=60, stdout=False) 122 | print(response.metadata.stdout) 123 | ``` 124 | 125 | Note: `stdout=True` is only supported if you have access to the algorithm source. 126 | 127 | 128 | ## Working with data 129 | The Algorithmia client also provides a way to manage both Algorithmia hosted data 130 | and data from Dropbox or S3 accounts that you've connected to you Algorithmia account. 131 | 132 | ### Create directories 133 | Create directories by instantiating a `DataDirectory` object and calling `create()`: 134 | 135 | ```python 136 | client.dir("data://.my/foo").create() 137 | client.dir("dropbox://somefolder").create() 138 | ``` 139 | 140 | ### Upload files to a directory 141 | 142 | Upload files by calling `put` on a `DataFile` object, 143 | or by calling `putFile` on a `DataDirectory` object. 144 | 145 | ```python 146 | foo = client.dir("data://.my/foo") 147 | foo.file("remote_file").putFile("/path/to/myfile") 148 | foo.file("sample.txt").put("sample text contents") 149 | foo.file("binary_file").put(some_binary_data) 150 | ``` 151 | 152 | Note: you can instantiate a `DataFile` by either `client.file(path)` or `client.dir(path).file(filename)` 153 | 154 | 155 | ### Download contents of file 156 | 157 | Download files by calling `getString`, `getBytes`, `getJson`, or `getFile` on a `DataFile` object: 158 | 159 | ```python 160 | foo = client.dir("data://.my/foo") 161 | sampleText = foo.file("sample.txt").getString() # String object 162 | binaryContent = foo.file("binary_file").getBytes() # Binary data 163 | tempFile = foo.file("myfile").getFile() # Open file descriptor 164 | ``` 165 | 166 | ### Delete files and directories 167 | 168 | Delete files and directories by calling `delete` on their respective `DataFile` or `DataDirectory` object. 169 | DataDirectories take an optional `force` parameter that indicates whether the directory should be deleted 170 | if it contains files or other directories. 171 | 172 | ```python 173 | foo = client.dir("data://.my/foo") 174 | foo.file("sample.txt").delete() 175 | foo.delete(true) // true implies force deleting the directory and its contents 176 | ``` 177 | 178 | 179 | ### List directory contents 180 | 181 | Iterate over the contents of a directory using the iterated returned by calling `list`, `files`, or `dirs` 182 | on a `DataDirectory` object: 183 | 184 | ```python 185 | foo = client.dir("data://.my/foo") 186 | 187 | # List files in "foo" 188 | for file in foo.files(): 189 | print(file.path + " at URL: " + file.url + " last modified " + file.last_modified) 190 | 191 | # List directories in "foo" 192 | for file in foo.dirs(): 193 | print(dir.path + " at URL: " + file.url) 194 | 195 | # List everything in "foo" 196 | for entry in foo.list(): 197 | print(entry.path + " at URL: " + entry.url) 198 | ``` 199 | 200 | ### Manage directory permissions 201 | 202 | Directory permissions may be set when creating a directory, or may be updated on already existing directories. 203 | 204 | ```python 205 | from Algorithmia.acl import ReadAcl, AclType 206 | foo = client.dir("data://.my/foo") 207 | # ReadAcl.public is a wrapper for Acl(AclType.public) to make things easier 208 | foo.create(ReadAcl.public) 209 | 210 | acl = foo.get_permissions() # Acl object 211 | acl.read_acl == AclType.public # True 212 | 213 | foo.update_permissions(ReadAcl.private) 214 | foo.get_permissions().read_acl == AclType.private # True 215 | ``` 216 | 217 | # Algorithmia CLI 218 | 219 | Algorithmia CLI is a cross-platform tool for interfacing with algorithms and the Algorithmia Data API. 220 | 221 | ## Configure Authentication 222 | 223 | In order to make calls with the CLI, you'll need to configure the authentication with an API key. If you don't already have an API key, get started by signing up for an account at [Algorithmia.com](https://algorithmia.com). Once you've completed the sign up process, copy the API key from your account dashboard. 224 | 225 | Begin the configuration process by running the command `algo auth`. 226 | You will see an interactive prompt to guide you through setting up a default profile: 227 | 228 | ``` 229 | $ algo auth 230 | Configuring authentication for profile: 'default' 231 | Enter API Endpoint [https://api.algorithmia.com]: 232 | Enter API Key: 233 | (optional) enter path to custom CA certificate: 234 | Profile is ready to use. Test with 'algo ls' 235 | ``` 236 | 237 | See [Using multiple profiles](#using-multiple-profiles) for instructions on how to set authenticate and use more than one profile with the Algorithmia CLI tool. 238 | 239 | ## Usage 240 | 241 | To call an algorithm from the CLI, use the command syntax: `algo run`, followed by the algorithm’s username and algorithm name, the data options, and finally the input. Here is a basic example calling the [Factor algorithm](https://algorithmia.com/algorithms/kenny/Factor): 242 | 243 | ```text 244 | $ algo run kenny/factor -d 19635 245 | [3,5,7,11,17] 246 | ``` 247 | 248 | Run `algo run --help` to see more command options or view the following [Options](#options) section. 249 | 250 | ### Options 251 | 252 | #### Input Data Options 253 | The Algorithmia CLI supports JSON, text, and binary data, as well as an option to auto-detect the data type. 254 | 255 | | Option Flag | Description | 256 | | :------------ | :--------------- | 257 | | -d, --data | If the data parses as JSON, assume JSON, else if the data is valid UTF-8, assume text, else assume binary | 258 | | -D, --data-file | Same as --data, but the input data is read from a file | 259 | 260 | You may also explictly specify the input type as text (`-t`/`-T`), json (`-j`/`-J`), or binary (`-b`/`-B`) instead of using the auto-detection (`-d`/`-D`). 261 | 262 | #### Output Options 263 | 264 | The algorithm result is printed to STDOUT by defauft. Additional notices may be printed to STDERR. If you'd like to output the result to a file, use the output option flag followed by a filename: 265 | 266 | ```text 267 | $ algo run kenny/factor -d 17 --output results.txt 268 | ``` 269 | 270 | | Option Flag | Description | 271 | | :------------ |:--------------- | 272 | | --debug | Print algorithm's STDOUT (author-only) | 273 | | -o, --output | Print result to a file | 274 | 275 | #### Other Options 276 | 277 | | Option Flag | Description | 278 | | :------------ |:--------------- | 279 | | --timeout | Sets algorithm timeout 280 | 281 | #### Examples: 282 | 283 | ```text 284 | $ algo run kenny/factor/0.1.0 -d '79' Run algorithm with specified version & data input 285 | $ algo run anowell/Dijkstra -D routes.json Run algorithm with file input 286 | $ algo run anowell/Dijkstra -D - < routes.json Same as above but using STDIN 287 | $ algo run opencv/SmartThumbnail -D in.png -o out.png Runs algorithm with binary files as input 288 | $ algo run kenny/factor -d 17 --timeout 2 Runs algorithm with a timeout of 2 seconds 289 | ``` 290 | 291 | 292 | ## The Algorithmia Data API 293 | 294 | Use the Algorithmia CLI to interact with the Algorithmia Data API. You can use the CLI to create and manage your data directories. 295 | 296 | **Data commands include:** 297 | 298 | | Command | Description | 299 | | :------------ |:--------------- | 300 | | ls | List contents of a data directory | 301 | | mkdir | Create a data directory | 302 | | rmdir | Delete a data directory | 303 | | rm | Remove a file from a data directory | 304 | | cp | Copy file(s) to or from a data directory | 305 | | cat | Concatenate & print file(s) in a directory | 306 | 307 | ### Examples of the Algorithmia Data API usage: 308 | 309 | Create a data directory: 310 | ```text 311 | $ algo mkdir .my/cuteAnimals 312 | 313 | Created directory data://.my/cuteAnimals 314 | ``` 315 | 316 | Copy a file from your local directory to the new data directory: 317 | 318 | ```text 319 | $ algo cp chubby_kittens.jpg data://.my/cuteAnimals 320 | 321 | Uploaded data://.my/cuteAnimals/chubby_kittens.jpg 322 | ``` 323 | 324 | ## Using multiple profiles 325 | 326 | ### Add additional profiles 327 | 328 | With the Algorithmia CLI, you can configure multiple custom profiles to use. To add a new profile, simply specify a profile to `algo auth` follow the same interactive prompt. 329 | 330 | ```text 331 | algo auth --profile second_user 332 | Configuring authentication for profile: 'second_user' 333 | Enter API Endpoint [https://api.algorithmia.com]: 334 | Enter API Key: 335 | (optional) enter path to custom CA certificate: 336 | ``` 337 | 338 | Now you may use `algo ls --profile second_user` to list files in your `second_user` account. For more information, see the auth command help with `algo auth --help`. 339 | 340 | ### Using profiles in commands 341 | 342 | When running commands, the Algorithmia CLI will use the default profile unless otherwise specified with the `--profile ` option. See the following example: 343 | 344 | ```text 345 | $ algo run kenny/factor -d 17 --profile second_user 346 | [17] 347 | ``` 348 | 349 | 350 | 351 | # Running tests 352 | 353 | Make sure you have `numpy` installed before running `datafile_test.py` 354 | ```bash 355 | export ALGORITHMIA_API_KEY={{Your API key here}} 356 | cd Test 357 | python acl_test.py 358 | python algo_test.py 359 | python datadirectorytest.py 360 | python datafile_test.py 361 | python utiltest.py 362 | ``` 363 | -------------------------------------------------------------------------------- /Test/__init__.py: -------------------------------------------------------------------------------- 1 | # change python path here? 2 | -------------------------------------------------------------------------------- /Test/api/__init__.py: -------------------------------------------------------------------------------- 1 | from .app import start_webserver_reg as start_webserver_reg 2 | from .self_signed_app import start_webserver_self_signed as start_webserver_self_signed 3 | 4 | -------------------------------------------------------------------------------- /Test/api/app.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Request, status 2 | from typing import Optional 3 | from fastapi.responses import Response, JSONResponse 4 | import json 5 | import base64 6 | from multiprocessing import Process 7 | import uvicorn 8 | 9 | regular_app = FastAPI() 10 | 11 | 12 | def start_webserver_reg(): 13 | def _start_webserver(): 14 | uvicorn.run(regular_app, host="127.0.0.1", port=8080, log_level="debug") 15 | 16 | p = Process(target=_start_webserver) 17 | p.start() 18 | return p 19 | 20 | 21 | @regular_app.post("/v1/algo/{username}/{algoname}") 22 | async def process_algo_req(request: Request, username, algoname, output: Optional[str] = None): 23 | metadata = {"request_id": "req-55c0480d-6af3-4a21-990a-5c51d29f5725", "duration": 0.000306774} 24 | content_type = request.headers['Content-Type'] 25 | auth = request.headers.get('Authorization', None) 26 | if auth is None: 27 | return {"error": {"message": "authorization required"}} 28 | request = await request.body() 29 | if output and output == "void": 30 | return {"async": "abcd123", "request_id": "req-55c0480d-6af3-4a21-990a-5c51d29f5725"} 31 | elif output and output == "raw": 32 | return Response(request.decode(), status_code=200) 33 | elif algoname == "500": 34 | return Response("Internal Server Error", status_code=500) 35 | elif algoname == "raise_exception": 36 | return {"error": {"message": "This is an exception"}} 37 | else: 38 | if content_type != "application/octet-stream": 39 | request = request.decode('utf-8') 40 | if content_type == "text/plain": 41 | metadata['content_type'] = "text" 42 | elif content_type == "application/json": 43 | request = json.loads(request) 44 | metadata['content_type'] = "json" 45 | else: 46 | metadata['content_type'] = "binary" 47 | request = base64.b64encode(request) 48 | output = {"result": request, "metadata": metadata} 49 | return output 50 | 51 | 52 | @regular_app.post("/v1/algo/{username}/{algoname}/{githash}", status_code=status.HTTP_200_OK) 53 | async def process_hello_world(request: Request, username, algoname, githash): 54 | metadata = {"request_id": "req-55c0480d-6af3-4a21-990a-5c51d29f5725", "duration": 0.000306774, 55 | 'content_type': "text"} 56 | request = await request.body() 57 | request = request.decode('utf-8') 58 | return {"result": f"hello {request}", "metadata": metadata} 59 | 60 | 61 | ### Algorithm Routes 62 | @regular_app.get('/v1/algorithms/{username}/{algoname}') 63 | async def process_get_algo(username, algoname): 64 | if algoname == "echo" and username == 'quality': 65 | return {"id": "21df7a38-eab8-4ac8-954c-41a285535e69", "name": "echo", 66 | "details": {"summary": "", "label": "echo", "tagline": ""}, 67 | "settings": {"algorithm_callability": "public", "source_visibility": "closed", 68 | "package_set": "python36", "license": "apl", "royalty_microcredits": 0, 69 | "network_access": "full", "pipeline_enabled": True, "insights_enabled": False, 70 | "algorithm_environment": "067110e7-8969-4441-b3d6-5333f18a3db3"}, 71 | "version_info": {"semantic_version": "0.1.0", "git_hash": "0cfd7a6600f1fa05f9fe93016e661a9332c4ded2", 72 | "version_uuid": "e06d2808-bb5e-46ae-b7bc-f3d9968e3c6b"}, 73 | "build": {"build_id": "a9ae2c93-6f4e-42c0-ac54-baa4a66e53d3", "status": "succeeded", 74 | "commit_sha": "0cfd7a6600f1fa05f9fe93016e661a9332c4ded2", 75 | "started_at": "2022-05-08T22:43:09.050Z", "finished_at": "2022-05-08T22:43:28.646Z", 76 | "version_info": {"semantic_version": "0.1.0"}, "resource_type": "algorithm_build"}, 77 | "source": {"scm": {"id": "internal", "provider": "internal", "default": True, "enabled": True}}, 78 | "compilation": {"successful": True, "output": ""}, 79 | "self_link": "https://api.algorithmia.com/v1/algorithms/quality/echo/versions/0cfd7a6600f1fa05f9fe93016e661a9332c4ded2", 80 | "resource_type": "algorithm"} 81 | elif algoname == "echo": 82 | return JSONResponse(content={"error": {"id": "1cfb98c5-532e-4cbf-9192-fdd45b86969c", "code": 2001, 83 | "message": "Caller is not authorized to perform the operation"}}, 84 | status_code=403) 85 | else: 86 | return JSONResponse(content={"error": "No such algorithm"}, status_code=404) 87 | 88 | 89 | @regular_app.get("/v1/algorithms/{username}/{algoname}/builds/{buildid}") 90 | async def get_build_id(username, algoname, buildid): 91 | return {"status": "succeeded", "build_id": buildid, "commit_sha": "bcdadj", 92 | "started_at": "2021-09-27T22:54:20.786Z", "finished_at": "2021-09-27T22:54:40.898Z", 93 | "version_info": {"semantic_version": "0.1.1"}} 94 | 95 | 96 | @regular_app.get("/v1/algorithms/{username}/{algoname}/builds/{buildid}/logs") 97 | async def get_build_log(username, algoname, buildid): 98 | return {"logs": "This is a log"} 99 | 100 | 101 | @regular_app.get("/v1/algorithms/{username}/{algoname}/scm/status") 102 | async def get_scm_status(username, algoname): 103 | return {"scm_connection_status": "active"} 104 | 105 | 106 | @regular_app.get("/v1/scms") 107 | async def get_scms(): 108 | return {'results': [{'default': True, 'enabled': True, 'id': 'internal', 'name': '', 'provider': 'internal'}, 109 | {'default': False, 'enabled': True, 'id': 'github', 'name': 'https://github.com', 110 | 'provider': 'github', 'scm': {'client_id': '0ff25ba21ec67dbed6e2'}, 111 | 'oauth': {'client_id': '0ff25ba21ec67dbed6e2'}, 112 | 'urls': {'web': 'https://github.com', 'api': 'https://api.github.com', 113 | 'ssh': 'ssh://git@github.com'}}, 114 | {'default': False, 'enabled': True, 'id': 'aadebe70-007f-48ff-ba38-49007c6e0377', 115 | 'name': 'https://gitlab.com', 'provider': 'gitlab', 116 | 'scm': {'client_id': 'ca459576279bd99ed480236a267cc969f8322caad292fa5147cc7fdf7b530a7e'}, 117 | 'oauth': {'client_id': 'ca459576279bd99ed480236a267cc969f8322caad292fa5147cc7fdf7b530a7e'}, 118 | 'urls': {'web': 'https://gitlab.com', 'api': 'https://gitlab.com', 119 | 'ssh': 'ssh://git@gitlab.com'}}, 120 | {'default': False, 'enabled': True, 'id': '24ad1496-5a1d-43e2-9d96-42fce8e5484f', 121 | 'name': 'IQIVA Public GitLab', 'provider': 'gitlab', 122 | 'scm': {'client_id': '3341c989f9d28043d2597388aa4f43ce60a74830b981c4b7d79becf641959376'}, 123 | 'oauth': {'client_id': '3341c989f9d28043d2597388aa4f43ce60a74830b981c4b7d79becf641959376'}, 124 | 'urls': {'web': 'https://gitlab.com', 'api': 'https://gitlab.com', 125 | 'ssh': 'ssh://git@gitlab.com'}}, 126 | {'default': False, 'enabled': False, 'id': '83cd96ae-b1f4-4bd9-b9ca-6f7f25c37708', 127 | 'name': 'GitlabTest', 'provider': 'gitlab', 128 | 'scm': {'client_id': '5e257d6e168d579d439b7d38cdfa647e16573ae1dace6d93a30c5c60b4e5dd32'}, 129 | 'oauth': {'client_id': '5e257d6e168d579d439b7d38cdfa647e16573ae1dace6d93a30c5c60b4e5dd32'}, 130 | 'urls': {'web': 'https://gitlab.com', 'api': 'https://gitlab.com', 131 | 'ssh': 'ssh://git@gitlab.com'}}]} 132 | 133 | 134 | @regular_app.get("/v1/algorithms/{algo_id}/errors") 135 | async def get_algo_errors(algo_id): 136 | return JSONResponse(content={"error": {"message": "not found"}}, status_code=404) 137 | 138 | 139 | @regular_app.post("/v1/algorithms/{username}") 140 | async def create_algorithm(request: Request, username): 141 | payload = await request.json() 142 | return {"id": "2938ca9f-54c8-48cd-b0d0-0fb7f2255cdc", "name": payload["name"], 143 | "details": {"label": payload["details"]["label"]}, 144 | "settings": {"algorithm_callability": "private", "source_visibility": "open", 145 | "package_set": "tensorflow-gpu-2.3-python38", "license": "apl", "network_access": "isolated", 146 | "pipeline_enabled": False, "insights_enabled": False, 147 | "algorithm_environment": "fd980f4f-1f1c-4b2f-a128-d60b40c6567a"}, 148 | "source": {"scm": {"id": "internal", "provider": "internal", "default": True, "enabled": True}}, 149 | "resource_type": "algorithm"} 150 | 151 | 152 | @regular_app.put('/v1/algorithms/{username}/{algoname}') 153 | async def update_algorithm(request: Request, username, algoname): 154 | return { 155 | "id": "2938ca9f-54c8-48cd-b0d0-0fb7f2255cdc", 156 | "name": algoname, 157 | "details": { 158 | "summary": "Example Summary", 159 | "label": "QA", 160 | "tagline": "Example Tagline" 161 | }, 162 | "settings": { 163 | "algorithm_callability": "private", 164 | "source_visibility": "open", 165 | "package_set": "tensorflow-gpu-2.3-python38", 166 | "license": "apl", 167 | "network_access": "isolated", 168 | "pipeline_enabled": False, 169 | "insights_enabled": False, 170 | "algorithm_environment": "fd980f4f-1f1c-4b2f-a128-d60b40c6567a" 171 | }, 172 | "version_info": { 173 | "git_hash": "e85db9bca2fad519f540b445f30d12523e4dec9c", 174 | "version_uuid": "1d9cb91d-11ca-49cb-a7f4-28f67f277654" 175 | }, 176 | "source": { 177 | "scm": { 178 | "id": "internal", 179 | "provider": "internal", 180 | "default": True, 181 | "enabled": True 182 | } 183 | }, 184 | "compilation": { 185 | "successful": True, 186 | "output": "" 187 | }, 188 | "self_link": f"http://localhost:8080/v1/algorithms/{username}/{algoname}/versions/e85db9bca2fad519f540b445f30d12523e4dec9c", 189 | "resource_type": "algorithm" 190 | } 191 | 192 | 193 | @regular_app.post("/v1/algorithms/{username}/{algoname}/compile") 194 | async def compile_algorithm(username, algoname): 195 | return { 196 | "id": "2938ca9f-54c8-48cd-b0d0-0fb7f2255cdc", 197 | "name": algoname, 198 | "details": { 199 | "summary": "Example Summary", 200 | "label": "QA", 201 | "tagline": "Example Tagline" 202 | }, 203 | "settings": { 204 | "algorithm_callability": "private", 205 | "source_visibility": "open", 206 | "package_set": "tensorflow-gpu-2.3-python38", 207 | "license": "apl", 208 | "network_access": "isolated", 209 | "pipeline_enabled": False, 210 | "insights_enabled": False, 211 | "algorithm_environment": "fd980f4f-1f1c-4b2f-a128-d60b40c6567a" 212 | }, 213 | "version_info": { 214 | "git_hash": "e85db9bca2fad519f540b445f30d12523e4dec9c", 215 | "version_uuid": "1d9cb91d-11ca-49cb-a7f4-28f67f277654" 216 | }, 217 | "source": { 218 | "scm": { 219 | "id": "internal", 220 | "provider": "internal", 221 | "default": True, 222 | "enabled": True 223 | } 224 | }, 225 | "compilation": { 226 | "successful": True, 227 | "output": "" 228 | }, 229 | "self_link": f"http://localhost:8080/v1/algorithms/{username}/{algoname}/versions/e85db9bca2fad519f540b445f30d12523e4dec9c", 230 | "resource_type": "algorithm" 231 | } 232 | 233 | fail_cnt = 0 234 | 235 | @regular_app.post("/v1/algorithms/{username}/{algoname}/versions") 236 | async def publish_algorithm(request: Request, username, algoname): 237 | global fail_cnt 238 | if "failonce" == algoname and fail_cnt == 0: 239 | fail_cnt +=1 240 | return JSONResponse(content="This is an expected failure mode, try again", status_code=400) 241 | elif "failalways" == algoname: 242 | return JSONResponse(status_code=500) 243 | return {"id": "2938ca9f-54c8-48cd-b0d0-0fb7f2255cdc", "name": algoname, 244 | "details": {"summary": "Example Summary", "label": "QA", "tagline": "Example Tagline"}, 245 | "settings": {"algorithm_callability": "private", "source_visibility": "open", 246 | "package_set": "tensorflow-gpu-2.3-python38", "license": "apl", "network_access": "isolated", 247 | "pipeline_enabled": False, "insights_enabled": False, 248 | "algorithm_environment": "fd980f4f-1f1c-4b2f-a128-d60b40c6567a"}, 249 | "version_info": {"semantic_version": "0.1.0", "git_hash": "e85db9bca2fad519f540b445f30d12523e4dec9c", 250 | "release_notes": "created programmatically", "sample_input": "payload", 251 | "version_uuid": "e85db9bca2fad519f540b445f30d12523e4dec9c"}, 252 | "source": {"scm": {"id": "internal", "provider": "internal", "default": True, "enabled": True}}, 253 | "compilation": {"successful": True}, 254 | "self_link": f"http://localhost:8080/v1/algorithms/{username}/{algoname}/versions/e85db9bca2fad519f540b445f30d12523e4dec9c", 255 | "resource_type": "algorithm"} 256 | 257 | 258 | @regular_app.get("/v1/algorithms/{username}/{algoname}/versions") 259 | async def versions_of_algorithm(request: Request, username, algoname): 260 | return {"marker": None, "next_link": None, "results": [ 261 | {"id": "21df7a38-eab8-4ac8-954c-41a285535e69", "name": algoname, 262 | "details": {"summary": "", "label": algoname, "tagline": ""}, 263 | "settings": {"algorithm_callability": "public", "source_visibility": "closed", "package_set": "python36", 264 | "license": "apl", "royalty_microcredits": 0, "network_access": "full", "pipeline_enabled": True, 265 | "insights_enabled": False, "algorithm_environment": "067110e7-8969-4441-b3d6-5333f18a3db3"}, 266 | "version_info": {"semantic_version": "0.1.0", "git_hash": "0cfd7a6600f1fa05f9fe93016e661a9332c4ded2", 267 | "version_uuid": "e06d2808-bb5e-46ae-b7bc-f3d9968e3c6b"}, 268 | "build": {"build_id": "a9ae2c93-6f4e-42c0-ac54-baa4a66e53d3", "status": "succeeded", 269 | "commit_sha": "0cfd7a6600f1fa05f9fe93016e661a9332c4ded2", "started_at": "2022-05-08T22:43:09.050Z", 270 | "finished_at": "2022-05-08T22:43:28.646Z", "version_info": {"semantic_version": "0.1.0"}, 271 | "resource_type": "algorithm_build"}, 272 | "source": {"scm": {"id": "internal", "provider": "internal", "default": True, "enabled": True}}, 273 | "compilation": {"successful": True}, 274 | "self_link": f"https://api.algorithmia.com/v1/algorithms/{username}/{algoname}/versions" 275 | "/0cfd7a6600f1fa05f9fe93016e661a9332c4ded2", 276 | "resource_type": "algorithm"}]} 277 | 278 | 279 | @regular_app.get("/v1/algorithms/{username}/{algoname}/versions/{algohash}") 280 | async def get_algorithm_info(username, algoname, algohash): 281 | if algohash == "e85db9bca2fad519f540b445f30d12523e4dec9c": 282 | return {"id": "21df7a38-eab8-4ac8-954c-41a285535e69", "name": algoname, 283 | "details": {"summary": "", "label": algoname, "tagline": ""}, 284 | "settings": {"algorithm_callability": "public", "source_visibility": "closed", "language": "python3", 285 | "environment": "cpu", "package_set": "python36", "license": "apl", 286 | "royalty_microcredits": 0, "network_access": "full", "pipeline_enabled": True, 287 | "insights_enabled": False, 288 | "algorithm_environment": "067110e7-8969-4441-b3d6-5333f18a3db3"}, 289 | "version_info": {"semantic_version": "0.1.0", "git_hash": "0cfd7a6600f1fa05f9fe93016e661a9332c4ded2", 290 | "version_uuid": "e06d2808-bb5e-46ae-b7bc-f3d9968e3c6b"}, 291 | "build": {"build_id": "a9ae2c93-6f4e-42c0-ac54-baa4a66e53d3", "status": "succeeded", 292 | "commit_sha": "0cfd7a6600f1fa05f9fe93016e661a9332c4ded2", 293 | "started_at": "2022-05-08T22:43:09.050Z", "finished_at": "2022-05-08T22:43:28.646Z", 294 | "version_info": {"semantic_version": "0.1.0"}, "resource_type": "algorithm_build"}, 295 | "source": {"scm": {"id": "internal", "provider": "internal", "default": True, "enabled": True}}, 296 | "compilation": {"successful": True, "output": ""}, "resource_type": "algorithm"} 297 | else: 298 | return JSONResponse(content={"error": {"message": "not found"}}, status_code=404) 299 | 300 | 301 | ### Admin Routes 302 | @regular_app.post("/v1/users") 303 | async def create_user(request: Request): 304 | payload = await request.body() 305 | data = json.loads(payload) 306 | username = data['username'] 307 | email = data['email'] 308 | return { 309 | "id": "1e5c89ab-3d5c-4bad-b8a3-6c8a294d4418", 310 | "username": username, 311 | "email": email, 312 | "fullname": username, 313 | "self_link": f"http://localhost:8080/v1/users/{username}", "resource_type": "user" 314 | } 315 | 316 | 317 | @regular_app.get("/v1/users/{user_id}/errors") 318 | async def get_user_errors(user_id): 319 | return [] 320 | 321 | 322 | @regular_app.get("/v1/organization/types") 323 | async def get_org_types(): 324 | return [ 325 | {"id": "d0c85ea6-ddfa-11ea-a0c8-12a811be4db3", "name": "basic"}, 326 | {"id": "d0bff917-ddfa-11ea-a0c8-12a811be4db3", "name": "legacy"}, 327 | {"id": "d0c9d825-ddfa-11ea-a0c8-12a811be4db3", "name": "pro"} 328 | ] 329 | 330 | 331 | @regular_app.post("/v1/organizations") 332 | async def create_org(request: Request): 333 | payload = await request.body() 334 | data = json.loads(payload) 335 | org_name = data["org_name"] 336 | org_email = data["org_email"] 337 | return {"id": "55073c92-5f8e-4d7e-a14d-568f94924fd9", 338 | "org_name": org_name, 339 | "org_label": "some label", 340 | "org_contact_name": "Some owner", 341 | "org_email": org_email, 342 | "org_created_at": "2021-10-22T16:41:32", 343 | "org_url": None, 344 | "type_id": "d0c85ea6-ddfa-11ea-a0c8-12a811be4db3", 345 | "stripe_customer_id": None, 346 | "external_admin_group": None, 347 | "external_member_group": None, 348 | "external_id": None, 349 | "owner_ids": None, 350 | "resource_type": "organization", 351 | "self_link": "http://localhost:8080/v1/organizations/a_myOrg1542" 352 | } 353 | 354 | 355 | @regular_app.put("/v1/organizations/{orgname}/members/{username}") 356 | async def add_user_to_org(orgname, username): 357 | return Response(status_code=200) 358 | 359 | 360 | @regular_app.get("/v1/organizations/{orgname}/errors") 361 | async def org_errors(orgname): 362 | return [] 363 | 364 | 365 | @regular_app.put("/v1/organizations/{org_name}") 366 | async def edit_org(org_name): 367 | return Response(status_code=204) 368 | 369 | 370 | @regular_app.get("/v1/organizations/{org_name}") 371 | async def get_org_by_name(org_name): 372 | return { 373 | "id": "55073c92-5f8e-4d7e-a14d-568f94924fd9", 374 | "org_name": org_name, 375 | "org_label": "some label", 376 | "org_contact_name": "Some owner", 377 | "org_email": "a_myOrg1542@algo.com", 378 | "org_created_at": "2021-10-22T16:41:32", 379 | "org_url": None, 380 | "type_id": "d0c85ea6-ddfa-11ea-a0c8-12a811be4db3", 381 | "stripe_customer_id": None, 382 | "external_admin_group": None, 383 | "external_member_group": None, 384 | "external_id": None, 385 | "owner_ids": None, 386 | "resource_type": "organization", 387 | "self_link": "http://localhost:8080/v1/organizations/a_myOrg1542" 388 | } 389 | 390 | 391 | @regular_app.get("/v1/algorithms/{username}/{algoname}/builds/{buildid}/logs") 392 | async def get_build_log(username, algoname, buildid): 393 | return {"logs": "This is a log"} 394 | 395 | 396 | @regular_app.get("/v1/algorithm-environments/edge/languages") 397 | async def get_supported_langs(): 398 | return [{"name": "anaconda3", "display_name": "Conda (Environments) - beta", 399 | "configuration": "{\n \"display_name\": \"Conda (Environments) - beta\",\n \"req_files\": [\n \"environment.yml\"\n ],\n \"artifacts\": [\n {\"source\":\"/home/algo/.cache\", \"destination\":\"/home/algo/.cache/\"},\n {\"source\":\"/home/algo/anaconda_environment\", \"destination\": \"/home/algo/anaconda_environment/\"},\n {\"source\":\"/opt/algorithm\", \"destination\":\"/opt/algorithm/\"}\n ]\n}\n"}, 400 | {"name": "csharp-dotnet-core2", "display_name": "C# .NET Core 2.x+ (Environments)", 401 | "configuration": "{\n \"display_name\": \"C# .NET Core 2.x+ (Environments)\",\n \"artifacts\": [\n {\"source\":\"/opt/algorithm/bin/Release/*/*\", \"destination\":\"/opt/algorithm/\"},\n {\"source\":\"/opt/algorithm/resources\", \"destination\":\"/opt/algorithm/resources/\"},\n {\"source\":\"/home/algo/.nuget\", \"destination\":\"/home/algo/.nuget/\"}\n ]\n}\n"}, 402 | {"name": "java11", "display_name": "Java OpenJDK 11.0 (Environments)", 403 | "configuration": "{\n \"display_name\": \"Java OpenJDK 11.0 (Environments)\",\n \"artifacts\": [\n {\"source\":\"/opt/algorithm/target/*.jar\", \"destination\":\"/opt/algorithm/target/algorithm.jar\"},\n {\"source\":\"/opt/algorithm/target/lib\", \"destination\":\"/opt/algorithm/target/lib/\"}\n ]\n}\n"}, 404 | {"name": "python2", "display_name": "Python 2.x (Environments)", 405 | "configuration": "{\n \"display_name\": \"Python 2.x (Environments)\",\n \"req_files\": [\n \"requirements.txt\"\n ],\n \"artifacts\": [\n {\"source\":\"/home/algo/.local\", \"destination\":\"/home/algo/.local/\"},\n {\"source\":\"/opt/algorithm\", \"destination\":\"/opt/algorithm/\"}\n ]\n}\n"}, 406 | {"name": "python3", "display_name": "Python 3.x (Environments)", 407 | "configuration": "{\n \"display_name\": \"Python 3.x (Environments)\",\n \"req_files\": [\n \"requirements.txt\"\n ],\n \"artifacts\": [\n {\"source\":\"/home/algo/.local\", \"destination\":\"/home/algo/.local/\"},\n {\"source\":\"/opt/algorithm\", \"destination\":\"/opt/algorithm/\"}\n ]\n}\n"}, 408 | {"name": "r36", "display_name": "R 3.6.x (Environments)", 409 | "configuration": "{\n \"display_name\": \"R 3.6.x (Environments)\",\n \"req_files\": [\n \"packages.txt\"\n ],\n \"artifacts\": [\n {\"source\":\"/opt/algorithm\", \"destination\":\"/opt/algorithm/\"},\n {\"source\":\"/usr/local/lib/R/site-library\", \"destination\":\"/usr/local/lib/R/site-library/\"}\n ]\n}\n\n"}, 410 | {"name": "scala-2", "display_name": "Scala 2.x & sbt 1.3.x (Environments)", 411 | "configuration": "{\n \"display_name\": \"Scala 2.x & sbt 1.3.x (Environments)\",\n \"artifacts\": [\n {\"source\":\"/opt/algorithm/target/universal/stage\", \"destination\":\"/opt/algorithm/stage/\"}\n ]\n}\n\n"}] 412 | 413 | 414 | @regular_app.get("/v1/algorithm-environments/edge/languages/{language}/environments") 415 | async def get_environments_by_lang(language): 416 | return { 417 | "environments": [ 418 | { 419 | "id": "717d36e0-222c-44a0-9aa8-06f4ebc1b82a", 420 | "environment_specification_id": "f626effa-e519-431e-9d7a-0d3a7563ae1e", 421 | "display_name": "Python 2.7", 422 | "description": "Generic Python 2.7 installation", 423 | "created_at": "2020-12-21T21:47:53.239", 424 | "language": { 425 | "name": language, 426 | "display_name": "Python 2.x (Environments)", 427 | "configuration": "{\n \"display_name\": \"Python 2.x (Environments)\",\n \"req_files\": [\n " 428 | " \"requirements.txt\"\n ],\n \"artifacts\": [\n {" 429 | "\"source\":\"/home/algo/.local\", \"destination\":\"/home/algo/.local/\"}," 430 | "\n {\"source\":\"/opt/algorithm\", " 431 | "\"destination\":\"/opt/algorithm/\"}\n ]\n}\n " 432 | }, 433 | "machine_type": "CPU" 434 | }, 435 | { 436 | "id": "6f57e041-54e0-4e1a-8b2f-4589bb2c06f8", 437 | "environment_specification_id": "faf81400-eb15-4f64-81c0-3d4ed7181e77", 438 | "display_name": "Python 2.7 + GPU support", 439 | "description": "Python2.7 installation with CUDA 9.0 and CUDNN7", 440 | "created_at": "2020-08-14T07:22:32.955", 441 | "language": { 442 | "name": language, 443 | "display_name": "Python 2.x (Environments)", 444 | "configuration": "{\n \"display_name\": \"Python 2.x (Environments)\",\n \"req_files\": [\n " 445 | " \"requirements.txt\"\n ],\n \"artifacts\": [\n {" 446 | "\"source\":\"/home/algo/.local\", \"destination\":\"/home/algo/.local/\"}," 447 | "\n {\"source\":\"/opt/algorithm\", " 448 | "\"destination\":\"/opt/algorithm/\"}\n ]\n}\n " 449 | }, 450 | "machine_type": "GPU" 451 | } 452 | ] 453 | } 454 | 455 | 456 | @regular_app.get("/v1/secret-provider") 457 | async def get_service_providers(): 458 | return [ 459 | { 460 | "id": "dee00b6c-05c4-4de7-98d8-e4a3816ed75f", 461 | "name": "algorithmia_internal_secret_provider", 462 | "description": "Internal Secret Provider", 463 | "moduleName": "module", 464 | "factoryClassName": "com.algorithmia.plugin.sqlsecretprovider.InternalSecretProviderFactory", 465 | "interfaceVersion": "1.0", 466 | "isEnabled": True, 467 | "isDefault": True, 468 | "created": "2021-03-11T20:42:23Z", 469 | "modified": "2021-03-11T20:42:23Z" 470 | } 471 | ] 472 | 473 | 474 | @regular_app.get("/v1/algorithms/{algorithm_id}/secrets") 475 | async def get_secrets_for_algorithm(algorithm_id): 476 | return { 477 | "secrets": [ 478 | { 479 | "id": "45e97c47-3ae6-46be-87ee-8ab23746706b", 480 | "short_name": "MLOPS_SERVICE_URL", 481 | "description": "", 482 | "secret_key": "MLOPS_SERVICE_URL", 483 | "owner_type": "algorithm", 484 | "owner_id": "fa2cd80b-d22a-4548-b16a-45dbad2d3499", 485 | "provider_id": "dee00b6c-05c4-4de7-98d8-e4a3816ed75f", 486 | "created": "2022-07-22T14:36:01Z", 487 | "modified": "2022-07-22T14:36:01Z" 488 | }, 489 | { 490 | "id": "50dca60e-317f-4582-8854-5b83b4d182d0", 491 | "short_name": "deploy_id", 492 | "description": "", 493 | "secret_key": "DEPLOYMENT_ID", 494 | "owner_type": "algorithm", 495 | "owner_id": "fa2cd80b-d22a-4548-b16a-45dbad2d3499", 496 | "provider_id": "dee00b6c-05c4-4de7-98d8-e4a3816ed75f", 497 | "created": "2022-07-21T19:04:31Z", 498 | "modified": "2022-07-21T19:04:31Z" 499 | }, 500 | { 501 | "id": "5a75cdc8-ecc8-4715-8c4b-8038991f1608", 502 | "short_name": "model_path", 503 | "description": "", 504 | "secret_key": "MODEL_PATH", 505 | "owner_type": "algorithm", 506 | "owner_id": "fa2cd80b-d22a-4548-b16a-45dbad2d3499", 507 | "provider_id": "dee00b6c-05c4-4de7-98d8-e4a3816ed75f", 508 | "created": "2022-07-21T19:04:31Z", 509 | "modified": "2022-07-21T19:04:31Z" 510 | }, 511 | { 512 | "id": "80e51ed3-f6db-419d-9349-f59f4bbfdcbb", 513 | "short_name": "model_id", 514 | "description": "", 515 | "secret_key": "MODEL_ID", 516 | "owner_type": "algorithm", 517 | "owner_id": "fa2cd80b-d22a-4548-b16a-45dbad2d3499", 518 | "provider_id": "dee00b6c-05c4-4de7-98d8-e4a3816ed75f", 519 | "created": "2022-07-21T19:04:30Z", 520 | "modified": "2022-07-21T19:04:30Z" 521 | }, 522 | { 523 | "id": "8773c654-ea2f-4ac5-9ade-55dfc47fec9d", 524 | "short_name": "datarobot_api_token", 525 | "description": "", 526 | "secret_key": "DATAROBOT_MLOPS_API_TOKEN", 527 | "owner_type": "algorithm", 528 | "owner_id": "fa2cd80b-d22a-4548-b16a-45dbad2d3499", 529 | "provider_id": "dee00b6c-05c4-4de7-98d8-e4a3816ed75f", 530 | "created": "2022-07-21T19:04:31Z", 531 | "modified": "2022-07-21T19:04:31Z" 532 | } 533 | ] 534 | } 535 | 536 | 537 | @regular_app.post("/v1/algorithms/{algorithm_id}/secrets") 538 | async def set_algorithm_secret(algorithm_id): 539 | return { 540 | "id":"959af771-7cd8-4981-91c4-70def15bbcdc", 541 | "short_name":"tst", 542 | "description":"", 543 | "secret_key":"test", 544 | "owner_type":"algorithm", 545 | "owner_id":"fa2cd80b-d22a-4548-b16a-45dbad2d3499", 546 | "provider_id":"dee00b6c-05c4-4de7-98d8-e4a3816ed75f", 547 | "created":"2022-07-22T18:28:42Z", 548 | "modified":"2022-07-22T18:28:42Z" 549 | } -------------------------------------------------------------------------------- /Test/api/self_signed_app.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Request 2 | from typing import Optional 3 | from fastapi.responses import Response 4 | import json 5 | import base64 6 | from multiprocessing import Process 7 | import uvicorn 8 | 9 | self_signed_app = FastAPI() 10 | 11 | 12 | def start_webserver_self_signed(): 13 | def _start_webserver(): 14 | uvicorn.run(self_signed_app, host="127.0.0.1", port=8090, log_level="debug", 15 | ssl_certfile="Test/resources/cert.cert", ssl_keyfile="Test/resources/cert.key") 16 | 17 | p = Process(target=_start_webserver) 18 | p.start() 19 | return p 20 | 21 | 22 | @self_signed_app.post("/v1/algo/{username}/{algoname}") 23 | async def process_algo_req(request: Request, username, algoname, output: Optional[str] = None): 24 | metadata = {"request_id": "req-55c0480d-6af3-4a21-990a-5c51d29f5725", "duration": 0.000306774} 25 | content_type = request.headers['Content-Type'] 26 | auth = request.headers.get('Authorization', None) 27 | if auth is None: 28 | return {"error": {"message": "authorization required"}} 29 | request = await request.body() 30 | if output and output == "void": 31 | return {"async": "abcd123", "request_id": "req-55c0480d-6af3-4a21-990a-5c51d29f5725"} 32 | elif output and output == "raw": 33 | return Response(request.decode(), status_code=200) 34 | elif algoname == "500": 35 | return Response("Internal Server Error", status_code=500) 36 | elif algoname == "raise_exception": 37 | return {"error": {"message": "This is an exception"}} 38 | else: 39 | if content_type != "application/octet-stream": 40 | request = request.decode('utf-8') 41 | if content_type == "text/plain": 42 | metadata['content_type'] = "text" 43 | elif content_type == "application/json": 44 | request = json.loads(request) 45 | metadata['content_type'] = "json" 46 | else: 47 | metadata['content_type'] = "binary" 48 | request = base64.b64encode(request) 49 | output = {"result": request, "metadata": metadata} 50 | return output 51 | 52 | 53 | @self_signed_app.post("/v1/algo/{username}/{algoname}/{githash}") 54 | async def process_hello_world(request: Request, username, algoname, githash): 55 | metadata = {"request_id": "req-55c0480d-6af3-4a21-990a-5c51d29f5725", "duration": 0.000306774, 56 | 'content_type': "text"} 57 | request = await request.body() 58 | request = request.decode('utf-8') 59 | return {"result": f"hello {request}", "metadata": metadata} 60 | 61 | 62 | ### Algorithm Routes 63 | @self_signed_app.get('/v1/algorithms/{username}/{algoname}') 64 | async def process_get_algo(request: Request, username, algoname): 65 | if algoname == "echo": 66 | return {"id": "21df7a38-eab8-4ac8-954c-41a285535e69", "name": "echo", 67 | "details": {"summary": "", "label": "echo", "tagline": ""}, 68 | "settings": {"algorithm_callability": "public", "source_visibility": "closed", 69 | "package_set": "python36", "license": "apl", "royalty_microcredits": 0, 70 | "network_access": "full", "pipeline_enabled": True, "insights_enabled": False, 71 | "algorithm_environment": "067110e7-8969-4441-b3d6-5333f18a3db3"}, 72 | "version_info": {"semantic_version": "0.1.0", "git_hash": "0cfd7a6600f1fa05f9fe93016e661a9332c4ded2", 73 | "version_uuid": "e06d2808-bb5e-46ae-b7bc-f3d9968e3c6b"}, 74 | "build": {"build_id": "a9ae2c93-6f4e-42c0-ac54-baa4a66e53d3", "status": "succeeded", 75 | "commit_sha": "0cfd7a6600f1fa05f9fe93016e661a9332c4ded2", 76 | "started_at": "2022-05-08T22:43:09.050Z", "finished_at": "2022-05-08T22:43:28.646Z", 77 | "version_info": {"semantic_version": "0.1.0"}, "resource_type": "algorithm_build"}, 78 | "source": {"scm": {"id": "internal", "provider": "internal", "default": True, "enabled": True}}, 79 | "compilation": {"successful": True, "output": ""}, 80 | "self_link": "https://api.algorithmia.com/v1/algorithms/quality/echo/versions/0cfd7a6600f1fa05f9fe93016e661a9332c4ded2", 81 | "resource_type": "algorithm"} 82 | else: 83 | return {"error": "No such algorithm"} 84 | 85 | 86 | @self_signed_app.get("/v1/algorithms/{username}/{algoname}/builds/{buildid}") 87 | async def get_build_id(username, algoname, buildid): 88 | return {"status": "succeeded", "build_id": buildid, "commit_sha": "bcdadj", 89 | "started_at": "2021-09-27T22:54:20.786Z", "finished_at": "2021-09-27T22:54:40.898Z", 90 | "version_info": {"semantic_version": "0.1.1"}} 91 | 92 | 93 | @self_signed_app.get("/v1/algorithms/{username}/{algoname}/builds/{buildid}/logs") 94 | async def get_build_log(username, algoname, buildid): 95 | return {"logs": "This is a log"} 96 | 97 | 98 | @self_signed_app.get("/v1/algorithms/{username}/{algoname}/scm/status") 99 | async def get_scm_status(username, algoname): 100 | return {"scm_connection_status": "active"} 101 | 102 | 103 | @self_signed_app.get("/v1/algorithms/{algo_id}/errors") 104 | async def get_algo_errors(algo_id): 105 | return {"error": {"message": "not found"}} 106 | 107 | 108 | @self_signed_app.post("/v1/algorithms/{username}") 109 | async def create_algorithm(request: Request, username): 110 | payload = await request.json() 111 | return {"id": "2938ca9f-54c8-48cd-b0d0-0fb7f2255cdc", "name": payload["name"], 112 | "details": {"label": payload["details"]["label"]}, 113 | "settings": {"algorithm_callability": "private", "source_visibility": "open", 114 | "package_set": "tensorflow-gpu-2.3-python38", "license": "apl", "network_access": "isolated", 115 | "pipeline_enabled": False, "insights_enabled": False, 116 | "algorithm_environment": "fd980f4f-1f1c-4b2f-a128-d60b40c6567a"}, 117 | "source": {"scm": {"id": "internal", "provider": "internal", "default": True, "enabled": True}}, 118 | "resource_type": "algorithm"} 119 | 120 | 121 | @self_signed_app.post("/v1/algorithms/{username}/{algoname}/compile") 122 | async def compile_algorithm(username, algoname): 123 | return { 124 | "id": "2938ca9f-54c8-48cd-b0d0-0fb7f2255cdc", 125 | "name": algoname, 126 | "details": { 127 | "summary": "Example Summary", 128 | "label": "QA", 129 | "tagline": "Example Tagline" 130 | }, 131 | "settings": { 132 | "algorithm_callability": "private", 133 | "source_visibility": "open", 134 | "package_set": "tensorflow-gpu-2.3-python38", 135 | "license": "apl", 136 | "network_access": "isolated", 137 | "pipeline_enabled": False, 138 | "insights_enabled": False, 139 | "algorithm_environment": "fd980f4f-1f1c-4b2f-a128-d60b40c6567a" 140 | }, 141 | "version_info": { 142 | "git_hash": "e85db9bca2fad519f540b445f30d12523e4dec9c", 143 | "version_uuid": "1d9cb91d-11ca-49cb-a7f4-28f67f277654" 144 | }, 145 | "source": { 146 | "scm": { 147 | "id": "internal", 148 | "provider": "internal", 149 | "default": True, 150 | "enabled": True 151 | } 152 | }, 153 | "compilation": { 154 | "successful": True, 155 | "output": "" 156 | }, 157 | "self_link": f"http://localhost:8080/v1/algorithms/{username}/{algoname}/versions/e85db9bca2fad519f540b445f30d12523e4dec9c", 158 | "resource_type": "algorithm" 159 | } 160 | 161 | 162 | @self_signed_app.post("/v1/algorithms/{username}/{algoname}/versions") 163 | async def publish_algorithm(request: Request, username, algoname): 164 | return {"id": "2938ca9f-54c8-48cd-b0d0-0fb7f2255cdc", "name": algoname, 165 | "details": {"summary": "Example Summary", "label": "QA", "tagline": "Example Tagline"}, 166 | "settings": {"algorithm_callability": "private", "source_visibility": "open", 167 | "package_set": "tensorflow-gpu-2.3-python38", "license": "apl", "network_access": "isolated", 168 | "pipeline_enabled": False, "insights_enabled": False, 169 | "algorithm_environment": "fd980f4f-1f1c-4b2f-a128-d60b40c6567a"}, 170 | "version_info": {"semantic_version": "0.1.0", "git_hash": "e85db9bca2fad519f540b445f30d12523e4dec9c", 171 | "release_notes": "created programmatically", "sample_input": "payload", 172 | "version_uuid": "e85db9bca2fad519f540b445f30d12523e4dec9c"}, 173 | "source": {"scm": {"id": "internal", "provider": "internal", "default": True, "enabled": True}}, 174 | "compilation": {"successful": True}, 175 | "self_link": f"http://localhost:8080/v1/algorithms/{username}/{algoname}/versions/e85db9bca2fad519f540b445f30d12523e4dec9c", 176 | "resource_type": "algorithm"} 177 | 178 | 179 | @self_signed_app.get("/v1/algorithms/{username}/{algoname}/versions") 180 | async def versions_of_algorithm(request: Request, username, algoname): 181 | return {"marker": None, "next_link": None, "results": [ 182 | {"id": "21df7a38-eab8-4ac8-954c-41a285535e69", "name": algoname, 183 | "details": {"summary": "", "label": algoname, "tagline": ""}, 184 | "settings": {"algorithm_callability": "public", "source_visibility": "closed", "package_set": "python36", 185 | "license": "apl", "royalty_microcredits": 0, "network_access": "full", "pipeline_enabled": True, 186 | "insights_enabled": False, "algorithm_environment": "067110e7-8969-4441-b3d6-5333f18a3db3"}, 187 | "version_info": {"semantic_version": "0.1.0", "git_hash": "0cfd7a6600f1fa05f9fe93016e661a9332c4ded2", 188 | "version_uuid": "e06d2808-bb5e-46ae-b7bc-f3d9968e3c6b"}, 189 | "build": {"build_id": "a9ae2c93-6f4e-42c0-ac54-baa4a66e53d3", "status": "succeeded", 190 | "commit_sha": "0cfd7a6600f1fa05f9fe93016e661a9332c4ded2", "started_at": "2022-05-08T22:43:09.050Z", 191 | "finished_at": "2022-05-08T22:43:28.646Z", "version_info": {"semantic_version": "0.1.0"}, 192 | "resource_type": "algorithm_build"}, 193 | "source": {"scm": {"id": "internal", "provider": "internal", "default": True, "enabled": True}}, 194 | "compilation": {"successful": True}, 195 | "self_link": f"https://api.algorithmia.com/v1/algorithms/{username}/{algoname}/versions" 196 | "/0cfd7a6600f1fa05f9fe93016e661a9332c4ded2", 197 | "resource_type": "algorithm"}]} 198 | 199 | 200 | @self_signed_app.get("/v1/algorithms/{username}/{algoname}/versions/{algohash}") 201 | async def get_algorithm_info(username, algoname, algohash): 202 | if algohash == "e85db9bca2fad519f540b445f30d12523e4dec9c": 203 | return {"id": "21df7a38-eab8-4ac8-954c-41a285535e69", "name": algoname, 204 | "details": {"summary": "", "label": algoname, "tagline": ""}, 205 | "settings": {"algorithm_callability": "public", "source_visibility": "closed", "language": "python3", 206 | "environment": "cpu", "package_set": "python36", "license": "apl", 207 | "royalty_microcredits": 0, "network_access": "full", "pipeline_enabled": True, 208 | "insights_enabled": False, 209 | "algorithm_environment": "067110e7-8969-4441-b3d6-5333f18a3db3"}, 210 | "version_info": {"semantic_version": "0.1.0", "git_hash": "0cfd7a6600f1fa05f9fe93016e661a9332c4ded2", 211 | "version_uuid": "e06d2808-bb5e-46ae-b7bc-f3d9968e3c6b"}, 212 | "build": {"build_id": "a9ae2c93-6f4e-42c0-ac54-baa4a66e53d3", "status": "succeeded", 213 | "commit_sha": "0cfd7a6600f1fa05f9fe93016e661a9332c4ded2", 214 | "started_at": "2022-05-08T22:43:09.050Z", "finished_at": "2022-05-08T22:43:28.646Z", 215 | "version_info": {"semantic_version": "0.1.0"}, "resource_type": "algorithm_build"}, 216 | "source": {"scm": {"id": "internal", "provider": "internal", "default": True, "enabled": True}}, 217 | "compilation": {"successful": True, "output": ""}, "resource_type": "algorithm"} 218 | else: 219 | return {"error": {"message": "not found"}} 220 | 221 | 222 | ### Admin Routes 223 | @self_signed_app.post("/v1/users") 224 | async def create_user(request: Request): 225 | payload = await request.body() 226 | data = json.loads(payload) 227 | username = data['username'] 228 | email = data['email'] 229 | return { 230 | "id": "1e5c89ab-3d5c-4bad-b8a3-6c8a294d4418", 231 | "username": username, 232 | "email": email, 233 | "fullname": username, 234 | "self_link": f"http://localhost:8080/v1/users/{username}", "resource_type": "user" 235 | } 236 | 237 | 238 | @self_signed_app.get("/v1/users/{user_id}/errors") 239 | async def get_user_errors(user_id): 240 | return [] 241 | 242 | 243 | @self_signed_app.get("/v1/organization/types") 244 | async def get_org_types(): 245 | return [ 246 | {"id": "d0c85ea6-ddfa-11ea-a0c8-12a811be4db3", "name": "basic"}, 247 | {"id": "d0bff917-ddfa-11ea-a0c8-12a811be4db3", "name": "legacy"}, 248 | {"id": "d0c9d825-ddfa-11ea-a0c8-12a811be4db3", "name": "pro"} 249 | ] 250 | 251 | 252 | @self_signed_app.post("/v1/organizations") 253 | async def create_org(request: Request): 254 | payload = await request.body() 255 | data = json.loads(payload) 256 | org_name = data["org_name"] 257 | org_email = data["org_email"] 258 | return {"id": "55073c92-5f8e-4d7e-a14d-568f94924fd9", 259 | "org_name": org_name, 260 | "org_label": "some label", 261 | "org_contact_name": "Some owner", 262 | "org_email": org_email, 263 | "org_created_at": "2021-10-22T16:41:32", 264 | "org_url": None, 265 | "type_id": "d0c85ea6-ddfa-11ea-a0c8-12a811be4db3", 266 | "stripe_customer_id": None, 267 | "external_admin_group": None, 268 | "external_member_group": None, 269 | "external_id": None, 270 | "owner_ids": None, 271 | "resource_type": "organization", 272 | "self_link": "http://localhost:8080/v1/organizations/a_myOrg1542" 273 | } 274 | 275 | 276 | @self_signed_app.put("/v1/organizations/{orgname}/members/{username}") 277 | async def add_user_to_org(orgname, username): 278 | return Response(status_code=200) 279 | 280 | 281 | @self_signed_app.get("/v1/organizations/{orgname}/errors") 282 | async def org_errors(orgname): 283 | return [] 284 | 285 | 286 | @self_signed_app.put("/v1/organizations/{org_name}") 287 | async def edit_org(org_name): 288 | return Response(status_code=204) 289 | 290 | 291 | @self_signed_app.get("/v1/organizations/{org_name}") 292 | async def get_org_by_name(org_name): 293 | return { 294 | "id": "55073c92-5f8e-4d7e-a14d-568f94924fd9", 295 | "org_name": org_name, 296 | "org_label": "some label", 297 | "org_contact_name": "Some owner", 298 | "org_email": "a_myOrg1542@algo.com", 299 | "org_created_at": "2021-10-22T16:41:32", 300 | "org_url": None, 301 | "type_id": "d0c85ea6-ddfa-11ea-a0c8-12a811be4db3", 302 | "stripe_customer_id": None, 303 | "external_admin_group": None, 304 | "external_member_group": None, 305 | "external_id": None, 306 | "owner_ids": None, 307 | "resource_type": "organization", 308 | "self_link": "http://localhost:8080/v1/organizations/a_myOrg1542" 309 | } 310 | 311 | 312 | @self_signed_app.get("/v1/algorithms/{username}/{algoname}/builds/{buildid}/logs") 313 | async def get_build_log(username, algoname, buildid): 314 | return {"logs": "This is a log"} 315 | 316 | 317 | @self_signed_app.get("/v1/algorithm-environments/edge/languages") 318 | async def get_supported_langs(): 319 | return [{"name": "anaconda3", "display_name": "Conda (Environments) - beta", 320 | "configuration": "{\n \"display_name\": \"Conda (Environments) - beta\",\n \"req_files\": [\n \"environment.yml\"\n ],\n \"artifacts\": [\n {\"source\":\"/home/algo/.cache\", \"destination\":\"/home/algo/.cache/\"},\n {\"source\":\"/home/algo/anaconda_environment\", \"destination\": \"/home/algo/anaconda_environment/\"},\n {\"source\":\"/opt/algorithm\", \"destination\":\"/opt/algorithm/\"}\n ]\n}\n"}, 321 | {"name": "csharp-dotnet-core2", "display_name": "C# .NET Core 2.x+ (Environments)", 322 | "configuration": "{\n \"display_name\": \"C# .NET Core 2.x+ (Environments)\",\n \"artifacts\": [\n {\"source\":\"/opt/algorithm/bin/Release/*/*\", \"destination\":\"/opt/algorithm/\"},\n {\"source\":\"/opt/algorithm/resources\", \"destination\":\"/opt/algorithm/resources/\"},\n {\"source\":\"/home/algo/.nuget\", \"destination\":\"/home/algo/.nuget/\"}\n ]\n}\n"}, 323 | {"name": "java11", "display_name": "Java OpenJDK 11.0 (Environments)", 324 | "configuration": "{\n \"display_name\": \"Java OpenJDK 11.0 (Environments)\",\n \"artifacts\": [\n {\"source\":\"/opt/algorithm/target/*.jar\", \"destination\":\"/opt/algorithm/target/algorithm.jar\"},\n {\"source\":\"/opt/algorithm/target/lib\", \"destination\":\"/opt/algorithm/target/lib/\"}\n ]\n}\n"}, 325 | {"name": "python2", "display_name": "Python 2.x (Environments)", 326 | "configuration": "{\n \"display_name\": \"Python 2.x (Environments)\",\n \"req_files\": [\n \"requirements.txt\"\n ],\n \"artifacts\": [\n {\"source\":\"/home/algo/.local\", \"destination\":\"/home/algo/.local/\"},\n {\"source\":\"/opt/algorithm\", \"destination\":\"/opt/algorithm/\"}\n ]\n}\n"}, 327 | {"name": "python3", "display_name": "Python 3.x (Environments)", 328 | "configuration": "{\n \"display_name\": \"Python 3.x (Environments)\",\n \"req_files\": [\n \"requirements.txt\"\n ],\n \"artifacts\": [\n {\"source\":\"/home/algo/.local\", \"destination\":\"/home/algo/.local/\"},\n {\"source\":\"/opt/algorithm\", \"destination\":\"/opt/algorithm/\"}\n ]\n}\n"}, 329 | {"name": "r36", "display_name": "R 3.6.x (Environments)", 330 | "configuration": "{\n \"display_name\": \"R 3.6.x (Environments)\",\n \"req_files\": [\n \"packages.txt\"\n ],\n \"artifacts\": [\n {\"source\":\"/opt/algorithm\", \"destination\":\"/opt/algorithm/\"},\n {\"source\":\"/usr/local/lib/R/site-library\", \"destination\":\"/usr/local/lib/R/site-library/\"}\n ]\n}\n\n"}, 331 | {"name": "scala-2", "display_name": "Scala 2.x & sbt 1.3.x (Environments)", 332 | "configuration": "{\n \"display_name\": \"Scala 2.x & sbt 1.3.x (Environments)\",\n \"artifacts\": [\n {\"source\":\"/opt/algorithm/target/universal/stage\", \"destination\":\"/opt/algorithm/stage/\"}\n ]\n}\n\n"}] 333 | 334 | 335 | @self_signed_app.get("/v1/algorithm-environments/edge/languages/{language}/environments") 336 | async def get_environments_by_lang(language): 337 | return { 338 | "environments": [ 339 | { 340 | "id": "717d36e0-222c-44a0-9aa8-06f4ebc1b82a", 341 | "environment_specification_id": "f626effa-e519-431e-9d7a-0d3a7563ae1e", 342 | "display_name": "Python 2.7", 343 | "description": "Generic Python 2.7 installation", 344 | "created_at": "2020-12-21T21:47:53.239", 345 | "language": { 346 | "name": language, 347 | "display_name": "Python 2.x (Environments)", 348 | "configuration": "{\n \"display_name\": \"Python 2.x (Environments)\",\n \"req_files\": [\n " 349 | " \"requirements.txt\"\n ],\n \"artifacts\": [\n {" 350 | "\"source\":\"/home/algo/.local\", \"destination\":\"/home/algo/.local/\"}," 351 | "\n {\"source\":\"/opt/algorithm\", " 352 | "\"destination\":\"/opt/algorithm/\"}\n ]\n}\n " 353 | }, 354 | "machine_type": "CPU" 355 | }, 356 | { 357 | "id": "6f57e041-54e0-4e1a-8b2f-4589bb2c06f8", 358 | "environment_specification_id": "faf81400-eb15-4f64-81c0-3d4ed7181e77", 359 | "display_name": "Python 2.7 + GPU support", 360 | "description": "Python2.7 installation with CUDA 9.0 and CUDNN7", 361 | "created_at": "2020-08-14T07:22:32.955", 362 | "language": { 363 | "name": language, 364 | "display_name": "Python 2.x (Environments)", 365 | "configuration": "{\n \"display_name\": \"Python 2.x (Environments)\",\n \"req_files\": [\n " 366 | " \"requirements.txt\"\n ],\n \"artifacts\": [\n {" 367 | "\"source\":\"/home/algo/.local\", \"destination\":\"/home/algo/.local/\"}," 368 | "\n {\"source\":\"/opt/algorithm\", " 369 | "\"destination\":\"/opt/algorithm/\"}\n ]\n}\n " 370 | }, 371 | "machine_type": "GPU" 372 | } 373 | ] 374 | } 375 | -------------------------------------------------------------------------------- /Test/conftest.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from time import sleep 3 | import os, signal 4 | if sys.version_info.major >= 3: 5 | from Test.api import start_webserver_reg, start_webserver_self_signed 6 | import pytest 7 | 8 | @pytest.fixture(scope='package', autouse=True) 9 | def fastapi_start(): 10 | p_reg = start_webserver_reg() 11 | p_self_signed = start_webserver_self_signed() 12 | sleep(2) 13 | yield p_reg, p_self_signed 14 | os.kill(p_reg.pid, signal.SIGKILL) 15 | os.kill(p_self_signed.pid, signal.SIGKILL) -------------------------------------------------------------------------------- /Test/regular/CLI_test.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | # look in ../ BEFORE trying to import Algorithmia. If you append to the 4 | # you will load the version installed on the computer. 5 | sys.path = ['../'] + sys.path 6 | 7 | import unittest 8 | import os 9 | import json 10 | import Algorithmia 11 | from Algorithmia.CLI import CLI 12 | import argparse 13 | import shutil 14 | 15 | if sys.version_info.major >= 3: 16 | class CLIDummyTest(unittest.TestCase): 17 | @classmethod 18 | def setUpClass(cls): 19 | cls.client = Algorithmia.client(api_address="http://localhost:8080", api_key="simabcd123") 20 | cls.bearerClient = Algorithmia.client(api_address="http://localhost:8080", bearer_token="simabcd123.token.token") 21 | 22 | def test_run(self): 23 | name = "util/Echo" 24 | inputs = "test" 25 | 26 | parser = argparse.ArgumentParser('CLI for interacting with Algorithmia') 27 | 28 | subparsers = parser.add_subparsers(help='sub cmd', dest='subparser_name') 29 | parser_run = subparsers.add_parser('run', help='algo run [input options] [output options]') 30 | 31 | parser_run.add_argument('algo') 32 | parser_run.add_argument('-d', '--data', action='store', help='detect input type', default=None) 33 | parser_run.add_argument('-t', '--text', action='store', help='treat input as text', default=None) 34 | parser_run.add_argument('-j', '--json', action='store', help='treat input as json data', default=None) 35 | parser_run.add_argument('-b', '--binary', action='store', help='treat input as binary data', default=None) 36 | parser_run.add_argument('-D', '--data-file', action='store', help='specify a path to an input file', 37 | default=None) 38 | parser_run.add_argument('-T', '--text-file', action='store', help='specify a path to a text file', 39 | default=None) 40 | parser_run.add_argument('-J', '--json-file', action='store', help='specify a path to a json file', 41 | default=None) 42 | parser_run.add_argument('-B', '--binary-file', action='store', help='specify a path to a binary file', 43 | default=None) 44 | parser_run.add_argument('--timeout', action='store', type=int, default=300, 45 | help='specify a timeout (seconds)') 46 | parser_run.add_argument('--debug', action='store_true', 47 | help='print the stdout from the algo ') 48 | parser_run.add_argument('--profile', action='store', type=str, default='default') 49 | parser_run.add_argument('-o', '--output', action='store', default=None, type=str) 50 | 51 | args = parser.parse_args(['run', name, '-d', inputs]) 52 | 53 | result = CLI().runalgo(args, self.client) 54 | self.assertEqual(result, inputs) 55 | 56 | def test_run_token(self): 57 | name = "util/Echo" 58 | inputs = "test" 59 | 60 | parser = argparse.ArgumentParser('CLI for interacting with Algorithmia') 61 | 62 | subparsers = parser.add_subparsers(help='sub cmd', dest='subparser_name') 63 | parser_run = subparsers.add_parser('run', help='algo run [input options] [output options]') 64 | 65 | parser_run.add_argument('algo') 66 | parser_run.add_argument('-d', '--data', action='store', help='detect input type', default=None) 67 | parser_run.add_argument('-t', '--text', action='store', help='treat input as text', default=None) 68 | parser_run.add_argument('-j', '--json', action='store', help='treat input as json data', default=None) 69 | parser_run.add_argument('-b', '--binary', action='store', help='treat input as binary data', default=None) 70 | parser_run.add_argument('-D', '--data-file', action='store', help='specify a path to an input file', 71 | default=None) 72 | parser_run.add_argument('-T', '--text-file', action='store', help='specify a path to a text file', 73 | default=None) 74 | parser_run.add_argument('-J', '--json-file', action='store', help='specify a path to a json file', 75 | default=None) 76 | parser_run.add_argument('-B', '--binary-file', action='store', help='specify a path to a binary file', 77 | default=None) 78 | parser_run.add_argument('--timeout', action='store', type=int, default=300, 79 | help='specify a timeout (seconds)') 80 | parser_run.add_argument('--debug', action='store_true', 81 | help='print the stdout from the algo ') 82 | parser_run.add_argument('--profile', action='store', type=str, default='default') 83 | parser_run.add_argument('-o', '--output', action='store', default=None, type=str) 84 | 85 | args = parser.parse_args(['run', name, '-d', inputs]) 86 | 87 | result = CLI().runalgo(args, self.bearerClient) 88 | self.assertEqual(result, inputs) 89 | 90 | 91 | class CLIMainTest(unittest.TestCase): 92 | def setUp(self): 93 | # create a directory to use in testing the cp command 94 | self.client = Algorithmia.client() 95 | CLI().mkdir("data://.my/moredata", self.client) 96 | if not os.path.exists("../TestFiles/"): 97 | os.mkdir("../TestFiles/") 98 | 99 | def test_ls(self): 100 | parentDir = "data://.my/" 101 | newDir = "test" 102 | 103 | CLI().mkdir(parentDir + newDir, self.client) 104 | result = CLI().ls(parentDir, self.client) 105 | self.assertTrue(result is not None and "moredata" in result and newDir in result) 106 | 107 | CLI().rmdir(parentDir + newDir, self.client) 108 | 109 | def test_mkdir(self): 110 | 111 | parentDir = "data://.my/" 112 | newDir = "test" 113 | 114 | CLI().mkdir(parentDir + newDir, self.client) 115 | result = CLI().ls(parentDir, self.client) 116 | self.assertTrue(newDir in result) 117 | 118 | CLI().rmdir(parentDir + newDir, self.client) 119 | 120 | def test_rmdir(self): 121 | parentDir = "data://.my/" 122 | newDir = "testRmdir" 123 | 124 | CLI().mkdir(parentDir + newDir, self.client) 125 | result = CLI().ls(parentDir, self.client) 126 | self.assertTrue(newDir in result) 127 | 128 | CLI().rmdir(parentDir + newDir, self.client) 129 | 130 | result = CLI().ls(parentDir, self.client) 131 | self.assertTrue(newDir not in result) 132 | 133 | def test_cat(self): 134 | file = "data://.my/moredata/test.txt" 135 | localfile = "./../TestFiles/test.txt" 136 | fileContents = "some text in test file" 137 | 138 | CLI().rm(file, self.client) 139 | testfile = open(localfile, "w") 140 | testfile.write(fileContents) 141 | testfile.close() 142 | 143 | CLI().cp([localfile], file, self.client) 144 | 145 | result = CLI().cat([file], self.client) 146 | self.assertEqual(result, fileContents) 147 | 148 | def test_get_build_logs(self): 149 | user = os.environ.get('ALGO_USER_NAME') 150 | algo = "Echo" 151 | 152 | result = json.loads(CLI().getBuildLogs(user, algo, self.client)) 153 | if "error" in result: 154 | print(result) 155 | self.assertTrue("error" not in result) 156 | 157 | # local to remote 158 | def test_cp_L2R(self): 159 | localfile = "./../TestFiles/test.txt" 160 | testfile = open(localfile, "w") 161 | testfile.write("some text") 162 | testfile.close() 163 | 164 | src = [localfile] 165 | dest = "data://.my/moredata/test.txt" 166 | CLI().cp(src, dest, self.client) 167 | 168 | result = CLI().ls("data://.my/moredata/", self.client) 169 | self.assertTrue("test.txt" in result) 170 | 171 | # remote to remote 172 | def test_cp_R2R(self): 173 | 174 | src = ["data://.my/moredata/test.txt"] 175 | dest = "data://.my/moredata/test2.txt" 176 | CLI().cp(src, dest, self.client) 177 | 178 | result = CLI().ls("data://.my/moredata/", self.client) 179 | self.assertTrue("test2.txt" in result) 180 | 181 | # remote to local 182 | def test_cp_R2L(self): 183 | src = ["data://.my/moredata/test.txt"] 184 | dest = "./../test.txt" 185 | 186 | CLI().cp(src, dest, self.client) 187 | self.assertTrue(os.path.isfile(dest)) 188 | 189 | def test_auth(self): 190 | # key for test account 191 | key = os.getenv('ALGORITHMIA_API_KEY') 192 | api_address = "https://api.algorithmia.com" 193 | profile = 'default' 194 | CLI().auth(api_address, key, profile=profile) 195 | resultK = CLI().getAPIkey(profile) 196 | resultA = CLI().getAPIaddress(profile) 197 | self.assertEqual(resultK, key) 198 | self.assertEqual(resultA, api_address) 199 | 200 | def test_auth_cert(self): 201 | 202 | localfile = "./../TestFiles/fakecert.pem" 203 | 204 | testfile = open(localfile, "w") 205 | testfile.write("") 206 | testfile.close() 207 | 208 | # key for test account 209 | key = os.getenv('ALGORITHMIA_API_KEY') 210 | address = 'https://api.algorithmia.com' 211 | cacert = localfile 212 | profile = 'test' 213 | 214 | CLI().auth(address, key, cacert=cacert, profile=profile) 215 | resultK = CLI().getAPIkey(profile) 216 | resultA = CLI().getAPIaddress(profile) 217 | resultC = CLI().getCert(profile) 218 | self.assertEqual(resultK, key) 219 | self.assertEqual(resultA, address) 220 | self.assertEqual(resultC, cacert) 221 | 222 | def test_auth_token(self): 223 | address = 'https://api.algorithmia.com' 224 | bearer = 'testtokenabcd' 225 | profile = 'test' 226 | 227 | CLI().auth(apiaddress=address, bearer=bearer, profile=profile) 228 | resultA = CLI().getAPIaddress(profile) 229 | resultT = CLI().getBearerToken(profile) 230 | self.assertEqual(resultA, address) 231 | self.assertEqual(resultT, bearer) 232 | 233 | def test_get_environment(self): 234 | result = CLI().get_environment_by_language("python2", self.client) 235 | print(result) 236 | if ("error" in result): 237 | print(result) 238 | self.assertTrue(result is not None and "display_name" in result) 239 | 240 | def test_list_languages(self): 241 | result = CLI().list_languages(self.client) 242 | if ("error" in result[0]): 243 | print(result) 244 | self.assertTrue(result is not None and "anaconda3" in result[1]) 245 | 246 | def test_rm(self): 247 | localfile = "./../TestFiles/testRM.txt" 248 | 249 | testfile = open(localfile, "w") 250 | testfile.write("some text") 251 | testfile.close() 252 | 253 | src = [localfile] 254 | dest = "data://.my/moredata/" 255 | CLI().cp(src, dest, self.client) 256 | 257 | result1 = CLI().ls(dest, self.client) 258 | 259 | CLI().rm("data://.my/moredata/testRM.txt", self.client) 260 | 261 | result2 = CLI().ls(dest, self.client) 262 | 263 | self.assertTrue("testRM.txt" in result1 and "testRM.txt" not in result2) 264 | 265 | def test_get_template(self): 266 | filename = "./../temptest" 267 | envid = "36fd467e-fbfe-4ea6-aa66-df3f403b7132" 268 | response = CLI().get_template(envid, filename, self.client) 269 | print(response) 270 | self.assertTrue(response.ok) 271 | try: 272 | shutil.rmtree(filename) 273 | except OSError as e: 274 | print(e) 275 | 276 | def test_api_address_auth(self): 277 | api_key = os.getenv('ALGORITHMIA_API_KEY') 278 | api_address = "https://api.algorithmia.com" 279 | CLI().auth(api_address, api_key) 280 | profile = "default" 281 | 282 | client = Algorithmia.client(CLI().getAPIkey(profile), CLI().getAPIaddress(profile), CLI().getCert(profile)) 283 | result2 = CLI().ls("data://.my", client) 284 | print(result2) 285 | self.assertTrue(result2 != "") 286 | 287 | 288 | if __name__ == '__main__': 289 | unittest.main() 290 | -------------------------------------------------------------------------------- /Test/regular/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algorithmiaio/algorithmia-python/31f0e9047483bea72b4fdec9ef42b9174b2aa2dc/Test/regular/__init__.py -------------------------------------------------------------------------------- /Test/regular/acl_test.py: -------------------------------------------------------------------------------- 1 | import sys 2 | # look in ../ BEFORE trying to import Algorithmia. If you append to the 3 | # you will load the version installed on the computer. 4 | sys.path = ['../'] + sys.path 5 | 6 | import unittest 7 | import Algorithmia 8 | from Algorithmia.acl import AclType, Acl, ReadAcl 9 | from Algorithmia.datadirectory import DataDirectory 10 | 11 | class AclTypeTest(unittest.TestCase): 12 | def test_types(self): 13 | self.assertTrue(AclType.private.acl_string is None) 14 | self.assertEquals(AclType.my_algos.acl_string, 'algo://.my/*') 15 | self.assertEquals(AclType.public.acl_string, 'user://*') 16 | self.assertEquals(AclType.default, AclType.my_algos) 17 | 18 | def test_from_acl_response(self): 19 | self.assertEquals(AclType.from_acl_response([]), AclType.private) 20 | self.assertEquals(AclType.from_acl_response(['algo://.my/*']), AclType.my_algos) 21 | self.assertEquals(AclType.from_acl_response(['user://*']), AclType.public) 22 | 23 | def test_create_acl(self): 24 | c = Algorithmia.client() 25 | dd = DataDirectory(c, 'data://.my/privatePermissions') 26 | if dd.exists(): 27 | dd.delete(True) 28 | dd.create(ReadAcl.private) 29 | 30 | dd_perms = DataDirectory(c, 'data://.my/privatePermissions').get_permissions() 31 | self.assertEquals(dd_perms.read_acl, AclType.private) 32 | 33 | dd.update_permissions(ReadAcl.public) 34 | dd_perms = DataDirectory(c, 'data://.my/privatePermissions').get_permissions() 35 | self.assertEquals(dd_perms.read_acl, AclType.public) 36 | 37 | if __name__ == '__main__': 38 | unittest.main() 39 | -------------------------------------------------------------------------------- /Test/regular/algo_failure_test.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | if sys.version_info[0] >= 3: 4 | import unittest 5 | import Algorithmia 6 | import uvicorn 7 | import time 8 | from multiprocessing import Process 9 | 10 | # look in ../ BEFORE trying to import Algorithmia. If you append to the 11 | # you will load the version installed on the computer. 12 | sys.path = ['../'] + sys.path 13 | from requests import Response 14 | 15 | class AlgoTest(unittest.TestCase): 16 | error_500 = Response() 17 | error_500.status_code = 500 18 | error_message = "Non-Algorithm related Failure: " + str(error_500) 19 | 20 | @classmethod 21 | def setUpClass(cls): 22 | cls.client = Algorithmia.client(api_address="http://localhost:8080", api_key="simabcd123") 23 | 24 | def test_throw_500_error_HTTP_response_on_algo_request(self): 25 | try: 26 | result = self.client.algo('util/500').pipe(bytearray('foo', 'utf-8')) 27 | except Exception as e: 28 | result = e 29 | pass 30 | self.assertEqual(str(self.error_message), str(result)) 31 | 32 | def test_retry_on_400_error_publish(self): 33 | result = self.client.algo("util/failonce").publish() 34 | self.assertEqual(result['version_info']['semantic_version'], "0.1.0") 35 | 36 | def test_throw_on_always_500_publish(self): 37 | try: 38 | result = self.client.algo("util/failalways").publish() 39 | except Exception as e: 40 | result = e 41 | pass 42 | self.assertEqual(str(self.error_message), str(result)) 43 | -------------------------------------------------------------------------------- /Test/regular/algo_test.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | from Algorithmia.errors import AlgorithmException 4 | from Algorithmia.algorithm import OutputType 5 | import Algorithmia 6 | 7 | # look in ../ BEFORE trying to import Algorithmia. If you append to the 8 | # you will load the version installed on the computer. 9 | sys.path = ['../'] + sys.path 10 | 11 | import unittest 12 | 13 | if sys.version_info.major >= 3: 14 | 15 | class AlgoDummyTest(unittest.TestCase): 16 | @classmethod 17 | def setUpClass(cls): 18 | cls.client = Algorithmia.client(api_address="http://localhost:8080", api_key="simabcd123") 19 | cls.environment_id = "abcd-123" 20 | 21 | def test_call_customCert(self): 22 | result = self.client.algo('quality/echo').pipe(bytearray('foo', 'utf-8')) 23 | self.assertEqual('binary', result.metadata.content_type) 24 | self.assertEqual(bytearray('foo', 'utf-8'), result.result) 25 | 26 | 27 | 28 | def test_normal_call(self): 29 | result = self.client.algo('quality/echo').pipe("foo") 30 | self.assertEqual("text", result.metadata.content_type) 31 | self.assertEqual("foo", result.result) 32 | 33 | def test_async_call(self): 34 | result = self.client.algo('quality/echo').set_options(output=OutputType.void).pipe("foo") 35 | self.assertTrue(hasattr(result, "async_protocol")) 36 | self.assertTrue(hasattr(result, "request_id")) 37 | 38 | def test_raw_call(self): 39 | result = self.client.algo('quality/echo').set_options(output=OutputType.raw).pipe("foo") 40 | self.assertEqual("foo", result) 41 | 42 | def test_dict_call(self): 43 | result = self.client.algo('quality/echo').pipe({"foo": "bar"}) 44 | self.assertEqual("json", result.metadata.content_type) 45 | self.assertEqual({"foo": "bar"}, result.result) 46 | 47 | def test_algo_exists(self): 48 | result = self.client.algo('quality/echo').exists() 49 | self.assertEqual(True, result) 50 | 51 | def test_algo_no_exists(self): 52 | result = self.client.algo('quality/not_echo').exists() 53 | self.assertEqual(False, result) 54 | 55 | #TODO: add more coverage examples to check kwargs 56 | def test_get_versions(self): 57 | result = self.client.algo('quality/echo').versions() 58 | self.assertTrue('results' in result) 59 | self.assertTrue('version_info' in result['results'][0]) 60 | self.assertTrue('semantic_version' in result['results'][0]['version_info']) 61 | self.assertEqual('0.1.0', result['results'][0]['version_info']['semantic_version']) 62 | 63 | def test_text_unicode(self): 64 | telephone = u"\u260E" 65 | # Unicode input to pipe() 66 | result1 = self.client.algo('quality/echo').pipe(telephone) 67 | self.assertEqual('text', result1.metadata.content_type) 68 | self.assertEqual(telephone, result1.result) 69 | 70 | # Unicode return in .result 71 | result2 = self.client.algo('quality/echo').pipe(result1.result) 72 | self.assertEqual('text', result2.metadata.content_type) 73 | self.assertEqual(telephone, result2.result) 74 | 75 | def test_algo_info(self): 76 | result = self.client.algo('quality/echo').info() 77 | self.assertTrue('results' in result) 78 | self.assertTrue('resource_type' in result['results'][0]) 79 | self.assertTrue(result['results'][0]['resource_type'] == "algorithm") 80 | 81 | def test_update_algo(self): 82 | details = { 83 | "summary": "Example Summary", 84 | "label": "QA", 85 | "tagline": "Example Tagline" 86 | } 87 | settings = { 88 | "source_visibility": "open", 89 | "algorithm_environment": self.environment_id, 90 | "license": "apl", 91 | "network_access": "isolated", 92 | "pipeline_enabled": False 93 | } 94 | version_info = { 95 | "sample_input": "hello" 96 | } 97 | result = self.client.algo('quality/echo').update(details=details, settings=settings, version_info=version_info) 98 | self.assertTrue('id' in result) 99 | 100 | 101 | def test_get_build_by_id(self): 102 | result = self.client.algo("quality/echo").get_build("1a392e2c-b09f-4bae-a616-56c0830ac8e5") 103 | self.assertTrue('commit_sha' in result) 104 | 105 | def test_get_build_logs(self): 106 | result = self.client.algo("quality/echo").get_build_logs("1a392e2c-b09f-4bae-a616-56c0830ac8e5") 107 | self.assertTrue('logs' in result) 108 | 109 | def test_get_scm_status(self): 110 | result = self.client.algo("quality/echo").get_scm_status() 111 | self.assertTrue('scm_connection_status' in result) 112 | 113 | def test_exception_ipa_algo(self): 114 | try: 115 | result = self.client.algo('zeryx/raise_exception').pipe("") 116 | except AlgorithmException as e: 117 | self.assertEqual(e.message, "This is an exception") 118 | 119 | def test_algorithm_programmatic_create_process(self): 120 | algorithm_name = "hello" 121 | payload = "John" 122 | expected_response = "hello John" 123 | full_path = "quality/" + algorithm_name 124 | details = { 125 | "summary": "Example Summary", 126 | "label": "QA", 127 | "tagline": "Example Tagline" 128 | } 129 | settings = { 130 | "source_visibility": "open", 131 | "algorithm_environment": self.environment_id, 132 | "license": "apl", 133 | "network_access": "isolated", 134 | "pipeline_enabled": False 135 | } 136 | version_info = { 137 | "sample_input": "hello" 138 | } 139 | created_algo = self.client.algo(full_path) 140 | print("about to create algo") 141 | response = created_algo.create(details=details, settings=settings, version_info=version_info) 142 | print("created algo") 143 | self.assertEqual(response['name'], algorithm_name, "algorithm creation failed") 144 | 145 | # --- Creation complete, compiling 146 | 147 | response = created_algo.compile() 148 | git_hash = response['version_info']['git_hash'] 149 | algo_with_build = self.client.algo(full_path + "/" + git_hash) 150 | self.assertEqual(response['name'], created_algo.algoname) 151 | 152 | # --- compiling complete, now testing algorithm request 153 | response = algo_with_build.pipe(payload).result 154 | self.assertEqual(response, expected_response, "compiling failed") 155 | 156 | # --- testing complete, now publishing new release. 157 | 158 | pub_settings = {"algorithm_callability": "private"} 159 | pub_version_info = { 160 | "release_notes": "created programmatically", 161 | "sample_input": payload, 162 | "version_type": "minor" 163 | } 164 | pub_details = {"label": "testing123"} 165 | 166 | response = algo_with_build.publish( 167 | details=pub_details, 168 | settings=pub_settings, 169 | version_info=pub_version_info 170 | ) 171 | self.assertEqual(response["version_info"]["semantic_version"], "0.1.0", 172 | "Publishing failed, semantic version is not correct.") 173 | 174 | # --- publishing complete, getting additional information 175 | 176 | response = created_algo.info(git_hash) 177 | 178 | self.assertEqual(response['version_info']['semantic_version'], "0.1.0", "information is incorrect") 179 | 180 | 181 | def test_set_secret(self): 182 | short_name = "tst" 183 | secret_key = "test_key" 184 | secret_value = "test_value" 185 | description = "loreum epsum" 186 | response = self.client.algo("quality/echo").set_secret(short_name, secret_key, secret_value, description) 187 | self.assertEqual(response['id'], "959af771-7cd8-4981-91c4-70def15bbcdc", "invalid ID for created secret") 188 | 189 | 190 | else: 191 | class AlgoTest(unittest.TestCase): 192 | def setUp(self): 193 | self.client = Algorithmia.client() 194 | 195 | def test_call_customCert(self): 196 | open("./test.pem", 'w') 197 | c = Algorithmia.client(ca_cert="./test.pem") 198 | result = c.algo('quality/echo').pipe(bytearray('foo', 'utf-8')) 199 | self.assertEqual('binary', result.metadata.content_type) 200 | self.assertEqual(bytearray('foo', 'utf-8'), result.result) 201 | try: 202 | os.remove("./test.pem") 203 | except OSError as e: 204 | print(e) 205 | 206 | def test_call_binary(self): 207 | result = self.client.algo('quality/echo').pipe(bytearray('foo', 'utf-8')) 208 | self.assertEqual('binary', result.metadata.content_type) 209 | self.assertEqual(bytearray('foo', 'utf-8'), result.result) 210 | 211 | def test_async_call(self): 212 | result = self.client.algo('quality/echo').set_options(output=OutputType.void).pipe("foo") 213 | self.assertTrue(hasattr(result, "async_protocol")) 214 | self.assertTrue(hasattr(result, "request_id")) 215 | 216 | def test_raw_call(self): 217 | result = self.client.algo('quality/echo').set_options(output=OutputType.raw).pipe("foo") 218 | self.assertEqual("foo", result) 219 | 220 | #TODO: add more coverage examples to check kwargs 221 | def test_get_versions(self): 222 | result = self.client.algo('quality/echo').versions() 223 | self.assertTrue('results' in result) 224 | self.assertTrue('version_info' in result['results'][0]) 225 | self.assertTrue('semantic_version' in result['results'][0]['version_info']) 226 | self.assertEqual('0.1.0', result['results'][0]['version_info']['semantic_version']) 227 | 228 | def test_text_unicode(self): 229 | telephone = u"\u260E" 230 | 231 | # Unicode input to pipe() 232 | result1 = self.client.algo('quality/echo').pipe(telephone) 233 | self.assertEqual('text', result1.metadata.content_type) 234 | self.assertEqual(telephone, result1.result) 235 | 236 | # Unicode return in .result 237 | result2 = self.client.algo('quality/echo').pipe(result1.result) 238 | self.assertEqual('text', result2.metadata.content_type) 239 | self.assertEqual(telephone, result2.result) 240 | 241 | 242 | def test_get_scm_status(self): 243 | result = self.client.algo("quality/echo").get_scm_status() 244 | self.assertTrue('scm_connection_status' in result) 245 | 246 | def test_exception_ipa_algo(self): 247 | try: 248 | result = self.client.algo('zeryx/raise_exception').pipe("") 249 | except AlgorithmException as e: 250 | self.assertEqual(e.message, "This is an exception") 251 | 252 | if __name__ == '__main__': 253 | unittest.main() 254 | -------------------------------------------------------------------------------- /Test/regular/client_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import sys 4 | from datetime import datetime 5 | from random import random 6 | from random import seed 7 | 8 | sys.path = ['../'] + sys.path 9 | 10 | import unittest 11 | import Algorithmia 12 | from Algorithmia.errors import AlgorithmException 13 | from uuid import uuid4 14 | 15 | if sys.version_info.major >= 3: 16 | unicode = str 17 | 18 | 19 | class ClientDummyTest(unittest.TestCase): 20 | @classmethod 21 | def setUpClass(cls): 22 | cls.client = Algorithmia.client(api_address="http://localhost:8080", api_key="simabcd123") 23 | 24 | admin_username = "a_Mrtest" 25 | admin_org_name = "a_myOrg" 26 | environment_name = "Python 3.9" 27 | 28 | def setUp(self): 29 | self.admin_username = self.admin_username + str(int(random() * 10000)) 30 | self.admin_org_name = self.admin_org_name + str(int(random() * 10000)) 31 | 32 | def test_create_user(self): 33 | response = self.client.create_user( 34 | {"username": self.admin_username, "email": self.admin_username + "@algo.com", "passwordHash": "", 35 | "shouldCreateHello": False}) 36 | 37 | if type(response) is dict: 38 | self.assertEqual(self.admin_username, response['username']) 39 | else: 40 | self.assertIsNotNone(response) 41 | 42 | def test_get_org_types(self): 43 | response = self.client.get_org_types() 44 | self.assertTrue(len(response) > 0) 45 | 46 | def test_create_org(self): 47 | response = self.client.create_org( 48 | {"org_name": self.admin_org_name, "org_label": "some label", "org_contact_name": "Some owner", 49 | "org_email": self.admin_org_name + "@algo.com", "type_id": "basic"}) 50 | 51 | self.assertEqual(self.admin_org_name, response[u'org_name']) 52 | 53 | def test_get_org(self): 54 | response = self.client.get_org("a_myOrg84") 55 | self.assertEqual("a_myOrg84", response['org_name']) 56 | 57 | def test_get_environment(self): 58 | response = self.client.get_environment("python2") 59 | 60 | if u'error' not in response: 61 | self.assertTrue(response is not None and u'environments' in response) 62 | 63 | def test_get_scms(self): 64 | response = self.client.scms() 65 | results = response['results'] 66 | internal = [result for result in results if result['id'] == 'internal'] 67 | self.assertTrue(len(internal) == 1) 68 | 69 | def test_edit_org(self): 70 | org_name = "a_myOrg84" 71 | 72 | obj = { 73 | "id": "b85d8c4e-7f3c-40b9-9659-6adc2cb0e16f", 74 | "org_name": "a_myOrg84", 75 | "org_label": "some label", 76 | "org_contact_name": "Some owner", 77 | "org_email": "a_myOrg84@algo.com", 78 | "org_created_at": "2020-11-30T23:51:40", 79 | "org_url": "https://algorithmia.com", 80 | "type_id": "basic", 81 | "resource_type": "organization" 82 | } 83 | 84 | response = self.client.edit_org(org_name, obj) 85 | if type(response) is dict: 86 | print(response) 87 | else: 88 | self.assertEqual(204, response.status_code) 89 | 90 | def test_get_supported_languages(self): 91 | response = self.client.get_supported_languages() 92 | self.assertTrue(response is not None) 93 | 94 | if type(response) is not list: 95 | self.assertTrue(u'error' in response) 96 | else: 97 | language_found = any('anaconda3' in languages['name'] for languages in response) 98 | self.assertTrue(response is not None and language_found) 99 | 100 | def test_invite_to_org(self): 101 | response = self.client.invite_to_org("a_myOrg38", "a_Mrtest4") 102 | if type(response) is dict: 103 | self.assertTrue(u'error' in response) 104 | else: 105 | self.assertEqual(200, response.status_code) 106 | 107 | # This test will require updating after the /v1/organizations/{org_name}/errors endpoint has been 108 | # deployed to the remote environment. 109 | def test_get_organization_errors(self): 110 | response = self.client.get_organization_errors(self.admin_org_name) 111 | self.assertTrue(response is not None) 112 | 113 | if type(response) is list: 114 | self.assertEqual(0, len(response), 'Received unexpected result, should have been 0.') 115 | 116 | def test_get_user_errors(self): 117 | response = self.client.get_user_errors(self.admin_username) 118 | 119 | self.assertTrue(response is not None) 120 | self.assertEqual(0, len(response)) 121 | 122 | def test_get_algorithm_errors(self): 123 | try: 124 | _ = self.client.get_algorithm_errors('hello') 125 | self.assertFalse(True) 126 | except AlgorithmException as e: 127 | self.assertTrue(e.message == "No such algorithm") 128 | 129 | def test_no_auth_client(self): 130 | 131 | key = os.environ.get('ALGORITHMIA_API_KEY', "") 132 | if key != "": 133 | del os.environ['ALGORITHMIA_API_KEY'] 134 | 135 | client = Algorithmia.client(api_address="http://localhost:8080") 136 | error = None 137 | try: 138 | client.algo("demo/hello").pipe("world") 139 | except Exception as e: 140 | error = e 141 | finally: 142 | os.environ['ALGORITHMIA_API_KEY'] = key 143 | self.assertEqual(str(error), str(AlgorithmException(message="authorization required", stack_trace=None, 144 | error_type=None))) 145 | if __name__ == '__main__': 146 | unittest.main() 147 | -------------------------------------------------------------------------------- /Test/regular/datadirectory_test.py: -------------------------------------------------------------------------------- 1 | import sys 2 | # look in ../ BEFORE trying to import Algorithmia. If you append to the 3 | # you will load the version installed on the computer. 4 | sys.path = ['../'] + sys.path 5 | 6 | import unittest 7 | import os 8 | 9 | import Algorithmia 10 | from Algorithmia.datadirectory import DataDirectory, LocalDataDirectory 11 | from Algorithmia.data import DataObjectType 12 | from Algorithmia.acl import Acl, AclType 13 | 14 | class DataDirectoryTest(unittest.TestCase): 15 | def setUp(self): 16 | self.client = Algorithmia.client() 17 | 18 | def test_get_name(self): 19 | dd = DataDirectory(self.client, 'data://.my/this/is/a/long/path') 20 | self.assertEqual('path', dd.getName()) 21 | 22 | def test_directory_does_not_exit(self): 23 | dd = DataDirectory(self.client, "data://.my/this_should_never_be_created") 24 | self.assertFalse(dd.exists()) 25 | 26 | def test_alternate_directory_syntax(self): 27 | # Check alternate dir syntax 28 | dd = self.client.dir("data://.my/this_should_never_be_created") 29 | self.assertFalse(dd.exists()) 30 | 31 | def test_empty_directory_creation_and_deletion(self): 32 | dd = DataDirectory(self.client, "data://.my/empty_test_directory") 33 | 34 | if (dd.exists()): 35 | dd.delete(False) 36 | 37 | self.assertFalse(dd.exists()) 38 | 39 | dd.create() 40 | self.assertTrue(dd.exists()) 41 | 42 | # get rid of it 43 | dd.delete(False) 44 | self.assertFalse(dd.exists()) 45 | 46 | def test_nonempty_directory_creation_and_deletion(self): 47 | dd = DataDirectory(self.client, "data://.my/nonempty_test_directory") 48 | 49 | if (dd.exists()): 50 | dd.delete(True) 51 | 52 | self.assertFalse(dd.exists()) 53 | 54 | dd.create() 55 | self.assertTrue(dd.exists()) 56 | 57 | f = dd.file('one') 58 | self.assertFalse(f.exists()) 59 | f.put('data') 60 | self.assertTrue(f.exists()) 61 | 62 | # Try deleting without the force - the directory should still be there 63 | self.assertRaises(Exception, dd.delete) 64 | self.assertTrue(dd.exists()) 65 | self.assertTrue(f.exists()) 66 | 67 | dd.delete(True) 68 | self.assertFalse(dd.exists()) 69 | self.assertFalse(f.exists()) 70 | 71 | def list_files_small(self, collectionName): 72 | dd = DataDirectory(self.client, collectionName) 73 | if (dd.exists()): 74 | dd.delete(True) 75 | 76 | dd.create() 77 | 78 | f1 = dd.file('a') 79 | f1.put('data') 80 | 81 | f2 = dd.file('b') 82 | f2.put('data') 83 | 84 | size = 0 85 | all_files = set() 86 | for f in dd.files(): 87 | all_files.add(f.path) 88 | size += 1 89 | 90 | self.assertEqual(2, size) 91 | self.assertTrue('.my/test_list_files_small/a' in all_files) 92 | self.assertTrue('.my/test_list_files_small/b' in all_files) 93 | 94 | dd.delete(True) 95 | 96 | def get_files(self, collectionName): 97 | dd = self.client.dir(collectionName) 98 | if dd.exists(): 99 | dd.delete(True) 100 | 101 | dd.create() 102 | 103 | f1 = dd.file('a') 104 | f1.put('data') 105 | 106 | f2 = dd.file('b') 107 | f2.put('data') 108 | 109 | local_path = dd.getDir() 110 | self.assertTrue(os.path.isfile(os.path.join(local_path, "a"))) 111 | self.assertTrue(os.path.isfile(os.path.join(local_path, "b"))) 112 | 113 | 114 | def test_list_files_small_without_trailing_slash(self): 115 | self.list_files_small('data://.my/test_list_files_small') 116 | 117 | def test_list_files_small_with_trailing_slash(self): 118 | self.list_files_small('data://.my/test_list_files_small/') 119 | 120 | def test_get_directory(self): 121 | self.get_files("data://.my/test_list_files_small") 122 | 123 | def test_list_folders(self): 124 | dd = DataDirectory(self.client, 'data://.my/') 125 | 126 | dirName = '.my/test_list_directory' 127 | testDir = DataDirectory(self.client, 'data://' + dirName) 128 | if testDir.exists(): 129 | testDir.delete(True) 130 | 131 | all_folders = set() 132 | for f in dd.dirs(): 133 | all_folders.add(f.path) 134 | self.assertFalse(dirName in all_folders) 135 | 136 | testDir.create() 137 | all_folders = set() 138 | for f in dd.dirs(): 139 | all_folders.add(f.path) 140 | self.assertTrue(dirName in all_folders) 141 | 142 | testDir.delete(True) 143 | 144 | 145 | # TODO: replicate this in Marketplace 146 | # def test_list_files_with_paging(self): 147 | # NUM_FILES = 1100 148 | # EXTENSION = '.txt' 149 | # 150 | # dd = DataDirectory(self.client, 'data://.my/pythonLargeDataDirList') 151 | # if not dd.exists(): 152 | # dd.create() 153 | # 154 | # for i in range(NUM_FILES): 155 | # dd.file(str(i) + EXTENSION).put(str(i)) 156 | # 157 | # seenFiles = [False] * NUM_FILES 158 | # numFiles = 0 159 | # 160 | # for f in dd.files(): 161 | # numFiles += 1 162 | # name = f.getName() 163 | # index = int(name[:-1 * len(EXTENSION)]) 164 | # seenFiles[index] = True 165 | # 166 | # allSeen = True 167 | # for cur in seenFiles: 168 | # allSeen = (allSeen and cur) 169 | # 170 | # self.assertEqual(NUM_FILES, numFiles) 171 | # self.assertTrue(allSeen) 172 | 173 | def test_data_object(self): 174 | dd = DataDirectory(self.client, 'data://foo') 175 | self.assertTrue(dd.is_dir()) 176 | self.assertFalse(dd.is_file()) 177 | self.assertTrue(dd.get_type() is DataObjectType.directory) 178 | 179 | class LocalDataDirectoryTest(unittest.TestCase): 180 | _DUMMY_DIR = 'dummy_dir_that_should_not_exist' 181 | _EXISTING_DIR = 'existing_dir_that_should_not_exist_before_test' 182 | EXISTING_FILES = ['file1.txt', 'file2.txt'] 183 | def setUp(self): 184 | self.client = Algorithmia.client() 185 | self.DUMMY_DIR = 'file://' + self._DUMMY_DIR 186 | self.EXISTING_DIR = 'file://' + self._EXISTING_DIR 187 | # create existing dir w files in it 188 | os.mkdir(self._EXISTING_DIR) 189 | for fname in self.EXISTING_FILES: 190 | with open(self._EXISTING_DIR+'/'+fname, 'w') as f: 191 | f.write('filler text') 192 | # ensure dummy dir does not exist yet 193 | assert not os.path.isdir(self.DUMMY_DIR) 194 | def tearDown(self): 195 | for fname in self.EXISTING_FILES: 196 | os.remove(self._EXISTING_DIR+'/'+fname) 197 | os.rmdir(self._EXISTING_DIR) 198 | def test_exist_or_not(self): 199 | self.assertTrue(self.client.dir(self.EXISTING_DIR).exists()) 200 | self.assertFalse(self.client.dir(self.DUMMY_DIR).exists()) 201 | def test_create_delete(self): 202 | self.client.dir(self.DUMMY_DIR).create() 203 | self.assertTrue(self.client.dir(self.DUMMY_DIR).exists()) 204 | self.client.dir(self.DUMMY_DIR).delete() 205 | self.assertFalse(self.client.dir(self.DUMMY_DIR).exists()) 206 | def test_list(self): 207 | contents = set(x for x in self.client.dir(self.EXISTING_DIR).list()) 208 | self.assertEqual(contents, set(self.EXISTING_FILES)) 209 | 210 | 211 | if __name__ == '__main__': 212 | unittest.main() 213 | -------------------------------------------------------------------------------- /Test/regular/datafile_test.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | # look in ../ BEFORE trying to import Algorithmia. If you append to the 4 | # you will load the version installed on the computer. 5 | sys.path = ['../'] + sys.path 6 | 7 | import unittest, os, uuid 8 | import numpy as np 9 | import Algorithmia 10 | import json 11 | from Algorithmia.datafile import DataFile, LocalDataFile, AdvancedDataFile 12 | 13 | class DataFileTest(unittest.TestCase): 14 | def setUp(self): 15 | self.client = Algorithmia.client() 16 | if not self.client.dir("data://.my/empty").exists(): 17 | self.client.dir("data://.my/empty").create() 18 | 19 | def test_get_nonexistant(self): 20 | df = self.client.file('data://.my/nonexistant/nonreal') 21 | try: 22 | df.getFile() 23 | retrieved_file = True 24 | except Exception as e: 25 | retrieved_file = False 26 | self.assertFalse(retrieved_file) 27 | 28 | def test_get_str(self): 29 | df = self.client.file('data://.my/nonexistant/nonreal') 30 | try: 31 | print(df.getString()) 32 | retrieved_file = True 33 | except Exception as e: 34 | retrieved_file = False 35 | self.assertFalse(retrieved_file) 36 | 37 | def test_set_attributes(self): 38 | df = DataFile(self.client, 'data://.my/empty') 39 | 40 | try: 41 | df.set_attributes({ 42 | 'last_modified': '2019-01-09T22:44:31.632Z', 43 | 'size': 0 44 | }) 45 | except Exception as e: 46 | self.fail("set_attributes failed with exception: " + str(e)) 47 | 48 | def test_putJson_getJson(self): 49 | file = '.my/empty/test.json' 50 | df = DataFile(self.client,'data://'+file) 51 | if sys.version_info[0] < 3: 52 | payload = {u"hello":u"world"} 53 | else: 54 | payload = {"hello": "world"} 55 | response = df.putJson(payload) 56 | self.assertEqual(response.path,file) 57 | result = self.client.file(file).getJson() 58 | self.assertEqual(str(result), str(payload)) 59 | 60 | def test_putNumpy_getNumpy(self): 61 | file = ".my/empty/numpy.json" 62 | df = DataFile(self.client, 'data://' + file) 63 | arr = np.array([0, 0, 0, 0], dtype=np.int64) 64 | response = df.putNumpy(arr) 65 | self.assertEqual(response.path, file) 66 | result = self.client.file(file).getNumpy() 67 | self.assertEqual(str(arr), str(result)) 68 | 69 | 70 | class LocalFileTest(unittest.TestCase): 71 | DUMMY_TEXT = 'this file gets populated during testing' 72 | EXISTING_TEXT = 'this file exists before testing' 73 | def setUp(self): 74 | self.client = Algorithmia.client() 75 | # Make a file that DOES exist and has contents, 76 | self.EXISTING_FILE = 'file://'+str(uuid.uuid1())+'.txt' 77 | f = open(self.EXISTING_FILE.replace('file://', ''), 'w') 78 | f.write(self.EXISTING_TEXT) 79 | f.close() 80 | # We need a dummy file that doesnt currently exist 81 | self.DUMMY_FILE = 'file://'+str(uuid.uuid1())+'.txt' 82 | if os.path.isfile(self.DUMMY_FILE): os.remove(self.DUMMY_FILE) 83 | def tearDown(self): 84 | os.remove(self.EXISTING_FILE.replace('file://', '')) 85 | if os.path.isfile(self.DUMMY_FILE): os.remove(self.DUMMY_FILE.replace('file://', '')) 86 | def test_local_remote(self): 87 | self.assertTrue(isinstance(self.client.file(self.DUMMY_FILE), LocalDataFile)) 88 | self.assertTrue(isinstance(self.client.file('data://foo'), DataFile)) 89 | def test_exists_or_not(self): 90 | self.assertTrue(self.client.file(self.EXISTING_FILE).exists()) 91 | self.assertFalse(self.client.file(self.DUMMY_FILE).exists()) 92 | def test_get_nonexistant(self): 93 | df = self.client.file(self.DUMMY_FILE) 94 | try: 95 | df.getFile() 96 | retrieved_file = True 97 | except Exception as e: 98 | retrieved_file = False 99 | self.assertFalse(retrieved_file) 100 | def test_put_and_read_and_delete(self): 101 | f = self.client.file(self.DUMMY_FILE) 102 | f.put(self.DUMMY_TEXT) 103 | # Check getString 104 | txt = self.client.file(self.DUMMY_FILE).getString() 105 | self.assertEqual(txt, self.DUMMY_TEXT) 106 | # Check delete 107 | deletion_status = self.client.file(self.DUMMY_FILE).delete() 108 | self.assertTrue(deletion_status) 109 | def test_read_types(self): 110 | # Check getBytes 111 | txt = self.client.file(self.EXISTING_FILE).getBytes().decode('utf-8') 112 | self.assertEqual(txt, self.EXISTING_TEXT) 113 | # Check getFile 114 | txt = self.client.file(self.EXISTING_FILE).getFile().read() 115 | self.assertEqual(txt, self.EXISTING_TEXT) 116 | 117 | class AdvancedDataFileTest(unittest.TestCase): 118 | def setUp(self): 119 | self.client = Algorithmia.client() 120 | if not self.client.dir("data://.my/empty").exists(): 121 | self.client.dir("data://.my/empty").create() 122 | 123 | def test_get_nonexistant(self): 124 | try: 125 | with self.client.file('data://.my/nonexistant/nonreal') as f: 126 | _ = f.read() 127 | retrieved_file = True 128 | except Exception as e: 129 | retrieved_file = False 130 | self.assertFalse(retrieved_file) 131 | 132 | def test_get_str(self): 133 | df = self.client.file('data://.my/nonexistant/nonreal', cleanup=True) 134 | try: 135 | print(df.getString()) 136 | retrieved_file = True 137 | except Exception as e: 138 | retrieved_file = False 139 | self.assertFalse(retrieved_file) 140 | 141 | def test_putJson_getJson(self): 142 | file = '.my/empty/test.json' 143 | df = AdvancedDataFile(self.client, 'data://' + file, cleanup=True) 144 | if sys.version_info[0] < 3: 145 | payload = {u"hello":u"world"} 146 | else: 147 | payload = {"hello": "world"} 148 | response = df.putJson(payload) 149 | self.assertEqual(response.path,file) 150 | result = json.loads(df.read()) 151 | self.assertDictEqual(result, payload) 152 | self.assertEqual(str(result), str(payload)) 153 | 154 | def test_putZipDir_getZipDir(self): 155 | local_directory = os.path.join(os.getcwd(), "Test/resources/zip_directory") 156 | remote_directory = "data://.my/empty/datafile.zip" 157 | df = AdvancedDataFile(self.client, remote_directory, cleanup=True) 158 | response = df.putAsZip(local_directory) 159 | self.assertEqual(response, df) 160 | 161 | unzipped_local_path = df.getAsZip() 162 | self.assertTrue(os.path.isdir(unzipped_local_path)) 163 | found_files = [] 164 | for _, _, files in os.walk(unzipped_local_path): 165 | for file in files: 166 | found_files.append(file) 167 | self.assertEqual(len(found_files), 3) 168 | 169 | 170 | if __name__ == '__main__': 171 | unittest.main() 172 | -------------------------------------------------------------------------------- /Test/regular/util_test.py: -------------------------------------------------------------------------------- 1 | import sys 2 | # look in ../ BEFORE trying to import Algorithmia. If you append to the 3 | # you will load the version installed on the computer. 4 | sys.path = ['../'] + sys.path 5 | 6 | import unittest 7 | 8 | import Algorithmia 9 | from Algorithmia.util import getParentAndBase, pathJoin 10 | 11 | class UtilTest(unittest.TestCase): 12 | def test_getParentAndBase(self): 13 | self.assertEqual(('a/b', 'c'), getParentAndBase('a/b/c')) 14 | self.assertEqual(('data://foo', 'bar'), getParentAndBase('data://foo/bar')) 15 | self.assertEqual(('data:///', 'foo'), getParentAndBase('data:///foo')) 16 | self.assertEqual(('data://', 'foo'), getParentAndBase('data://foo')) 17 | 18 | def test_getParentAndBase_errors(self): 19 | self.assertRaises(Exception, getParentAndBase, '/') 20 | self.assertRaises(Exception, getParentAndBase, '') 21 | self.assertRaises(Exception, getParentAndBase, 'a/') 22 | 23 | def test_pathJoin(self): 24 | self.assertEqual('/a/b/c/d', pathJoin('/a/b/c/', 'd')) 25 | self.assertEqual('/a/b/c/d', pathJoin('/a/b/c', 'd')) 26 | self.assertEqual('/a//b/c///d', pathJoin('/a//b/c//', '/d')) 27 | 28 | if __name__ == '__main__': 29 | unittest.main() 30 | -------------------------------------------------------------------------------- /Test/resources/cert.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDazCCAlOgAwIBAgIUTikiwxFBpLW4pC+5VfOis1xCYKcwDQYJKoZIhvcNAQEL 3 | BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM 4 | GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMjA1MDMxNzE2MjZaFw0yMjA2 5 | MDIxNzE2MjZaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw 6 | HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB 7 | AQUAA4IBDwAwggEKAoIBAQDBUMZkg/bCJurQIB9znskTjv8URtIK6qqvZpYGTbfI 8 | AzY6HiI0o1gPxjINZW7cBky/9MeEV5zyJghC4WoK099cIUNq2TmAWAjlRgIE8iEy 9 | 9z7QVfbSMainuw0RTlD5/8FRWtRe5v8qwbqLICMn3qv/KsG6bRezyS7UVihwFJua 10 | E4dki+y6KSha4RrCtC43inbPlncB4om7PfJQyt5nI7N4KxbY2L3BUa5/+x1ux/ni 11 | C/3y808vLJVQ6nLYgTEg/6K6lFrig0mUIMnCuOiBsrms3NmBPuDdRri/z1ulFHJB 12 | WVQVQ5DgWher0f/dMzHwyRj3ffC8bAPlhrvLHwPQtNeRAgMBAAGjUzBRMB0GA1Ud 13 | DgQWBBRoC77Hql6kEzk7WC6BeaPBu82K/jAfBgNVHSMEGDAWgBRoC77Hql6kEzk7 14 | WC6BeaPBu82K/jAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCn 15 | W9acM3+rxsiBBClTYEm2tOiukcXEkI7IzvW/4r7P24SmUiDD3vxVVbZ6nevVkg+P 16 | 4/QH+YYE3JUeXaN+xnHYjSy4NKxjd3EHT7BFxLMe0DQaodMj0klHqBtULNzojv8+ 17 | /5tpQsjDLeeeDyOIJNz8r6CU9Gzh7j1EBF8BRdLA1z2UVmt6l6d4o3xOTYpOlZs3 18 | gI+ASxF9ODQzCCOeMYO2qiuMV3RD0oNdIEHUiMD+iHeC1jFGlxZzaWNeuUzP7Yj/ 19 | MOwbBo8l6Hk2BUuUayLxZFLd0wN28IRkLEU5/SOh3mKz79nfPk6pD9rHUO1a53lI 20 | Ua5xJ5tSwG6bMtNnHYYX 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /Test/resources/cert.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDBUMZkg/bCJurQ 3 | IB9znskTjv8URtIK6qqvZpYGTbfIAzY6HiI0o1gPxjINZW7cBky/9MeEV5zyJghC 4 | 4WoK099cIUNq2TmAWAjlRgIE8iEy9z7QVfbSMainuw0RTlD5/8FRWtRe5v8qwbqL 5 | ICMn3qv/KsG6bRezyS7UVihwFJuaE4dki+y6KSha4RrCtC43inbPlncB4om7PfJQ 6 | yt5nI7N4KxbY2L3BUa5/+x1ux/niC/3y808vLJVQ6nLYgTEg/6K6lFrig0mUIMnC 7 | uOiBsrms3NmBPuDdRri/z1ulFHJBWVQVQ5DgWher0f/dMzHwyRj3ffC8bAPlhrvL 8 | HwPQtNeRAgMBAAECggEAPr2OhhTmQ0EKOJ4UVxwTuotQci5CAVUELIUo78bNfNa+ 9 | BMK+60KQVB5JJFvlTPemdS5miqc8wsJhMAOkvPripS4OiWES7nqj+HVuNli3OalQ 10 | 86DSyIlhaX6l0RYP5fOBtHu8LUjfS+swNfMqNchpHhmsYmsBpFIJJtUHrsihb7GR 11 | 4LpNOZ5go4+LG7FX9KaUE4FvAlS7hi6KLSMua10+3+NAlXggbcVikHr3Uq6eQIvk 12 | z09cs+q2FHaESdTjXSIitmYOfJU5KK3QSfXAr/vaqakjnMvfp8MzQ5dHFsy03HRZ 13 | Sy+LjRKOEOCMCT4DmGIPO4V89i3prbVH4JxixCOaeQKBgQDzuwERWE04JEtvfjxS 14 | OAciQKLIxhfa4t2VB65d3115wxfDPIBYU5Mx5YV4aQyOddNxBwpmX/wYwstx2JDZ 15 | 2JM0OjOKLnSvlQfr5UmsY9jUO7CdmgC5HpgbHNhc8uJFw4pd+XypWSjytmVxBSdb 16 | m0+in/iUUQuFNH/+BNLVVgWSiwKBgQDLDBCTEpKQvx2kAc8TEtwrWNhacZILab5D 17 | StQBEL62VfGMdXYaA5dXreo5nqioHfBR3BfAmDecmq3iEFE8/yHJs6pLdcmj0Z1L 18 | 034UQedYLCmL9zuAgC6p4SKIMPubnYtMrNJOL3lq0ibogz3rfOhdN2B6S88IYoSL 19 | M6asdoQN0wKBgCd1VPzr4MSAC75nH3joHS+Ma045087Z/6mK7s2/xbBax1QSTWz/ 20 | Sss/L1aJG0FNDgg0bZiZXYTctHcf6oN6Loq8CXALiVSLuhaUrlK8b3QcncFGF2vg 21 | 6hspllWl9L/6okIIjAgWqSxyHwYnIXIRONlJMMNCQ60zDK2hNkjXflt1AoGAX0w3 22 | Tz/NSGBaogozTUlxymp1iOV63R5xLRYmsKVSTTPDHeBXYNhEpOM8ZnS/xb/fdhwt 23 | jbgjib3TVKHB7zXzfr5zc91BmUCdaeRGbW2NDgYULdwIskP3IsZGtdL/lEb6BS+r 24 | uQRxISCnIEPQwQCr8mw2PM/tyIqsmMTSOmmZiv8CgYBAfIC/cNwJyID6AVauZHQo 25 | S3Bii9CPmPnuklBuS7ikX0bmZ93dzv537nqwGr0j9ksxabLWZRcoxx6MhgmXzXVT 26 | dy48TWpqpHiMNorYskB9tcZSrBCl70bu5qKp2owqWHW0d4hqH3lkBNFhfwNWm+qC 27 | 54x3T/1fqyaqeapCiE5FGA== 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /Test/resources/manifests/example_manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "required_files" : [ 3 | { "name": "squeezenet", 4 | "source_uri": "data://AlgorithmiaSE/image_cassification_demo/squeezenet1_1-f364aa15.pth", 5 | "fail_on_tamper": true, 6 | "metadata": { 7 | "dataset_md5_checksum": "46a44d32d2c5c07f7f66324bef4c7266" 8 | } 9 | }, 10 | { 11 | "name": "labels", 12 | "source_uri": "data://AlgorithmiaSE/image_cassification_demo/imagenet_class_index.json", 13 | "fail_on_tamper": true, 14 | "metadata": { 15 | "dataset_md5_checksum": "46a44d32d2c5c07f7f66324bef4c7266" 16 | } 17 | } 18 | ], 19 | "optional_files": [ 20 | { 21 | "name": "mobilenet", 22 | "source_uri": "data://AlgorithmiaSE/image_cassification_demo/mobilenet_v2-b0353104.pth", 23 | "fail_on_tamper": false, 24 | "metadata": { 25 | "dataset_md5_checksum": "46a44d32d2c5c07f7f66324bef4c7266" 26 | } 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /Test/resources/zip_directory/root.json: -------------------------------------------------------------------------------- 1 | {"location": "root"} -------------------------------------------------------------------------------- /Test/resources/zip_directory/subdirectory/__init__.py: -------------------------------------------------------------------------------- 1 | from .build_wait import get_build 2 | from .publish_algo import publish_algo 3 | from .test_algo import test_algo 4 | -------------------------------------------------------------------------------- /Test/resources/zip_directory/subdirectory/subdir.json: -------------------------------------------------------------------------------- 1 | {"foo": "bar"} -------------------------------------------------------------------------------- /Test/self_signed/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algorithmiaio/algorithmia-python/31f0e9047483bea72b4fdec9ef42b9174b2aa2dc/Test/self_signed/__init__.py -------------------------------------------------------------------------------- /Test/self_signed/algo_failure_test.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | if sys.version_info[0] >= 3: 4 | import unittest 5 | import Algorithmia 6 | import uvicorn 7 | import time 8 | from multiprocessing import Process 9 | 10 | # look in ../ BEFORE trying to import Algorithmia. If you append to the 11 | # you will load the version installed on the computer. 12 | sys.path = ['../'] + sys.path 13 | from requests import Response 14 | 15 | class AlgoTest(unittest.TestCase): 16 | error_500 = Response() 17 | error_500.status_code = 500 18 | error_message = "Non-Algorithm related Failure: " + str(error_500) 19 | 20 | @classmethod 21 | def setUpClass(cls): 22 | cls.client = Algorithmia.client(api_address="https://localhost:8090", api_key="simabcd123", ca_cert=False) 23 | 24 | def test_throw_500_error_HTTP_response_on_algo_request(self): 25 | try: 26 | result = self.client.algo('util/500').pipe(bytearray('foo', 'utf-8')) 27 | except Exception as e: 28 | result = e 29 | pass 30 | self.assertEqual(str(self.error_message), str(result)) 31 | -------------------------------------------------------------------------------- /Test/self_signed/algo_test.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | from Algorithmia.errors import AlgorithmException 4 | from Algorithmia.algorithm import OutputType 5 | import Algorithmia 6 | 7 | import unittest 8 | 9 | # look in ../ BEFORE trying to import Algorithmia. If you append to the 10 | # you will load the version installed on the computer. 11 | sys.path = ['../'] + sys.path 12 | 13 | if sys.version_info.major >= 3: 14 | 15 | class AlgoDummyTest(unittest.TestCase): 16 | @classmethod 17 | def setUpClass(cls): 18 | cls.client = Algorithmia.client(api_address="https://localhost:8090", api_key="simabcd123", ca_cert=False) 19 | 20 | def test_call_customCert(self): 21 | result = self.client.algo('quality/echo').pipe(bytearray('foo', 'utf-8')) 22 | self.assertEquals('binary', result.metadata.content_type) 23 | self.assertEquals(bytearray('foo', 'utf-8'), result.result) 24 | 25 | def test_normal_call(self): 26 | result = self.client.algo('quality/echo').pipe("foo") 27 | self.assertEquals("text", result.metadata.content_type) 28 | self.assertEquals("foo", result.result) 29 | 30 | def test_async_call(self): 31 | result = self.client.algo('quality/echo').set_options(output=OutputType.void).pipe("foo") 32 | self.assertTrue(hasattr(result, "async_protocol")) 33 | self.assertTrue(hasattr(result, "request_id")) 34 | 35 | def test_raw_call(self): 36 | result = self.client.algo('quality/echo').set_options(output=OutputType.raw).pipe("foo") 37 | self.assertEquals("foo", result) 38 | 39 | def test_dict_call(self): 40 | result = self.client.algo('quality/echo').pipe({"foo": "bar"}) 41 | self.assertEquals("json", result.metadata.content_type) 42 | self.assertEquals({"foo": "bar"}, result.result) 43 | 44 | def test_algo_exists(self): 45 | result = self.client.algo('quality/echo').exists() 46 | self.assertEquals(True, result) 47 | 48 | def test_algo_no_exists(self): 49 | result = self.client.algo('quality/not_echo').exists() 50 | self.assertEquals(False, result) 51 | 52 | # TODO: add more coverage examples to check kwargs 53 | def test_get_versions(self): 54 | result = self.client.algo('quality/echo').versions() 55 | self.assertTrue('results' in result) 56 | self.assertTrue('version_info' in result['results'][0]) 57 | self.assertTrue('semantic_version' in result['results'][0]['version_info']) 58 | self.assertEquals('0.1.0', result['results'][0]['version_info']['semantic_version']) 59 | 60 | def test_text_unicode(self): 61 | telephone = u"\u260E" 62 | # Unicode input to pipe() 63 | result1 = self.client.algo('quality/echo').pipe(telephone) 64 | self.assertEquals('text', result1.metadata.content_type) 65 | self.assertEquals(telephone, result1.result) 66 | 67 | # Unicode return in .result 68 | result2 = self.client.algo('quality/echo').pipe(result1.result) 69 | self.assertEquals('text', result2.metadata.content_type) 70 | self.assertEquals(telephone, result2.result) 71 | 72 | def test_algo_info(self): 73 | result = self.client.algo('quality/echo').info() 74 | self.assertTrue('results' in result) 75 | self.assertTrue('resource_type' in result['results'][0]) 76 | self.assertTrue(result['results'][0]['resource_type'] == "algorithm") 77 | 78 | def test_get_build_by_id(self): 79 | result = self.client.algo("quality/echo").get_build("1a392e2c-b09f-4bae-a616-56c0830ac8e5") 80 | self.assertTrue('commit_sha' in result) 81 | 82 | def test_get_build_logs(self): 83 | result = self.client.algo("quality/echo").get_build_logs("1a392e2c-b09f-4bae-a616-56c0830ac8e5") 84 | self.assertTrue('logs' in result) 85 | 86 | def test_get_scm_status(self): 87 | result = self.client.algo("quality/echo").get_scm_status() 88 | self.assertTrue('scm_connection_status' in result) 89 | 90 | def test_exception_ipa_algo(self): 91 | try: 92 | result = self.client.algo('zeryx/raise_exception').pipe("") 93 | except AlgorithmException as e: 94 | self.assertEqual(e.message, "This is an exception") 95 | 96 | if __name__ == '__main__': 97 | unittest.main() 98 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | six 3 | enum-compat 4 | toml 5 | argparse 6 | algorithmia-api-client==1.5.1 7 | algorithmia-adk>=1.2,<1.4 8 | numpy<2 9 | uvicorn==0.14.0 10 | fastapi==0.65.2 11 | -------------------------------------------------------------------------------- /requirements27.txt: -------------------------------------------------------------------------------- 1 | requests 2 | six 3 | enum-compat 4 | toml 5 | argparse 6 | algorithmia-api-client==1.5.1 7 | algorithmia-adk>=1.2,<1.4 8 | numpy<2 9 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | [bdist_wheel] 4 | universal=1 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import setup 4 | 5 | setup( 6 | name='algorithmia', 7 | version=os.environ.get('CLIENT_VERSION', '0.0.0'), 8 | description='Algorithmia Python Client', 9 | long_description='Algorithmia Python Client is a client library for accessing Algorithmia from python code. This library also gets bundled with any Python algorithms in Algorithmia.', 10 | url='http://github.com/algorithmiaio/algorithmia-python', 11 | license='MIT', 12 | author='Algorithmia', 13 | author_email='support@algorithmia.com', 14 | packages=['Algorithmia'], 15 | entry_points = { 16 | 'console_scripts': ['algo = Algorithmia.__main__:main'] 17 | }, 18 | install_requires=[ 19 | 'requests', 20 | 'six', 21 | 'enum-compat', 22 | 'toml', 23 | 'argparse', 24 | 'algorithmia-api-client==1.5.1', 25 | 'algorithmia-adk>=1.2,<1.4' 26 | ], 27 | include_package_data=True, 28 | classifiers=[ 29 | 'Development Status :: 5 - Production/Stable', 30 | 'Intended Audience :: Developers', 31 | 'Natural Language :: English', 32 | 'License :: OSI Approved :: MIT License', 33 | 'Operating System :: OS Independent', 34 | 'Programming Language :: Python', 35 | 'Programming Language :: Python :: 2', 36 | 'Programming Language :: Python :: 2.6', 37 | 'Programming Language :: Python :: 2.7', 38 | 'Programming Language :: Python :: 3', 39 | 'Programming Language :: Python :: 3.3', 40 | 'Topic :: Software Development :: Libraries :: Python Modules', 41 | ], 42 | ) 43 | --------------------------------------------------------------------------------