├── .gitignore ├── Default.sublime-keymap ├── Main.sublime-menu ├── README.md ├── awslambda.py ├── dependencies.json └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | -------------------------------------------------------------------------------- /Default.sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["super+shift+o"], "command": "select_edit_function" }, 3 | { "keys": ["super+option+i"], "command": "invoke_function" } 4 | ] -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id" : "awslambda", 4 | "caption" : "λ", 5 | "children": 6 | [ 7 | { 8 | "caption" : "Select Profile...", 9 | "command" : "select_profile" 10 | }, 11 | { 12 | "caption" : "Edit Function...", 13 | "command" : "select_edit_function" 14 | }, 15 | { 16 | "caption" : "Invoke Function...", 17 | "command" : "invoke_function" 18 | }, 19 | { 20 | "caption" : "Function Info...", 21 | "command" : "select_get_function_info" 22 | }, 23 | { 24 | "caption" : "Install Dependency...", 25 | "command" : "install_dependency" 26 | } 27 | ] 28 | } 29 | ] 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Sublime Text 3 plugin for editing AWS Lambda function sources easily. 2 | 3 | ## Features: 4 | * Supports multiple API key profiles 5 | * Automatically zips and uploads new function code on buffer save 6 | * Can easily fetch and install PyPI package dependencies 7 | * Invoke function directly from inside Sublime and view all output 8 | 9 | # Setup 10 | To use this plugin you will need to configure AWS with your access key ID and secret. 11 | 12 | ### AWS CLI Credentials 13 | If you use the AWS command-line interface [you can run `aws configure` to set up your credentials](http://boto3.readthedocs.io/en/latest/guide/configuration.html). 14 | They will be stored in `~/.aws/credentials`. 15 | 16 | ### Boto 17 | [Or you can configure boto](https://pypi.python.org/pypi/boto3/), the official AWS python client library. 18 | Create a file `~/.boto` with your key and secret: 19 | ``` 20 | [Credentials] 21 | aws_access_key_id = AKNGOINAGBQOWGQNW 22 | aws_secret_access_key = GEIOWGNQAVIONGhg10g08GOAG/GAing2eingAn 23 | ``` 24 | 25 | # Installing The Plugin 26 | 27 | ### Sublime Package Manager 28 | * You must [install the sublime package manager](https://packagecontrol.io/installation) if you don't have it already. 29 | * Select "Install Package" from the command palette and select "AWS Lambda" 30 | 31 | ### Video Instructions 32 | Here's a short video showing how to install sublime package manager and the AWS Lambda plugin: 33 | Installation Video 36 | 37 | 38 | # Demo Video 39 | ### See it in Action! 40 | Demo Video 43 | -------------------------------------------------------------------------------- /awslambda.py: -------------------------------------------------------------------------------- 1 | """Plugin for editing the source of a Lambda in Amazon Web Services. 2 | 3 | Configuration: configure your access key and default region 4 | as shown on https://pypi.python.org/pypi/boto3/1.2.3 5 | 6 | Required IAM roles: 7 | lambda:ListFunctions, 8 | lambda:UpdateFunctionCode, 9 | lambda:GetFunction 10 | """ 11 | 12 | import sublime 13 | import sublime_plugin 14 | import boto3 15 | import botocore 16 | import requests 17 | import subprocess 18 | import tempfile 19 | import os 20 | import json 21 | import re 22 | import zipfile 23 | import io 24 | import pprint 25 | from contextlib import contextmanager 26 | from base64 import b64decode 27 | 28 | INFO_FILE_NAME = ".sublime-lambda-info" 29 | SETTINGS_PATH = "awslambda" 30 | DEBUG = False 31 | 32 | 33 | def _dbg(*msgs): 34 | if DEBUG: 35 | print(msgs) 36 | 37 | 38 | @contextmanager 39 | def cd(newdir): 40 | """Change to a directory, change back when context exits.""" 41 | prevdir = os.getcwd() 42 | os.chdir(os.path.expanduser(newdir)) 43 | try: 44 | yield 45 | finally: 46 | os.chdir(prevdir) 47 | 48 | 49 | class AWSClient(): 50 | """Common AWS methods for all AWS-based commands.""" 51 | 52 | def get_aws_client(self, client_name): 53 | """Return a boto3 client with our session.""" 54 | session = self.get_aws_session() 55 | client = None 56 | try: 57 | client = session.client(client_name) 58 | except botocore.exceptions.NoRegionError: 59 | sublime.error_message("A region must be specified in your configuration.") 60 | return client 61 | 62 | def get_aws_session(self): 63 | """Custom AWS low-level session.""" 64 | if '_aws_session' in globals(): 65 | _dbg("_aws_session exists") 66 | return globals()['_aws_session'] 67 | # load profile from settings 68 | profile_name = self.get_profile_name() 69 | if profile_name not in self.get_available_profiles(): 70 | # this profile name appears to not exist 71 | _dbg("Got bogus AWS profile name {}, resetting...".format(profile_name)) 72 | profile_name = None 73 | session = boto3.session.Session(profile_name=profile_name) 74 | globals()['_aws_session'] = session 75 | return session 76 | 77 | def get_available_profiles(self): 78 | """Return different configuration profiles available for AWS. 79 | 80 | c.f. https://github.com/boto/boto3/issues/704#issuecomment-231459948 81 | """ 82 | sess = boto3.session.Session(profile_name=None) 83 | if not sess: 84 | return [] 85 | if not hasattr(sess, 'available_profiles'): 86 | # old boto :/ 87 | return sess._session.available_profiles 88 | return sess.available_profiles() 89 | 90 | def get_profile_name(self): 91 | """Get selected profile name.""" 92 | return self._settings().get("profile_name") 93 | 94 | def test_aws_credentials_exist(self): 95 | """Check if AWS credentials are available.""" 96 | session = boto3.session.Session() 97 | if session.get_credentials(): 98 | return True 99 | return False 100 | 101 | def _settings(self): 102 | """Get settings for this plugin.""" 103 | return sublime.load_settings(SETTINGS_PATH) 104 | 105 | 106 | class LambdaClient(AWSClient): 107 | """Common methods for Lambda commands.""" 108 | 109 | def __init__(self, *arg): 110 | """Init Lambda client.""" 111 | super() 112 | self.functions = [] 113 | 114 | def _clear_client(self): 115 | _dbg("Clearing client") 116 | if '_aws_session' in globals(): 117 | del globals()['_aws_session'] 118 | _dbg("deleted _aws_session") 119 | if '_lambda_client' in globals(): 120 | del globals()['_lambda_client'] 121 | _dbg("deleted _lambda_client") 122 | 123 | @property 124 | def client(self): 125 | """Return AWS Lambda boto3 client.""" 126 | if not self.test_aws_credentials_exist(): 127 | self._clear_client() 128 | sublime.error_message( 129 | "AWS credentials not found.\n" + 130 | "Please follow the instructions at\n" + 131 | "https://pypi.python.org/pypi/boto3/") 132 | raise Exception("AWS credentials needed") 133 | if '_lambda_client' in globals() and globals()['_lambda_client']: 134 | _dbg("_lambda_client_exists") 135 | return globals()['_lambda_client'] 136 | client = self.get_aws_client('lambda') 137 | globals()['_lambda_client'] = client 138 | return client 139 | 140 | def select_aws_profile(self, window): 141 | """Select a profile to use for our AWS session. 142 | 143 | Multiple profiles (access keys) can be defined in AWS credential configuration. 144 | """ 145 | profiles = self.get_available_profiles() 146 | if len(profiles) <= 1: 147 | # no point in going any further eh 148 | return 149 | 150 | def profile_selected_cb(selected_index): 151 | if selected_index == -1: 152 | # cancelled 153 | return 154 | profile = profiles[selected_index] 155 | if not profile: 156 | return 157 | # save in settings 158 | self._settings().set("profile_name", profile) 159 | # clear the current session 160 | self._clear_client() 161 | window.status_message("Using AWS profile {}".format(profile)) 162 | window.show_quick_panel(profiles, profile_selected_cb) 163 | 164 | def download_function(self, function): 165 | """Download source to a function and open it in a new window.""" 166 | arn = function['FunctionArn'] 167 | func_code_res = self.client.get_function(FunctionName=arn) 168 | url = func_code_res['Code']['Location'] 169 | temp_dir_path = self.extract_zip_url(url) 170 | self.open_lambda_package_in_new_window(temp_dir_path, function) 171 | 172 | def extract_zip_url(self, file_url): 173 | """Fetch a zip file and decompress it. 174 | 175 | :returns: hash of filename to contents. 176 | """ 177 | url = requests.get(file_url) 178 | with zipfile.ZipFile(io.BytesIO(url.content)) as zip: 179 | # extract to temporary directory 180 | temp_dir_path = tempfile.mkdtemp() 181 | print('created temporary directory', temp_dir_path) 182 | with cd(temp_dir_path): 183 | zip.extractall() # to cwd 184 | return temp_dir_path 185 | 186 | def zip_dir(self, dir_path): 187 | """Zip up a directory and all of its contents and return an in-memory zip file.""" 188 | out = io.BytesIO() 189 | zip = zipfile.ZipFile(out, "w", compression=zipfile.ZIP_DEFLATED) 190 | 191 | # files to skip 192 | skip_re = re.compile("\.pyc$") # no compiled python files pls 193 | for root, dirs, files in os.walk(dir_path): 194 | # add files 195 | for file in files: 196 | file_path = os.path.join(root, file) 197 | in_zip_path = file_path.replace(dir_path, "", 1).lstrip("\\/") 198 | print("Adding file to lambda zip archive: '{}'".format(in_zip_path)) 199 | if skip_re.search(in_zip_path): # skip this file? 200 | continue 201 | zip.write(file_path, in_zip_path) 202 | zip.close() 203 | if False: 204 | # debug 205 | zip.printdir() 206 | return out 207 | 208 | def upload_code(self, view, func): 209 | """Zip the temporary directory and upload it to AWS.""" 210 | print(func) 211 | sublime_temp_path = func['sublime_temp_path'] 212 | if not sublime_temp_path or not os.path.isdir(sublime_temp_path): 213 | print("error: failed to find temp lambda dir") 214 | # create zip archive, upload it 215 | try: 216 | view.set_status("lambda", "Creating lambda archive...") 217 | print("Creating zip archive...") 218 | zip_data = self.zip_dir(sublime_temp_path) # create in-memory zip archive of our temp dir 219 | zip_bytes = zip_data.getvalue() # read contents of BytesIO buffer 220 | except Exception as e: 221 | # view.show_popup("

Error saving

Failed to save: {}

".format(html.escape(e))) 222 | self.display_error("Error creating zip archive for upload: {}".format(e)) 223 | view.set_status("lambda", "Failed to save lambda") 224 | else: 225 | # zip success? 226 | if zip_bytes: 227 | print("Created zip archive, len={}".format(len(zip_bytes))) 228 | # upload time 229 | try: 230 | print("Uploading lambda archive...") 231 | res = self.client.update_function_code( 232 | FunctionName=func['FunctionArn'], 233 | ZipFile=zip_bytes, 234 | ) 235 | except Exception as e: 236 | self.display_error("Error uploading lambda: {}".format(e)) 237 | view.set_status("lambda", "Failed to upload lambda") 238 | else: 239 | print("Upload successful.") 240 | view.set_status("lambda", "Lambda uploaded as {} [{} bytes]".format(res['FunctionName'], res['CodeSize'])) 241 | else: 242 | # got empty zip archive? 243 | view.set_status("lambda", "Failed to save lambda") 244 | 245 | def _load_functions(self, quiet=False): 246 | paginator = self.client.get_paginator('list_functions') 247 | if not quiet: 248 | sublime.status_message("Fetching lambda functions...") 249 | response_iterator = paginator.paginate() 250 | self.functions = [] 251 | try: 252 | for page in response_iterator: 253 | # print(page['Functions']) 254 | for func in page['Functions']: 255 | self.functions.append(func) 256 | except botocore.exceptions.ClientError as cerr: 257 | # display error fetching functions 258 | if not quiet: 259 | sublime.error_message(cerr.response['Error']['Message']) 260 | raise cerr 261 | if not quiet: 262 | sublime.status_message("Lambda functions fetched.") 263 | 264 | def select_function(self, callback): 265 | """Prompt to select a function then calls callback(function).""" 266 | self._load_functions() 267 | if not self.functions: 268 | sublime.message_dialog("No lambda functions were found.") 269 | return 270 | func_list = [] 271 | for func in self.functions: 272 | last_mod = func['LastModified'] # ugh 273 | # last_mod = last_mod.strftime("%Y-%m-%d %H:%M") 274 | func_list.append([ 275 | func['FunctionName'], 276 | func['Description'], 277 | "Last modified: {}".format(last_mod), 278 | "Runtime: {}".format(func['Runtime']), 279 | "Size: {}".format(func['CodeSize']), 280 | ]) 281 | 282 | def selected_cb(selected_index): 283 | if selected_index == -1: 284 | # cancelled 285 | return 286 | function = self.functions[selected_index] 287 | if not function: 288 | sublime.error_message("Unknown function selected.") 289 | callback(function) 290 | self.window.show_quick_panel(func_list, selected_cb) 291 | 292 | def display_function_info(self, function): 293 | """Create an output panel with the function details.""" 294 | if not isinstance(self, sublime_plugin.WindowCommand): 295 | raise Exception("display_function_info must be called on a WindowCommand") 296 | # v = self.window.create_output_panel("lambda_info_{}".format(function['FunctionName'])) 297 | nv = self.window.new_file() 298 | nv.view.set_scratch(True) 299 | nv.run_command("display_function_info", {'function': function}) 300 | 301 | def edit_function(self, function): 302 | """Edit a function's source.""" 303 | if not isinstance(self, sublime_plugin.WindowCommand): 304 | raise Exception("edit_function must be called on a WindowCommand") 305 | nv = self.window.create_output_panel("lambda_info_{}".format(function['FunctionName'])) 306 | nv.view.set_scratch(True) 307 | nv.run_command("edit_function", {'function': function}) 308 | 309 | def open_in_new_window(self, paths=[], cmd=None): 310 | """Open paths in a new sublime window.""" 311 | # from wbond https://github.com/titoBouzout/SideBarEnhancements/blob/st3/SideBar.py#L1916 312 | items = [] 313 | 314 | executable_path = sublime.executable_path() 315 | 316 | if sublime.platform() == 'osx': 317 | app_path = executable_path[:executable_path.rfind(".app/") + 5] 318 | executable_path = app_path + "Contents/SharedSupport/bin/subl" 319 | items.append(executable_path) 320 | if cmd: 321 | items.extend(['--command', cmd]) 322 | items.extend(paths) 323 | subprocess.Popen(items) 324 | 325 | def lambda_info_path(self, package_path): 326 | """Return path to the lambda info file for a downloaded package.""" 327 | return os.path.join(package_path, INFO_FILE_NAME) 328 | 329 | def open_lambda_package_in_new_window(self, package_path, function): 330 | """Spawn a new sublime window to edit an unzipped lambda package.""" 331 | # add a file to the directory to pass in our function info 332 | lambda_info_path = self.lambda_info_path(package_path) 333 | 334 | function['sublime_temp_path'] = package_path 335 | with open(lambda_info_path, 'w') as f: 336 | f.write(json.dumps(function)) 337 | self.open_in_new_window(paths=[package_path], cmd="prepare_lambda_window") 338 | 339 | def invoke_function(self, func): 340 | """Invoke a lambda function. 341 | 342 | :returns: return_object, log_output, error 343 | """ 344 | payload = {'sublime': True} 345 | res = self.client.invoke( 346 | FunctionName=func['FunctionName'], 347 | InvocationType='RequestResponse', # synchronous 348 | LogType='Tail', # give us last 4kb output in x-amz-log-result 349 | Payload=json.dumps(payload), 350 | ) 351 | # if res['FunctionError']: 352 | # self.display_error("Failed to invoke function: " + res['FunctionError']) 353 | # return 354 | 355 | # return value from the lambda 356 | res_payload = res['Payload'] 357 | if res_payload: 358 | res_payload = res_payload.read() 359 | # output 360 | res_log = res['LogResult'] 361 | if res_log: 362 | res_log = b64decode(res_log).decode('utf-8') 363 | err = None 364 | if 'FunctionError' in res: 365 | err = res['FunctionError'] 366 | return res_payload, res_log, err 367 | 368 | def invoke_function_test(self, function_name): 369 | """Ignore for now.""" 370 | self.invoke_function() 371 | 372 | def get_window_function(self, window): 373 | """Try to see if there is a function associated with this window. 374 | 375 | :returns: function info dict. 376 | """ 377 | proj_data = window.project_data() 378 | if not proj_data or 'lambda_function' not in proj_data: 379 | return 380 | func = proj_data['lambda_function'] 381 | return func 382 | 383 | def get_view_function(self, view): 384 | """Try to see if there is a function associated with this view.""" 385 | win = view.window() 386 | return self.get_window_function(win) 387 | 388 | def display_error(self, err): 389 | """Pop up an error message to the user.""" 390 | sublime.message_dialog(err) 391 | 392 | 393 | class PrepareLambdaWindowCommand(sublime_plugin.WindowCommand, LambdaClient): 394 | """Called when a lambda package has been downloaded and extracted and opened in a new window.""" 395 | 396 | def run(self): 397 | """Mark this project as being tied to a lambda function.""" 398 | win = self.window 399 | lambda_file_name = os.path.join(win.folders()[0], INFO_FILE_NAME) 400 | if not os.path.isfile(lambda_file_name): 401 | print(lambda_file_name + " does not exist") 402 | return 403 | lambda_file = open(lambda_file_name, 'r') 404 | func_info_s = lambda_file.read() 405 | lambda_file.close() 406 | if not func_info_s: 407 | print("Failed to read lambda file info") 408 | func_info = json.loads(func_info_s) 409 | proj_data = win.project_data() 410 | proj_data['lambda_function'] = func_info 411 | win.set_project_data(proj_data) 412 | 413 | # open default func file if it exists 414 | default_function_file = os.path.join(win.folders()[0], 'lambda_function.py') 415 | if os.path.isfile(default_function_file): 416 | win.open_file(default_function_file) 417 | 418 | 419 | class LambdaSaveHookListener(sublime_plugin.EventListener, LambdaClient): 420 | """Listener for events pertaining to editing lambdas.""" 421 | 422 | def on_post_save_async(self, view): 423 | """Sync modified lambda source.""" 424 | func = self.get_view_function(view) 425 | if not func: 426 | return 427 | # okay we're saving a lambda project! let's sync it back up! 428 | self.upload_code(view, func) 429 | 430 | 431 | class SelectEditFunctionCommand(sublime_plugin.WindowCommand, LambdaClient): 432 | """Fetch functions.""" 433 | 434 | def run(self): 435 | """Display choices in a quick panel.""" 436 | self.select_function(self.download_function) 437 | 438 | 439 | class SelectGetFunctionInfoCommand(sublime_plugin.WindowCommand, LambdaClient): 440 | """Display some handy info about a function.""" 441 | 442 | def run(self): 443 | """Display choices in a quick panel.""" 444 | self.select_function(self.display_function_info) 445 | 446 | 447 | class InvokeFunctionCommand(sublime_plugin.WindowCommand, LambdaClient): 448 | """Invoke current function.""" 449 | 450 | def run(self): 451 | """Display function invocation result in a new file.""" 452 | window = self.window 453 | func = self.get_window_function(window) 454 | if not func: 455 | self.display_error("No function is associated with this window.") 456 | return 457 | result, result_log, error_status = self.invoke_function(func) 458 | # display output 459 | nv = self.window.new_file() 460 | nv.set_scratch(True) 461 | fargs = dict( 462 | function=func, 463 | result=result.decode("utf-8"), 464 | result_log=result_log, 465 | error_status=error_status 466 | ) 467 | nv.run_command("display_invocation_result", fargs) 468 | 469 | def is_enabled(self): 470 | """Enable or disable option, depending on if the current window is associated with a function.""" 471 | func = self.get_window_function(self.window) 472 | if not func: 473 | return False 474 | return True 475 | 476 | 477 | class InstallDependencyCommand(sublime_plugin.WindowCommand, LambdaClient): 478 | """Install a package via pip.""" 479 | 480 | def run(self): 481 | """Call out to system to install packages via pip.""" 482 | window = self.window 483 | func = self.get_window_function(window) 484 | if not func: 485 | self.display_error("No lambda function is associated with this window.") 486 | return 487 | self.window.show_input_panel("PyPI Packages To Install:", '', lambda s: self._install_packages(func, s), None, None) 488 | 489 | def _install_packages(self, func, packages): 490 | if not packages: 491 | print("No packages selected to isntall") 492 | return 493 | 494 | cmd = """set -x \ 495 | rm -f pip.log; \ 496 | pip install --target pip/ --no-compile --log pip.log $LAMBDA_PACKAGES_TO_INSTALL \ 497 | && rm -rf pip/*.dist-info pip/tests \ 498 | && mv pip/* $PWD/ \ 499 | ; rm -rf pip 500 | """ 501 | cwd = func['sublime_temp_path'] 502 | env = dict(LAMBDA_PACKAGES_TO_INSTALL=packages) 503 | output = "" 504 | with subprocess.Popen(cmd, 505 | executable="/bin/bash", 506 | shell=True, 507 | env=env, 508 | stdout=subprocess.PIPE, 509 | stderr=subprocess.STDOUT, 510 | universal_newlines=True, 511 | cwd=cwd) as proc: 512 | output = proc.stdout.read() 513 | print(output) 514 | # refresh file list 515 | self.window.run_command("refresh_folder_list") 516 | # if all went well there should be a pip log 517 | pip_log_path = os.path.join(cwd, "pip.log") 518 | if not os.path.isfile(pip_log_path): 519 | self.display_error("Failed to install {}\n{}".format(packages, output)) 520 | return 521 | # display result 522 | pip_logs = "" 523 | with open(pip_log_path, 'r') as logfile: 524 | pip_logs = logfile.read() 525 | os.unlink(pip_log_path) 526 | if pip_logs: 527 | nv = self.window.new_file() 528 | nv.set_scratch(True) 529 | nv.set_name("Pip output") 530 | nv.run_command("display_string", dict(str=pip_logs)) 531 | 532 | 533 | class EditFunctionInfoCommand(sublime_plugin.TextCommand, LambdaClient): 534 | """Open editor for source of a function.""" 535 | 536 | def run(self, edit, function=None): 537 | """Ok.""" 538 | self.download_function(function) 539 | 540 | 541 | class DisplayFunctionInfoCommand(sublime_plugin.TextCommand, LambdaClient): 542 | """Insert info about a function into the current view.""" 543 | 544 | def run(self, edit, function=None): 545 | """Ok.""" 546 | pp = pprint.PrettyPrinter(indent=4) 547 | self.view.insert(edit, self.view.text_point(0, 0), pp.pformat(function)) 548 | 549 | 550 | class DisplayStringCommand(sublime_plugin.TextCommand, LambdaClient): 551 | """Just display a new view with a string inside.""" 552 | 553 | def run(self, edit, str=None): 554 | """Display str.""" 555 | self.view.insert(edit, self.view.text_point(0, 0), str) 556 | 557 | 558 | class DisplayInvocationResultCommand(sublime_plugin.TextCommand, LambdaClient): 559 | """Display a function's results in this view.""" 560 | 561 | def run(self, edit, function=None, result=None, result_log=None, error_status=None): 562 | """Ok.""" 563 | err = "" 564 | if error_status: 565 | err = "\nError handled status: {}\n".format(error_status) 566 | out = """{funcname} Results 567 | {err} 568 | Log output: {log} 569 | 570 | Result: {res}""".format(funcname=function['FunctionName'], res=result, log=result_log, err=err) 571 | self.view.insert(edit, self.view.text_point(0, 0), out) 572 | 573 | 574 | class SelectProfileCommand(sublime_plugin.WindowCommand, LambdaClient): 575 | """Select an AWS configuration profile to use and save in settings.""" 576 | 577 | def run(self): 578 | """Display choices in a quick panel.""" 579 | self.select_aws_profile(self.window) 580 | 581 | def is_enabled(self): 582 | """Must have multiple profiles to select one.""" 583 | profiles = self.get_available_profiles() 584 | if len(profiles) > 1: 585 | return True 586 | return False 587 | -------------------------------------------------------------------------------- /dependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema_version": "3.0.0", 3 | 4 | "*": { 5 | "*": [ 6 | "boto3", 7 | "requests" 8 | ] 9 | } 10 | } -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | 2 | [flake8] 3 | ignore = 4 | max-line-length = 160 5 | 6 | --------------------------------------------------------------------------------