pax_global_header00006660000000000000000000000064133512734270014521gustar00rootroot0000000000000052 comment=8950f03d9d720879890af6c11537b8f9789ce5a9 aws-shell-0.2.1/000077500000000000000000000000001335127342700134205ustar00rootroot00000000000000aws-shell-0.2.1/.changes/000077500000000000000000000000001335127342700151065ustar00rootroot00000000000000aws-shell-0.2.1/.changes/0.2.0.json000066400000000000000000000011651335127342700164410ustar00rootroot00000000000000[ { "category": "Command History", "description": "Ensure aws prefix is not prepended twice in command history. Fixes `#157 `__", "type": "bugfix" }, { "category": "Keybinding", "description": "Switching between emacs/vi keybindings now functions properly", "type": "bugfix" }, { "category": "Documentation", "description": "The documentation pane can now be focused and navigated. Fixes `#74 `__, `#159 `__", "type": "enhancement" } ]aws-shell-0.2.1/.changes/0.2.1.json000066400000000000000000000003261335127342700164400ustar00rootroot00000000000000[ { "category": "AWS CLI", "description": "Fixes `#208 `__. Update the AWS Shell to support the latest version of the AWS CLI.", "type": "bugfix" } ]aws-shell-0.2.1/.gitignore000066400000000000000000000005641335127342700154150ustar00rootroot00000000000000*.py[co] *.DS_Store # Packages *.egg *.egg-info dist build eggs parts var sdist develop-eggs .installed.cfg # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox .cache #Translations *.mo #Mr Developer .mr.developer.cfg # Emacs backup files *~ # Eclipse IDE /.project /.pydevproject # IDEA IDE .idea* src/ # Completions Index completions.idx aws-shell-0.2.1/.pylintrc000066400000000000000000000264011335127342700152700ustar00rootroot00000000000000[MASTER] # Specify a configuration file. #rcfile= # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). #init-hook= # Add files or directories to the blacklist. They should be base names, not # paths. ignore=compat.py # Pickle collected data for later comparisons. persistent=yes # List of plugins (as comma separated values of python modules names) to load, # usually to register additional checkers. load-plugins= # Use multiple processes to speed up Pylint. jobs=1 # Allow loading of arbitrary C extensions. Extensions are imported into the # active Python interpreter and may run arbitrary code. unsafe-load-any-extension=no # A comma-separated list of package or module names from where C extensions may # be loaded. Extensions are loading into the active Python interpreter and may # run arbitrary code extension-pkg-whitelist= # Allow optimization of some AST trees. This will activate a peephole AST # optimizer, which will apply various small optimizations. For instance, it can # be used to obtain the result of joining multiple strings with the addition # operator. Joining a lot of strings can lead to a maximum recursion error in # Pylint and this flag can prevent that. It has one side effect, the resulting # AST will be different than the one from reality. optimize-ast=no [MESSAGES CONTROL] # Only show warnings with the listed confidence levels. Leave empty to show # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED confidence= # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option # multiple time. See also the "--disable" option for examples. #enable= # Disable the message, report, category or checker with the given id(s). You # can either give multiple identifiers separated by comma (,) or put this # option multiple times (only on the command line, not in the configuration # file where it should appear only once).You can also use "--disable=all" to # disable everything first and then reenable specific checks. For example, if # you want to run only the similarities checker, you can use "--disable=all # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" disable=E1608,R0201,W1627,E1601,E1603,E1602,E1605,E1604,E1607,E1606,W1621,W1620,W1623,W1622,W1625,W1624,W1609,W1608,W1607,W1606,W1605,W1604,W1603,W1602,W1601,W1639,W0613,W1640,I0021,W1638,I0020,C0111,W1618,W1619,W1630,W1626,W1637,W1634,W1635,W1610,W1611,W1612,W1613,W1614,W1615,W1616,W1617,W1632,R0903,W1633,W0231,W0704,W0232,W1628,W1629,W1636 [REPORTS] # Set the output format. Available formats are text, parseable, colorized, msvs # (visual studio) and html. You can also give a reporter class, eg # mypackage.mymodule.MyReporterClass. output-format=text # Put messages in a separate file for each module / package specified on the # command line instead of printing them on stdout. Reports (if any) will be # written in a file name "pylint_global.[txt|html]". files-output=no # Tells whether to display a full report or only the messages reports=no # Python expression which should return a note less than 10 (10 is the highest # note). You have access to the variables errors warning, statement which # respectively contain the number of errors / warnings messages and the total # number of statements analyzed. This is used by the global evaluation report # (RP0004). evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) # Template used to display messages. This is a python new-style format string # used to format the message information. See doc for all details #msg-template= [BASIC] # List of builtins function names that should not be used, separated by a comma bad-functions=apply,reduce # Good variable names which should always be accepted, separated by a comma good-names=e,i,j,k,n,ex,Run,_ # Bad variable names which should always be refused, separated by a comma bad-names=foo,bar,baz,toto,tutu,tata # Colon-delimited sets of names that determine each other's naming style when # the name regexes allow several styles. name-group= # Include a hint for the correct naming format with invalid-name include-naming-hint=no # Regular expression matching correct function names function-rgx=[a-z_][a-z0-9_]{2,50}$ # Naming hint for function names function-name-hint=[a-z_][a-z0-9_]{2,30}$ # Regular expression matching correct variable names variable-rgx=[a-z_][a-z0-9_]{0,50}$ # Naming hint for variable names variable-name-hint=[a-z_][a-z0-9_]{2,30}$ # Regular expression matching correct constant names const-rgx=(([a-zA-Z_][a-zA-Z0-9_]*)|(__.*__))$ # Naming hint for constant names const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ # Regular expression matching correct attribute names attr-rgx=[a-z_][a-z0-9_]{1,50}$ # Naming hint for attribute names attr-name-hint=[a-z_][a-z0-9_]{2,30}$ # Regular expression matching correct argument names argument-rgx=[a-z_][a-z0-9_]{0,50}$ # Naming hint for argument names argument-name-hint=[a-z_][a-z0-9_]{2,30}$ # Regular expression matching correct class attribute names class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ # Naming hint for class attribute names class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ # Regular expression matching correct inline iteration names inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ # Naming hint for inline iteration names inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ # Regular expression matching correct class names class-rgx=[A-Z_][a-zA-Z0-9]+$ # Naming hint for class names class-name-hint=[A-Z_][a-zA-Z0-9]+$ # Regular expression matching correct module names module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ # Naming hint for module names module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ # Regular expression matching correct method names method-rgx=[a-z_][a-z0-9_]{2,50}$ # Naming hint for method names method-name-hint=[a-z_][a-z0-9_]{2,30}$ # Regular expression which should only match function or class names that do # not require a docstring. no-docstring-rgx=.* # Minimum line length for functions/classes that require docstrings, shorter # ones are exempt. docstring-min-length=-1 [FORMAT] # Maximum number of characters on a single line. max-line-length=80 # Regexp for a line that is allowed to be longer than the limit. ignore-long-lines=^\s*(# )??$ # Allow the body of an if to be on the same line as the test if there is no # else. single-line-if-stmt=no # List of optional constructs for which whitespace checking is disabled no-space-check=trailing-comma,dict-separator # Maximum number of lines in a module max-module-lines=1000 # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 # tab). indent-string=' ' # Number of spaces of indent required inside a hanging or continued line. indent-after-paren=4 # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. expected-line-ending-format= [LOGGING] # Logging modules to check that the string format arguments are in logging # function parameter format logging-modules=logging [MISCELLANEOUS] # List of note tags to take in consideration, separated by a comma. notes=FIXME,XXX [SIMILARITIES] # Minimum lines number of a similarity. min-similarity-lines=3 # Ignore comments when computing similarities. ignore-comments=no # Ignore docstrings when computing similarities. ignore-docstrings=no # Ignore imports when computing similarities. ignore-imports=no [SPELLING] # Spelling dictionary name. Available dictionaries: none. To make it working # install python-enchant package. spelling-dict= # List of comma separated words that should not be checked. spelling-ignore-words= # A path to a file that contains private dictionary; one word per line. spelling-private-dict-file= # Tells whether to store unknown words to indicated private dictionary in # --spelling-private-dict-file option instead of raising a message. spelling-store-unknown-words=no [TYPECHECK] # Tells whether missing members accessed in mixin class should be ignored. A # mixin class is detected if its name ends with "mixin" (case insensitive). ignore-mixin-members=yes # List of module names for which member attributes should not be checked # (useful for modules/projects where namespaces are manipulated during runtime # and thus existing member attributes cannot be deduced by static analysis ignored-modules= # List of classes names for which member attributes should not be checked # (useful for classes with attributes dynamically set). ignored-classes=SQLObject # List of members which are set dynamically and missed by pylint inference # system, and so shouldn't trigger E0201 when accessed. Python regular # expressions are accepted. generated-members=REQUEST,acl_users,aq_parent,objects,DoesNotExist,md5,sha1,sha224,sha256,sha384,sha512 [VARIABLES] # Tells whether we should check for unused import in __init__ files. init-import=no # A regular expression matching the name of dummy variables (i.e. expectedly # not used). dummy-variables-rgx=_|dummy|ignore # List of additional names supposed to be defined in builtins. Remember that # you should avoid to define new builtins when possible. additional-builtins= # List of strings which can identify a callback function by name. A callback # name must start or end with one of those strings. callbacks=cb_,_cb [CLASSES] # List of method names used to declare (i.e. assign) instance attributes. defining-attr-methods=__init__,__new__,setUp # List of valid names for the first argument in a class method. valid-classmethod-first-arg=cls # List of valid names for the first argument in a metaclass class method. valid-metaclass-classmethod-first-arg=mcs # List of member names, which should be excluded from the protected access # warning. exclude-protected=_asdict,_fields,_replace,_source,_make [DESIGN] # Maximum number of arguments for function / method max-args=5 # Argument names that match this expression will be ignored. Default to name # with leading underscore ignored-argument-names=_.* # Maximum number of locals for function / method body max-locals=15 # Maximum number of return / yield for function / method body max-returns=6 # Maximum number of branch for function / method body max-branches=12 # Maximum number of statements in function / method body max-statements=30 # Maximum number of parents for a class (see R0901). max-parents=5 # Maximum number of attributes for a class (see R0902). max-attributes=7 # Minimum number of public methods for a class (see R0903). min-public-methods=0 # Maximum number of public methods for a class (see R0904). max-public-methods=20 [IMPORTS] # Deprecated modules which should not be used, separated by a comma deprecated-modules=regsub,string,TERMIOS,Bastion,rexec,UserDict # Create a graph of every (i.e. internal and external) dependencies in the # given file (report RP0402 must not be disabled) import-graph= # Create a graph of external dependencies in the given file (report RP0402 must # not be disabled) ext-import-graph= # Create a graph of internal dependencies in the given file (report RP0402 must # not be disabled) int-import-graph= [EXCEPTIONS] # Exceptions that will emit a warning when being caught. Defaults to # "Exception" overgeneral-exceptions=Exception aws-shell-0.2.1/.travis.yml000066400000000000000000000010651335127342700155330ustar00rootroot00000000000000language: python matrix: include: - python: 2.6 env: TEST_TYPE=test - python: 2.7 env: TEST_TYPE=test - python: 2.7 env: TEST_TYPE=check - python: 3.3 env: TEST_TYPE=test - python: 3.4 env: TEST_TYPE=test - python: 3.5 env: TEST_TYPE=test - python: 3.6 env: TEST_TYPE=test sudo: false install: - if [[ $TEST_TYPE == 'check' ]]; then pip install -r requirements-dev.txt; fi - python scripts/ci/install script: - make $TEST_TYPE aws-shell-0.2.1/CHANGELOG.rst000066400000000000000000000043451335127342700154470ustar00rootroot00000000000000========= CHANGELOG ========= 0.2.1 ===== * bugfix:AWS CLI: Fixes `#208 `__. Update the AWS Shell to support the latest version of the AWS CLI. 0.2.0 ===== * bugfix:Command History: Ensure aws prefix is not prepended twice in command history. Fixes `#157 `__ * bugfix:Keybinding: Switching between emacs/vi keybindings now functions properly * enhancement:Documentation: The documentation pane can now be focused and navigated. Fixes `#74 `__, `#159 `__ 0.1.1 ===== * bugfix:AWS CLI: Fix issue with latest version of the AWS CLI that would cause the AWS Shell to raise an exception on startup. The minimum version of the AWS CLI has been bumped to 1.10.30. (`issue 118 `__) 0.1.0 ===== * feature:Dot Commands: Add ``.exit/.quit`` dot commands (`issue 97 `__) * feature:Documentation: Show documentation for global arguments (`issue 51 `__) * feature:Dot Commands: Add ``.cd`` dot command (`issue 97 `__) * feature:Dot Commands: Add ``.profile`` dot command (`issue 97 `__) * feature:Command Line Arguments: Add ``--profile`` command line option (`issue 89 `__) * bugfix:Completer: Fix crash when attempting server side completion with no region configured option (`issue 84 `__) * feature:Lexer: Add lexer/syntax highlighting (`issue 27 `__) * feature:Server Side Completion: Add server side completion for Elastic Load Balancing (`issue 79 `__) * feature:Server Side Completion: Add server side completion for Amazon Kinesis (`issue 73 `__) * bugfix:Windows: Fix crash when using ``.edit`` on Windows (`issue 55 `__) aws-shell-0.2.1/LICENSE.txt000066400000000000000000000261361335127342700152530ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. aws-shell-0.2.1/MANIFEST.in000066400000000000000000000002111335127342700151500ustar00rootroot00000000000000include README.rst include LICENSE.txt include NOTICE.txt include awsshell/awsshellrc recursive-include awsshell/data *.json graft tests aws-shell-0.2.1/Makefile000066400000000000000000000017521335127342700150650ustar00rootroot00000000000000# Eventually I'll add: # py.test --cov awsshell --cov-report term-missing --cov-fail-under 95 tests/ # which will fail if tests are under 95% check: ###### FLAKE8 ##### # No unused imports, no undefined vars, # follow pep8, and max cyclomatic complexity of 15. # I'd eventually like to lower this down to < 10. flake8 --ignore=E731,W503 --exclude awsshell/compat.py --max-complexity 15 awsshell/*.py # # ##### DOC8 ###### # Correct rst formatting for docstrings # ## doc8 awsshell/*.py # # # # Proper docstring conventions according to pep257 # # pep257 --add-ignore=D100,D101,D102,D103,D104,D105,D204 # # # ###### PYLINT ERRORS ONLY ###### # # # pylint --rcfile .pylintrc -E awsshell test: python scripts/ci/run-tests pylint: ###### PYLINT ###### # Python linter. This will generally not have clean output. # So you'll need to manually verify this output. # # pylint --rcfile .pylintrc awsshell coverage: py.test --cov awsshell --cov-report term-missing tests/ aws-shell-0.2.1/NOTICE.txt000066400000000000000000000001221335127342700151350ustar00rootroot00000000000000AWS Shell Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. aws-shell-0.2.1/README.rst000066400000000000000000000234701335127342700151150ustar00rootroot00000000000000aws-shell - The interactive productivity booster for the AWS CLI ================================================================ .. image:: https://aws-developer-blog-media.s3-us-west-2.amazonaws.com/cli/Super-Charge-Your-AWS-Command-Line-Experience-with-aws-shell/aws-shell-final.gif Installation ============ The aws-shell requires python and `pip`_ to install. You can install the aws-shell using `pip`_:: $ pip install aws-shell If you are not installing into a virtualenv you can run:: $ sudo pip install aws-shell **Mac OS X (10.11 El Capitan) users**: There is a known issue with Apple and its included python package dependencies (more info at https://github.com/pypa/pip/issues/3165). We are investigating ways to fix this issue but in the meantime, to install the aws-shell, you can run: ``sudo pip install aws-shell --upgrade --ignore-installed six`` Once you've installed the aws-shell, you can now run:: $ aws-shell To exit the shell, press ``Ctrl-D``. Upgrading the aws-shell ----------------------- If you want to upgrade to the latest version of the aws-shell, you can run:: $ pip install --upgrade aws-shell You can also use this upgrade command whenever a new version of the AWS CLI is released that includes new services and API updates. You will then be able to use these new services and API updates in the aws-shell. Supported Python Versions ------------------------- The aws-shell works on the same python versions supported by the AWS CLI: * 2.6.5 and greater * 2.7.x and greater * 3.3.x and greater * 3.4.x and greater Configuration ============= The aws-shell uses the same configuration settings as the AWS CLI. If you've never used the AWS CLI before, the easiest way to get started is to run the ``configure`` command:: $ aws-shell aws> configure AWS Access Key ID [None]: your-access-key-id AWS Secret Access Key [None]: your-secret-access-key Default region name [None]: region-to-use (e.g us-west-2, us-west-1, etc). Default output format [None]: aws> For more information about configure settings, see the `AWS CLI Getting Started Guide`_. Basic Usage =========== The aws-shell accepts the same commands as the AWS CLI, except you don't need to provide the ``aws`` prefix. For example, here are a few commands you can try:: $ aws-shell aws> ec2 describe-regions { "Regions": [ { "Endpoint": "ec2.eu-west-1.amazonaws.com", "RegionName": "eu-west-1" }, ... aws> s3 ls 2015-12-07 15:03:34 bucket1 2015-12-07 15:03:34 bucket2 aws> dynamodb list-tables --output text TABLENAMES First TABLENAMES Second TABLENAMES Third Profiles -------- The aws-shell supports AWS CLI profiles. You have two options to use profiles. First, you can provide a profile when you start the aws-shell:: $ aws-shell --profile prod aws> When you do this all the server side completion as well as CLI commands you run will automatically use the ``prod`` profile. You can also change the current profile while you're in the aws-shell:: $ aws-shell aws> .profile demo Current shell profile changed to: demo You can also check what profile you've configured in the aws-shell using:: aws> .profile Current shell profile: demo After changing your profile using the ``.profile`` dot command, all server side completion as well as CLI commands will automatically use the new profile you've configured. Features ======== Auto Completion of Commands and Options --------------------------------------- The aws-shell provides auto completion of commands and options as you type. .. image:: https://cloud.githubusercontent.com/assets/368057/11824078/784a613e-a32c-11e5-8ac5-f1d1873cc643.png Shorthand Auto Completion ------------------------- The aws-shell can also fill in an example of the shorthand syntax used for various AWS CLI options: .. image:: https://cloud.githubusercontent.com/assets/368057/11823453/e95d85da-a328-11e5-8b8d-67566eccf9e3.png Server Side Auto Completion --------------------------- The aws-shell also leverages `boto3`_, the AWS SDK for Python, to auto complete server side resources such as Amazon EC2 instance Ids, Amazon Dynamodb table names, AWS IAM user names, Amazon S3 bucket names, etc. This feature is under active development. The list of supported resources continues to grow. .. image:: https://cloud.githubusercontent.com/assets/368057/11824022/3648b4fc-a32c-11e5-8e18-92f028eb1cee.png Fuzzy Searching --------------- Every auto completion value supports fuzzy searching. This enables you to specify the commands, options, and values you want to run with even less typing. You can try typing: * The first letter of each sub word: ``ec2 describe-reserved-instances-offerings`` -> ``ec2 drio`` * A little bit of each word: ``ec2 describe-instances`` -> ``ec2 descinst`` * Any part of the command: ``dynamodb table`` -> Offers all commands that contain the subsequence ``table``. .. image:: https://cloud.githubusercontent.com/assets/368057/11823996/18e69d16-a32c-11e5-80a2-defbaa6a8a80.png Inline Documentation -------------------- The aws-shell will automatically pull up documentation as you type commands. It will show inline documentation for CLI options. There is also a separate documentation panel that will show documentation for the current command or option you are typing. Pressing F9 will toggle focus to the documentation panel allowing you to navigate it using your selected keybindings. .. image:: https://cloud.githubusercontent.com/assets/368057/11823320/36ae9b04-a328-11e5-9661-81abfc0afe5a.png Fish-Style Auto Suggestions --------------------------- The aws-shell supports Fish-style auto-suggestions. Use the right arrow key to complete a suggestion. .. image:: https://cloud.githubusercontent.com/assets/368057/11822961/4bceff94-a326-11e5-87fa-c664e1e82be4.png Command History --------------- The aws-shell records the commands you run and writes them to ``~/.aws/shell/history``. You can use the up and down arrow keys to scroll through your history. .. image:: https://cloud.githubusercontent.com/assets/368057/11823211/b5851e9a-a327-11e5-877f-687dc1f90e27.png Toolbar Options --------------- The aws-shell has a bottom toolbar that provides several options: * ``F2`` toggles between fuzzy and substring matching * ``F3`` toggles between VI and Emacs key bindings * ``F4`` toggles between single and multi column auto completions * ``F5`` shows and hides the help documentation pane * ``F9`` toggles focus between the cli and documentation pane * ``F10`` or ``Ctrl-D`` exits the aws-shell As you toggle options in the toolbar, your preferences are persisted to the ``~/.aws/shell/awsshellrc`` file so that the next time you run the aws-shell, your preferences will be restored. .. image:: https://cloud.githubusercontent.com/assets/368057/11823907/8c3f1e60-a32b-11e5-9f99-fe504ea0a5dc.png Dot Commands ------------ The aws-shell provides additional commands specific to the aws-shell. The commands are available by adding the ``.`` prefix before a command. Exiting the Shell ~~~~~~~~~~~~~~~~~ You can run the ``.exit`` or ``.quit`` commands to exit the shell. Creating Shell Scripts with .edit ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ There are times when you may want to take a sequence of commands you've run in the aws-shell and combine them into a shell script. In addition to the command history that's persisted to the history file, the aws-shell also keeps track of all the commands you've run since you first started your aws-shell session. You can run the ``.edit`` command to open all these commands in an editor. The aws-shell will use the ``EDITOR`` environment variable before defaulting to ``notepad`` on Windows and ``vi`` on other platforms. :: aws> ec2 describe-instances aws> dynamodb list-tables aws> .edit Changing Profiles with .profile ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You can change the current AWS CLI profile used by the aws-shell by using the ``.profile`` dot command. If you run the ``.profile`` command with no arguments, the currently configured shell profile will be printed. :: aws> .profile demo Current shell profile changed to: demo aws> .profile Current shell profile: demo .cd ~~~ You can change the current working directory of the aws-shell by using the ``.cd`` command:: aws> !pwd /usr aws> .cd /tmp aws> !pwd /tmp Executing Shell Commands ------------------------ The aws-shell integrates with other commands in several ways. First, you can pipe AWS CLI commands to other processes as well as redirect output to a file:: aws> dynamodb list-tables --output text | head -n 1 TABLENAMES First aws> dynamodb list-tables --output text > /tmp/foo.txt Second, if you want to run a shell command rather than an AWS CLI command, you can add the ``!`` prefix to your command:: aws> !ls /tmp/ foo.txt bar.txt Developer Preview Status ======================== The aws-shell is currently in developer preview. We welcome feedback, feature requests, and bug reports. There may be backwards incompatible changes made in order to respond to customer feedback as we continue to iterate on the aws-shell. More Information ================ Below are miscellaneous links for more information: * `AWS CLI Reference Docs`_ * `AWS CLI User Guide`_ * `AWS CLI Blog`_ * `AWS CLI Github Repo`_ .. _pip: http://www.pip-installer.org/en/latest/ .. _AWS CLI Getting Started Guide: http://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html .. _boto3: https://github.com/boto/boto3 .. _AWS CLI Reference Docs: http://docs.aws.amazon.com/cli/latest/reference/ .. _AWS CLI User Guide: http://docs.aws.amazon.com/cli/latest/userguide/ .. _AWS CLI Blog: https://blogs.aws.amazon.com/cli/ .. _AWS CLI Github Repo: https://github.com/aws/aws-cli aws-shell-0.2.1/awsshell/000077500000000000000000000000001335127342700152425ustar00rootroot00000000000000aws-shell-0.2.1/awsshell/__init__.py000066400000000000000000000046321335127342700173600ustar00rootroot00000000000000from __future__ import unicode_literals, print_function import json import argparse import threading from awsshell import shellcomplete from awsshell import autocomplete from awsshell import app from awsshell import docs from awsshell import loaders from awsshell.index import completion from awsshell import utils __version__ = '0.2.1' def determine_doc_index_filename(): import awscli base = loaders.JSONIndexLoader.index_filename( awscli.__version__) return base + '.docs' def load_index(filename): load = loaders.JSONIndexLoader() return load.load_index(filename) def main(): parser = argparse.ArgumentParser() parser.add_argument('-p', '--profile', help='The profile name to use ' 'when starting the AWS Shell.') args = parser.parse_args() indexer = completion.CompletionIndex() try: index_str = indexer.load_index(utils.AWSCLI_VERSION) index_data = json.loads(index_str) except completion.IndexLoadError: print("First run, creating autocomplete index...") from awsshell.makeindex import write_index # TODO: Using internal method, but this will eventually # be moved into the CompletionIndex class anyways. index_file = indexer._filename_for_version(utils.AWSCLI_VERSION) write_index(index_file) index_str = indexer.load_index(utils.AWSCLI_VERSION) index_data = json.loads(index_str) doc_index_file = determine_doc_index_filename() from awsshell.makeindex import write_doc_index doc_data = docs.load_lazy_doc_index(doc_index_file) # There's room for improvement here. If the docs didn't finish # generating, we regen the whole doc index. Ideally we pick up # from where we left off. try: docs.load_doc_db(doc_index_file)['__complete__'] except KeyError: print("Creating doc index in the background. " "It will be a few minutes before all documentation is " "available.") t = threading.Thread(target=write_doc_index, args=(doc_index_file,)) t.daemon = True t.start() model_completer = autocomplete.AWSCLIModelCompleter(index_data) completer = shellcomplete.AWSShellCompleter(model_completer) shell = app.create_aws_shell(completer, model_completer, doc_data) if args.profile: shell.profile = args.profile shell.run() if __name__ == '__main__': main() aws-shell-0.2.1/awsshell/app.py000066400000000000000000000441601335127342700164010ustar00rootroot00000000000000"""AWS Shell application. Main entry point to the AWS Shell. """ from __future__ import unicode_literals import os import subprocess import logging import sys from prompt_toolkit.document import Document from prompt_toolkit.shortcuts import create_eventloop from prompt_toolkit.buffer import Buffer from prompt_toolkit.filters import Always from prompt_toolkit.interface import CommandLineInterface, Application from prompt_toolkit.interface import AbortAction, AcceptAction from prompt_toolkit.auto_suggest import AutoSuggestFromHistory from prompt_toolkit.history import InMemoryHistory, FileHistory from prompt_toolkit.enums import EditingMode from awsshell.ui import create_default_layout from awsshell.config import Config from awsshell.keys import KeyManager from awsshell.style import StyleFactory from awsshell.toolbar import Toolbar from awsshell.utils import build_config_file_path, temporary_file from awsshell import compat LOG = logging.getLogger(__name__) EXIT_REQUESTED = object() def create_aws_shell(completer, model_completer, docs): return AWSShell(completer, model_completer, docs) class InputInterrupt(Exception): """Stops the input of commands. Raising `InputInterrupt` is useful to force a cli rebuild, which is sometimes necessary in order for config changes to take effect. """ pass class ChangeDirHandler(object): def __init__(self, output=sys.stdout, err=sys.stderr, chdir=os.chdir): self._output = output self._err = err self._chdir = chdir def run(self, command, application): # command is a list of parsed commands if len(command) != 2: self._err.write("invalid syntax, must be: .cd dirname\n") return dirname = os.path.expandvars(os.path.expanduser(command[1])) try: self._chdir(dirname) except OSError as e: self._err.write("cd: %s\n" % e) class EditHandler(object): def __init__(self, popen_cls=None, env=None, err=sys.stderr): if popen_cls is None: popen_cls = subprocess.Popen self._popen_cls = popen_cls if env is None: env = os.environ self._env = env self._err = err def _get_editor_command(self): if 'EDITOR' in self._env: return self._env['EDITOR'] else: return compat.default_editor() def _generate_edit_history(self, application): history = list(application.history) commands = [h for h in history if not h.startswith(('.', '!'))] return '\n'.join(commands) def run(self, command, application): """Open application's history buffer in an editor. :type command: list :param command: The dot command as a list split on whitespace, e.g ``['.foo', 'arg1', 'arg2']`` :type application: AWSShell :param application: The application object. """ with temporary_file('w') as f: all_commands = self._generate_edit_history(application) f.write(all_commands) f.flush() editor = self._get_editor_command() try: p = self._popen_cls([editor, f.name]) p.communicate() except OSError: self._err.write("Unable to launch editor: %s\n" "You can configure which editor to use by " "exporting the EDITOR environment variable.\n" % editor) class ProfileHandler(object): USAGE = ( '.profile # Print the current profile\n' '.profile # Change the current profile\n' ) def __init__(self, output=sys.stdout, err=sys.stderr): self._output = output self._err = err def run(self, command, application): """Get or set the profile. If .profile is called with no args, the current profile is displayed. If the .profile command is called with a single arg, then the current profile for the application will be set to the new value. """ if len(command) == 1: profile = application.profile if profile is None: self._output.write( "Current shell profile: no profile configured\n" "You can change profiles using: .profile profile-name\n") else: self._output.write("Current shell profile: %s\n" % profile) elif len(command) == 2: new_profile_name = command[1] application.profile = new_profile_name self._output.write("Current shell profile changed to: %s\n" % new_profile_name) else: self._err.write("Usage:\n%s\n" % self.USAGE) class ExitHandler(object): def run(self, command, application): return EXIT_REQUESTED class DotCommandHandler(object): HANDLER_CLASSES = { 'edit': EditHandler, 'profile': ProfileHandler, 'cd': ChangeDirHandler, 'exit': ExitHandler, 'quit': ExitHandler, } def __init__(self, output=sys.stdout, err=sys.stderr): self._output = output self._err = err def handle_cmd(self, command, application): """Handle running a given dot command from a user. :type command: str :param command: The full dot command string, e.g. ``.edit``, of ``.profile prod``. :type application: AWSShell :param application: The application object. """ parts = command.split() cmd_name = parts[0][1:] if cmd_name not in self.HANDLER_CLASSES: self._unknown_cmd(parts, application) else: # Note we expect the class to support no-arg # instantiation. return self.HANDLER_CLASSES[cmd_name]().run(parts, application) def _unknown_cmd(self, cmd_parts, application): self._err.write("Unknown dot command: %s\n" % cmd_parts[0]) class AWSShell(object): """Encapsulate the ui, completer, command history, docs, and config. Runs the input event loop and delegates the command execution to either the `awscli` or the underlying shell. :type refresh_cli: bool :param refresh_cli: Flag to refresh the cli. :type config_obj: :class:`configobj.ConfigObj` :param config_obj: Contains the config information for reading and writing. :type config_section: :class:`configobj.Section` :param config_section: Convenience attribute to access the main section of the config. :type model_completer: :class:`AWSCLIModelCompleter` :param model_completer: Matches input with completions. `AWSShell` sets and gets the attribute `AWSCLIModelCompleter.match_fuzzy`. :type enable_vi_bindings: bool :param enable_vi_bindings: If True, enables Vi key bindings. Else, Emacs key bindings are enabled. :type show_completion_columns: bool param show_completion_columns: If True, completions are shown in multiple columns. Else, completions are shown in a single scrollable column. :type show_help: bool :param show_help: If True, shows the help pane. Else, hides the help pane. :type theme: str :param theme: The pygments theme. """ def __init__(self, completer, model_completer, docs, input=None, output=None, popen_cls=None): self.completer = completer self.model_completer = model_completer self.history = InMemoryHistory() self.file_history = FileHistory(build_config_file_path('history')) self._cli = None self._docs = docs self.current_docs = u'' self.refresh_cli = False self.key_manager = None self._dot_cmd = DotCommandHandler() self._env = os.environ.copy() self._profile = None self._input = input self._output = output if popen_cls is None: popen_cls = subprocess.Popen self._popen_cls = popen_cls # These attrs come from the config file. self.config_obj = None self.config_section = None self.enable_vi_bindings = None self.show_completion_columns = None self.show_help = None self.theme = None self.load_config() def load_config(self): """Load the config from the config file or template.""" config = Config() self.config_obj = config.load('awsshellrc') self.config_section = self.config_obj['aws-shell'] self.model_completer.match_fuzzy = self.config_section.as_bool( 'match_fuzzy') self.enable_vi_bindings = self.config_section.as_bool( 'enable_vi_bindings') self.show_completion_columns = self.config_section.as_bool( 'show_completion_columns') self.show_help = self.config_section.as_bool('show_help') self.theme = self.config_section['theme'] def save_config(self): """Save the config to the config file.""" self.config_section['match_fuzzy'] = self.model_completer.match_fuzzy self.config_section['enable_vi_bindings'] = self.enable_vi_bindings self.config_section['show_completion_columns'] = \ self.show_completion_columns self.config_section['show_help'] = self.show_help self.config_section['theme'] = self.theme self.config_obj.write() @property def cli(self): if self._cli is None or self.refresh_cli: self._cli = self.create_cli_interface(self.show_completion_columns) self.refresh_cli = False return self._cli def run(self): while True: try: document = self.cli.run(reset_current_buffer=True) text = document.text except InputInterrupt: pass except (KeyboardInterrupt, EOFError): self.save_config() break else: if text.startswith('.'): # These are special commands (dot commands) that are # interpreted by the aws-shell directly and typically used # to modify some type of behavior in the aws-shell. result = self._dot_cmd.handle_cmd(text, application=self) if result is EXIT_REQUESTED: break else: if text.startswith('!'): # Then run the rest as a normally shell command. full_cmd = text[1:] else: full_cmd = 'aws ' + text self.history.append(full_cmd) self.current_docs = u'' self.cli.buffers['clidocs'].reset( initial_document=Document(self.current_docs, cursor_position=0)) self.cli.request_redraw() p = self._popen_cls(full_cmd, shell=True, env=self._env) p.communicate() def stop_input_and_refresh_cli(self): """Stop input by raising an `InputInterrupt`, forces a cli refresh. The cli refresh is necessary because changing options such as key bindings, single vs multi column menu completions, and the help pane all require a rebuild. :raises: :class:`InputInterrupt `. """ self.refresh_cli = True self.cli.request_redraw() raise InputInterrupt def create_layout(self, display_completions_in_columns, toolbar): from awsshell.lexer import ShellLexer lexer = ShellLexer if self.config_section['theme'] == 'none': lexer = None return create_default_layout( self, u'aws> ', lexer=lexer, reserve_space_for_menu=True, display_completions_in_columns=display_completions_in_columns, get_bottom_toolbar_tokens=toolbar.handler) def create_buffer(self, completer, history): return Buffer( history=history, auto_suggest=AutoSuggestFromHistory(), enable_history_search=True, completer=completer, complete_while_typing=Always(), accept_action=AcceptAction.RETURN_DOCUMENT) def create_key_manager(self): """Create the :class:`KeyManager`. The inputs to KeyManager are expected to be callable, so we can't use the standard @property and @attrib.setter for these attributes. Lambdas cannot contain assignments so we're forced to define setters. :rtype: :class:`KeyManager` :return: A KeyManager with callables to set the toolbar options. Also includes the method stop_input_and_refresh_cli to ensure certain options take effect within the current session. """ def set_match_fuzzy(match_fuzzy): """Setter for fuzzy matching mode. :type match_fuzzy: bool :param match_fuzzy: The match fuzzy flag. """ self.model_completer.match_fuzzy = match_fuzzy def set_enable_vi_bindings(enable_vi_bindings): """Setter for vi mode keybindings. If vi mode is off, emacs mode is enabled by default by `prompt_toolkit`. :type enable_vi_bindings: bool :param enable_vi_bindings: The enable Vi bindings flag. """ self.enable_vi_bindings = enable_vi_bindings def set_show_completion_columns(show_completion_columns): """Setter for showing the completions in columns flag. :type show_completion_columns: bool :param show_completion_columns: The show completions in multiple columns flag. """ self.show_completion_columns = show_completion_columns def set_show_help(show_help): """Setter for showing the help container flag. :type show_help: bool :param show_help: The show help flag. """ self.show_help = show_help return KeyManager( lambda: self.model_completer.match_fuzzy, set_match_fuzzy, lambda: self.enable_vi_bindings, set_enable_vi_bindings, lambda: self.show_completion_columns, set_show_completion_columns, lambda: self.show_help, set_show_help, self.stop_input_and_refresh_cli) def create_application(self, completer, history, display_completions_in_columns): self.key_manager = self.create_key_manager() toolbar = Toolbar( lambda: self.model_completer.match_fuzzy, lambda: self.enable_vi_bindings, lambda: self.show_completion_columns, lambda: self.show_help) style_factory = StyleFactory(self.theme) buffers = { 'clidocs': Buffer(read_only=True) } if self.enable_vi_bindings: editing_mode = EditingMode.VI else: editing_mode = EditingMode.EMACS return Application( editing_mode=editing_mode, layout=self.create_layout(display_completions_in_columns, toolbar), mouse_support=False, style=style_factory.style, buffers=buffers, buffer=self.create_buffer(completer, history), on_abort=AbortAction.RETRY, on_exit=AbortAction.RAISE_EXCEPTION, on_input_timeout=self.on_input_timeout, key_bindings_registry=self.key_manager.manager.registry, ) def on_input_timeout(self, cli): if not self.show_help: return document = cli.current_buffer.document text = document.text LOG.debug("document.text = %s", text) LOG.debug("current_command = %s", self.completer.current_command) if text.strip(): command = self.completer.current_command key_name = '.'.join(command.split()).encode('utf-8') last_option = self.completer.last_option if last_option: self.current_docs = self._docs.extract_param( key_name, last_option) else: self.current_docs = self._docs.extract_description(key_name) else: self.current_docs = u'' position = cli.buffers['clidocs'].document.cursor_position # if the docs to be displayed have changed, reset position to 0 if cli.buffers['clidocs'].text != self.current_docs: position = 0 cli.buffers['clidocs'].reset( initial_document=Document( self.current_docs, cursor_position=position ) ) cli.request_redraw() def create_cli_interface(self, display_completions_in_columns): # A CommandLineInterface from prompt_toolkit # accepts two things: an application and an # event loop. loop = create_eventloop() app = self.create_application(self.completer, self.file_history, display_completions_in_columns) cli = CommandLineInterface(application=app, eventloop=loop, input=self._input, output=self._output) return cli @property def profile(self): return self._profile @profile.setter def profile(self, new_profile_name): # There's only two things that need to know about new profile # changes. # # First, the actual command runner. If we want # to use a different profile, it should ensure that the CLI # commands that get run use the new profile (via the # AWS_DEFAULT_PROFILE env var). # # Second, we also need to let the server side autocompleter know. # # Given this is easy to manage by hand, I don't think # it's worth adding an event system or observers just yet. # If this gets hard to manage, the complexity of those systems # would be worth it. self._env['AWS_DEFAULT_PROFILE'] = new_profile_name self.completer.change_profile(new_profile_name) self._profile = new_profile_name aws-shell-0.2.1/awsshell/autocomplete.py000066400000000000000000000142241335127342700203200ustar00rootroot00000000000000from __future__ import print_function from awsshell.fuzzy import fuzzy_search from awsshell.substring import substring_search class AWSCLIModelCompleter(object): """Autocompletion based on the JSON models for AWS services. This class consumes indexed data based on the JSON models from AWS service (which we pull through botocore's data loaders). """ def __init__(self, index_data, match_fuzzy=True): self._index = index_data self._root_name = 'aws' self._global_options = index_data[self._root_name]['arguments'] # These values mutate as autocompletions occur. # They track state to improve the autocompletion speed. self._current_name = 'aws' self._current = index_data[self._root_name] self._last_position = 0 self._current_line = '' self.last_option = '' # This will get populated as a command is completed. self.cmd_path = [self._current_name] self.match_fuzzy = match_fuzzy @property def global_arg_metadata(self): return self._index[self._root_name]['argument_metadata'] @property def arg_metadata(self): # Returns the required arguments for the current level. return self._current.get('argument_metadata', {}) def reset(self): # Resets all the state. Called after a user runs # a command. self._current_name = self._root_name self._current = self._index[self._root_name] self._last_position = 0 self.last_option = '' self.cmd_path = [self._current_name] def autocomplete(self, line): """Given a line, return a list of suggestions.""" current_length = len(line) self._current_line = line if current_length == 1 and self._last_position > 1: # Reset state. This is likely from a user completing # a previous command. self.reset() elif current_length < self._last_position: # The user has hit backspace. We'll need to check # the current words. return self._handle_backspace() elif not line: return [] elif current_length != self._last_position + 1: return self._complete_from_full_parse() # This position is important. We only update the _last_position # after we've checked the special cases above where that value # matters. self._last_position = len(line) if line and not line.strip(): # Special case, the user hits a space on a new line so # we autocomplete all the top level commands. return self._current['commands'] last_word = line.split()[-1] if last_word in self.arg_metadata or last_word in self._global_options: # The last thing we completed was an argument, record # this as self.last_arg self.last_option = last_word if line[-1] == ' ': # At this point the user has autocompleted a command # or an argument and has hit space. If they've # just completed a command, we need to change the # current context and traverse into the subcommand. # "ec2 " # ^--here, need to traverse into "ec2" # # Otherwise: # "ec2 --no-validate-ssl " # ^-- here, stay on "ec2" context. if not last_word.startswith('-'): next_command = self._current['children'].get(last_word) if next_command is not None: self._current = next_command self._current_name = last_word self.cmd_path.append(self._current_name) elif last_word in self.arg_metadata and \ self.arg_metadata[last_word]['example']: # Then this is an arg with a shorthand example so we'll # suggest that example. return [self.arg_metadata[last_word]['example']] # Even if we don't change context, we still want to # autocomplete all the commands for the current context # in either of the above two cases. return self._current['commands'][:] elif last_word.startswith('-'): # TODO: cache this for the duration of the current context. # We don't need to recompute this until the args are # different. all_args = self._get_all_args() if self.match_fuzzy: return fuzzy_search(last_word, all_args) else: return substring_search(last_word, all_args) if self.match_fuzzy: return fuzzy_search(last_word, self._current['commands']) else: return substring_search(last_word, self._current['commands']) def _get_all_args(self): if self._current['arguments'] != self._global_options: all_args = self._current['arguments'] + self._global_options else: all_args = self._current['arguments'] return all_args def _handle_backspace(self): return self._complete_from_full_parse() def _complete_from_full_parse(self): # We try to avoid calling this, but this is necessary # sometimes. In this scenario, we're resetting everything # and starting from the very beginning and reparsing # everything. # This is a naive implementation for now. This # can be optimized. self.reset() line = self._current_line for i in range(1, len(self._current_line)): self.autocomplete(line[:i]) return self.autocomplete(line) def _autocomplete_options(self, last_word): global_args = [] # Autocomplete argument names. current_arg_completions = [ cmd for cmd in self._current['arguments'] if cmd.startswith(last_word)] if self._current_name != self._root_name: # Also autocomplete global arguments. global_args = [ cmd for cmd in self._global_options if cmd.startswith(last_word)] return current_arg_completions + global_args aws-shell-0.2.1/awsshell/awsshellrc000066400000000000000000000007771335127342700173470ustar00rootroot00000000000000[aws-shell] # fuzzy or substring match. match_fuzzy = True # vi or emacs key bindings. enable_vi_bindings = False # multi or single column completion menu. show_completion_columns = False # show or hide the help pane. show_help = True # visual theme. possible values: manni, igor, xcode, vim, # autumn,vs, rrt, native, perldoc, borland, tango, emacs, # friendly, monokai, paraiso-dark, colorful, murphy, bw, # pastie, paraiso-light, trac, default, fruity. # to disable themes, set theme = none theme = vim aws-shell-0.2.1/awsshell/compat.py000066400000000000000000000007721335127342700171050ustar00rootroot00000000000000from __future__ import print_function import sys import platform PY3 = sys.version_info[0] == 3 ON_WINDOWS = platform.system() == 'Windows' if PY3: from html.parser import HTMLParser text_type = str from io import StringIO import dbm else: from HTMLParser import HTMLParser text_type = unicode from cStringIO import StringIO import anydbm as dbm if ON_WINDOWS: def default_editor(): return 'notepad.exe' else: def default_editor(): return 'vi' aws-shell-0.2.1/awsshell/config.py000066400000000000000000000067171335127342700170740ustar00rootroot00000000000000# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"). You # may not use this file except in compliance with the License. A copy of # the License is located at # # http://www.apache.org/licenses/LICENSE-2.0 # # or in the "license" file accompanying this file. This file is # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. import os import shutil from configobj import ConfigObj from awsshell.utils import build_config_file_path class Config(object): """Reads and writes the config template and user config file.""" def load(self, config_template, config_file=None): """Read the config file if it exists, else read the default config. Creates the user config file if it doesn't exist using the template. :type config_template: str :param config_template: The config template file name. :type config_file: str :param config_file: (Optional) The config file name. If None, the config_file name will be set to the config_template. :rtype: :class:`configobj.ConfigObj` :return: The config information for reading and writing. """ if config_file is None: config_file = config_template config_path = build_config_file_path(config_file) template_path = os.path.join(os.path.dirname(__file__), config_template) self._copy_template_to_config(template_path, config_path) return self._load_template_or_config(template_path, config_path) def _load_template_or_config(self, template_path, config_path): """Load the config file if it exists, else read the default config. :type template_path: str :param template_path: The template config file path. :type config_path: str :param config_path: The user's config file path. :rtype: :class:`configobj.ConfigObj` :return: The config information for reading and writing. """ expanded_config_path = os.path.expanduser(config_path) cfg = ConfigObj() cfg.filename = expanded_config_path cfg.merge(ConfigObj(template_path, interpolation=False)) cfg.merge(ConfigObj(expanded_config_path, interpolation=False)) return cfg def _copy_template_to_config(self, template_path, config_path, overwrite=False): """Write the default config from a template. :type template_path: str :param template_path: The template config file path. :type config_path: str :param config_path: The user's config file path. :type overwrite: bool :param overwrite: (Optional) Determines whether to overwrite the existing config file, if it exists. :raises: :class:`OSError ` """ config_path = os.path.expanduser(config_path) if not overwrite and os.path.isfile(config_path): return else: try: config_path_dir_name = os.path.dirname(config_path) os.makedirs(config_path_dir_name) except OSError: if not os.path.isdir(config_path_dir_name): raise shutil.copyfile(template_path, config_path) aws-shell-0.2.1/awsshell/data/000077500000000000000000000000001335127342700161535ustar00rootroot00000000000000aws-shell-0.2.1/awsshell/data/cloudformation/000077500000000000000000000000001335127342700212005ustar00rootroot00000000000000aws-shell-0.2.1/awsshell/data/cloudformation/2010-05-15/000077500000000000000000000000001335127342700222275ustar00rootroot00000000000000aws-shell-0.2.1/awsshell/data/cloudformation/2010-05-15/completions-1.json000066400000000000000000000010751335127342700256170ustar00rootroot00000000000000{ "operations": { "UpdateStack": { "StackName": { "resourceName": "Stack", "resourceIdentifier": "Name" } }, "DeleteStack": { "StackName": { "resourceName": "Stack", "resourceIdentifier": "Name" } }, "CancelUpdateStack": { "StackName": { "resourceName": "Stack", "resourceIdentifier": "Name" } } }, "resources": { "Stack": { "operation": "DescribeStacks", "resourceIdentifier": { "Name": "Stacks[].StackName" } } } }aws-shell-0.2.1/awsshell/data/dynamodb/000077500000000000000000000000001335127342700177505ustar00rootroot00000000000000aws-shell-0.2.1/awsshell/data/dynamodb/2012-08-10/000077500000000000000000000000001335127342700207775ustar00rootroot00000000000000aws-shell-0.2.1/awsshell/data/dynamodb/2012-08-10/completions-1.json000066400000000000000000000022331335127342700243640ustar00rootroot00000000000000{ "operations": { "UpdateTable": { "TableName": { "resourceName": "Table", "resourceIdentifier": "Name" } }, "DeleteTable": { "TableName": { "resourceName": "Table", "resourceIdentifier": "Name" } }, "Scan": { "TableName": { "resourceName": "Table", "resourceIdentifier": "Name" } }, "GetItem": { "TableName": { "resourceName": "Table", "resourceIdentifier": "Name" } }, "Query": { "TableName": { "resourceName": "Table", "resourceIdentifier": "Name" } }, "PutItem": { "TableName": { "resourceName": "Table", "resourceIdentifier": "Name" } }, "UpdateItem": { "TableName": { "resourceName": "Table", "resourceIdentifier": "Name" } }, "DeleteItem": { "TableName": { "resourceName": "Table", "resourceIdentifier": "Name" } } }, "resources": { "Table": { "operation": "ListTables", "resourceIdentifier": { "Name": "TableNames[]" } } } }aws-shell-0.2.1/awsshell/data/ec2/000077500000000000000000000000001335127342700166245ustar00rootroot00000000000000aws-shell-0.2.1/awsshell/data/ec2/2015-04-15/000077500000000000000000000000001335127342700176575ustar00rootroot00000000000000aws-shell-0.2.1/awsshell/data/ec2/2015-04-15/completions-1.json000066400000000000000000000315301335127342700232460ustar00rootroot00000000000000{ "operations": { "ResetNetworkInterfaceAttribute": { "NetworkInterfaceId": { "resourceName": "NetworkInterface", "resourceIdentifier": "Id" } }, "CreateSubnet": { "VpcId": { "resourceName": "Vpc", "resourceIdentifier": "Id" } }, "CopySnapshot": { "SourceSnapshotId": { "resourceName": "Snapshot", "resourceIdentifier": "Id" } }, "AttachNetworkInterface": { "NetworkInterfaceId": { "resourceName": "NetworkInterface", "resourceIdentifier": "Id" } }, "UnassignPrivateIpAddresses": { "NetworkInterfaceId": { "resourceName": "NetworkInterface", "resourceIdentifier": "Id" } }, "DescribeImageAttribute": { "ImageId": { "resourceName": "Image", "resourceIdentifier": "Id" } }, "DescribeInstances": { "InstanceIds": { "resourceName": "Instance", "resourceIdentifier": "Id" } }, "ResetSnapshotAttribute": { "SnapshotId": { "resourceName": "Snapshot", "resourceIdentifier": "Id" } }, "StartInstances": { "InstanceIds": { "resourceName": "Instance", "resourceIdentifier": "Id" } }, "DeleteDhcpOptions": { "DhcpOptionsId": { "resourceName": "DhcpOptions", "resourceIdentifier": "Id" } }, "RejectVpcPeeringConnection": { "VpcPeeringConnectionId": { "resourceName": "VpcPeeringConnection", "resourceIdentifier": "Id" } }, "DeleteInternetGateway": { "InternetGatewayId": { "resourceName": "InternetGateway", "resourceIdentifier": "Id" } }, "ResetImageAttribute": { "ImageId": { "resourceName": "Image", "resourceIdentifier": "Id" } }, "ModifyNetworkInterfaceAttribute": { "NetworkInterfaceId": { "resourceName": "NetworkInterface", "resourceIdentifier": "Id" } }, "MonitorInstances": { "InstanceIds": { "resourceName": "Instance", "resourceIdentifier": "Id" } }, "DeleteNetworkAcl": { "NetworkAclId": { "resourceName": "NetworkAcl", "resourceIdentifier": "Id" } }, "DisableVpcClassicLink": { "VpcId": { "resourceName": "Vpc", "resourceIdentifier": "Id" } }, "ModifyInstanceAttribute": { "InstanceId": { "resourceName": "Instance", "resourceIdentifier": "Id" } }, "EnableVpcClassicLink": { "VpcId": { "resourceName": "Vpc", "resourceIdentifier": "Id" } }, "AssociateDhcpOptions": { "VpcId": { "resourceName": "Vpc", "resourceIdentifier": "Id" } }, "AssociateRouteTable": { "RouteTableId": { "resourceName": "RouteTable", "resourceIdentifier": "Id" } }, "DescribeNetworkInterfaceAttribute": { "NetworkInterfaceId": { "resourceName": "NetworkInterface", "resourceIdentifier": "Id" } }, "DeleteVpc": { "VpcId": { "resourceName": "Vpc", "resourceIdentifier": "Id" } }, "GetPasswordData": { "InstanceId": { "resourceName": "Instance", "resourceIdentifier": "Id" } }, "UnmonitorInstances": { "InstanceIds": { "resourceName": "Instance", "resourceIdentifier": "Id" } }, "AcceptVpcPeeringConnection": { "VpcPeeringConnectionId": { "resourceName": "VpcPeeringConnection", "resourceIdentifier": "Id" } }, "DeleteSnapshot": { "SnapshotId": { "resourceName": "Snapshot", "resourceIdentifier": "Id" } }, "CreateRoute": { "RouteTableId": { "resourceName": "RouteTable", "resourceIdentifier": "Id" } }, "CreateNetworkAclEntry": { "NetworkAclId": { "resourceName": "NetworkAcl", "resourceIdentifier": "Id" } }, "RevokeSecurityGroupEgress": { "GroupId": { "resourceName": "SecurityGroup", "resourceIdentifier": "Id" } }, "DeregisterImage": { "ImageId": { "resourceName": "Image", "resourceIdentifier": "Id" } }, "AssignPrivateIpAddresses": { "NetworkInterfaceId": { "resourceName": "NetworkInterface", "resourceIdentifier": "Id" } }, "ReportInstanceStatus": { "Instances": { "resourceName": "Instance", "resourceIdentifier": "Id" } }, "DescribeSnapshotAttribute": { "SnapshotId": { "resourceName": "Snapshot", "resourceIdentifier": "Id" } }, "DeleteSubnet": { "SubnetId": { "resourceName": "Subnet", "resourceIdentifier": "Id" } }, "DeleteVolume": { "VolumeId": { "resourceName": "Volume", "resourceIdentifier": "Id" } }, "CreateNetworkAcl": { "VpcId": { "resourceName": "Vpc", "resourceIdentifier": "Id" } }, "AttachClassicLinkVpc": { "VpcId": { "resourceName": "Vpc", "resourceIdentifier": "Id" } }, "DeleteSecurityGroup": { "GroupId": { "resourceName": "SecurityGroup", "resourceIdentifier": "Id" } }, "CreateImage": { "InstanceId": { "resourceName": "Instance", "resourceIdentifier": "Id" } }, "DeleteVpcPeeringConnection": { "VpcPeeringConnectionId": { "resourceName": "VpcPeeringConnection", "resourceIdentifier": "Id" } }, "ModifyVolumeAttribute": { "VolumeId": { "resourceName": "Volume", "resourceIdentifier": "Id" } }, "ModifyImageAttribute": { "ImageId": { "resourceName": "Image", "resourceIdentifier": "Id" } }, "DeleteNetworkAclEntry": { "NetworkAclId": { "resourceName": "NetworkAcl", "resourceIdentifier": "Id" } }, "RunInstances": { "SubnetId": { "resourceName": "Subnet", "resourceIdentifier": "Id" } }, "GetConsoleOutput": { "InstanceId": { "resourceName": "Instance", "resourceIdentifier": "Id" } }, "StopInstances": { "InstanceIds": { "resourceName": "Instance", "resourceIdentifier": "Id" } }, "ModifySnapshotAttribute": { "SnapshotId": { "resourceName": "Snapshot", "resourceIdentifier": "Id" } }, "DescribeVpcAttribute": { "VpcId": { "resourceName": "Vpc", "resourceIdentifier": "Id" } }, "AttachInternetGateway": { "VpcId": { "resourceName": "Vpc", "resourceIdentifier": "Id" } }, "DescribeVolumeAttribute": { "VolumeId": { "resourceName": "Volume", "resourceIdentifier": "Id" } }, "DetachVolume": { "VolumeId": { "resourceName": "Volume", "resourceIdentifier": "Id" } }, "ReplaceNetworkAclEntry": { "NetworkAclId": { "resourceName": "NetworkAcl", "resourceIdentifier": "Id" } }, "AttachVolume": { "VolumeId": { "resourceName": "Volume", "resourceIdentifier": "Id" } }, "DetachNetworkInterface": {}, "TerminateInstances": { "InstanceIds": { "resourceName": "Instance", "resourceIdentifier": "Id" } }, "DetachInternetGateway": { "VpcId": { "resourceName": "Vpc", "resourceIdentifier": "Id" } }, "ResetInstanceAttribute": { "InstanceId": { "resourceName": "Instance", "resourceIdentifier": "Id" } }, "DeletePlacementGroup": { "GroupName": { "resourceName": "PlacementGroup", "resourceIdentifier": "Name" } }, "DeleteRouteTable": { "RouteTableId": { "resourceName": "RouteTable", "resourceIdentifier": "Id" } }, "CreateTags": { "Resources": { "resourceName": "Vpc", "resourceIdentifier": "Id" } }, "EnableVolumeIO": { "VolumeId": { "resourceName": "Volume", "resourceIdentifier": "Id" } }, "CreateRouteTable": { "VpcId": { "resourceName": "Vpc", "resourceIdentifier": "Id" } }, "CreateNetworkInterface": { "SubnetId": { "resourceName": "Subnet", "resourceIdentifier": "Id" } }, "ReplaceNetworkAclAssociation": { "NetworkAclId": { "resourceName": "NetworkAcl", "resourceIdentifier": "Id" } }, "ModifyVpcAttribute": { "VpcId": { "resourceName": "Vpc", "resourceIdentifier": "Id" } }, "DeleteNetworkInterface": { "NetworkInterfaceId": { "resourceName": "NetworkInterface", "resourceIdentifier": "Id" } }, "RebootInstances": { "InstanceIds": { "resourceName": "Instance", "resourceIdentifier": "Id" } }, "DescribeVolumeStatus": { "VolumeIds": { "resourceName": "Volume", "resourceIdentifier": "Id" } }, "DetachClassicLinkVpc": { "VpcId": { "resourceName": "Vpc", "resourceIdentifier": "Id" } }, "AuthorizeSecurityGroupEgress": { "GroupId": { "resourceName": "SecurityGroup", "resourceIdentifier": "Id" } }, "CreateVpcPeeringConnection": { "VpcId": { "resourceName": "Vpc", "resourceIdentifier": "Id" } }, "RevokeSecurityGroupIngress": { "GroupId": { "resourceName": "SecurityGroup", "resourceIdentifier": "Id" } }, "CreateSecurityGroup": { "VpcId": { "resourceName": "Vpc", "resourceIdentifier": "Id" } }, "DeleteKeyPair": { "KeyName": { "resourceName": "KeyPair", "resourceIdentifier": "Name" } }, "AuthorizeSecurityGroupIngress": { "GroupId": { "resourceName": "SecurityGroup", "resourceIdentifier": "Id" } }, "CreateSnapshot": { "VolumeId": { "resourceName": "Volume", "resourceIdentifier": "Id" } }, "DescribeInstanceAttribute": { "InstanceId": { "resourceName": "Instance", "resourceIdentifier": "Id" } } }, "resources": { "Subnet": { "operation": "DescribeSubnets", "resourceIdentifier": { "Id": "Subnets[].SubnetId" } }, "VpcPeeringConnection": { "operation": "DescribeVpcPeeringConnections", "resourceIdentifier": { "Id": "VpcPeeringConnections[].VpcPeeringConnectionId" } }, "NetworkAcl": { "operation": "DescribeNetworkAcls", "resourceIdentifier": { "Id": "NetworkAcls[].NetworkAclId" } }, "RouteTable": { "operation": "DescribeRouteTables", "resourceIdentifier": { "Id": "RouteTables[].RouteTableId" } }, "Snapshot": { "operation": "DescribeSnapshots", "resourceIdentifier": { "Id": "Snapshots[].SnapshotId" } }, "DhcpOptions": { "operation": "DescribeDhcpOptions", "resourceIdentifier": { "Id": "DhcpOptions[].DhcpOptionsId" } }, "SecurityGroup": { "operation": "DescribeSecurityGroups", "resourceIdentifier": { "Id": "SecurityGroups[].GroupId" } }, "NetworkInterface": { "operation": "DescribeNetworkInterfaces", "resourceIdentifier": { "Id": "NetworkInterfaces[].NetworkInterfaceId" } }, "Volume": { "operation": "DescribeVolumes", "resourceIdentifier": { "Id": "Volumes[].VolumeId" } }, "Instance": { "operation": "DescribeInstances", "resourceIdentifier": { "Id": "Reservations[].Instances[].InstanceId" } }, "KeyPair": { "operation": "DescribeKeyPairs", "resourceIdentifier": { "Name": "KeyPairs[].KeyName" } }, "InternetGateway": { "operation": "DescribeInternetGateways", "resourceIdentifier": { "Id": "InternetGateways[].InternetGatewayId" } }, "Vpc": { "operation": "DescribeVpcs", "resourceIdentifier": { "Id": "Vpcs[].VpcId" } }, "PlacementGroup": { "operation": "DescribePlacementGroups", "resourceIdentifier": { "Name": "PlacementGroups[].GroupName" } }, "Image": { "operation": "DescribeImages", "resourceIdentifier": { "Id": "Images[].ImageId" } } } } aws-shell-0.2.1/awsshell/data/elb/000077500000000000000000000000001335127342700167155ustar00rootroot00000000000000aws-shell-0.2.1/awsshell/data/elb/2012-06-01/000077500000000000000000000000001335127342700177425ustar00rootroot00000000000000aws-shell-0.2.1/awsshell/data/elb/2012-06-01/completions-1.json000066400000000000000000000100641335127342700233300ustar00rootroot00000000000000{ "operations": { "AddTags": { "LoadBalancerNames": { "resourceName": "LoadBalancer", "resourceIdentifier": "Name" } }, "ApplySecurityGroupsToLoadBalancer": { "LoadBalancerName": { "resourceName": "LoadBalancer", "resourceIdentifier": "Name" } }, "AttachLoadBalancerToSubnets": { "LoadBalancerName": { "resourceName": "LoadBalancer", "resourceIdentifier": "Name" } }, "ConfigureHealthCheck": { "LoadBalancerName": { "resourceName": "LoadBalancer", "resourceIdentifier": "Name" } }, "CreateAppCookieStickinessPolicy": { "LoadBalancerName": { "resourceName": "LoadBalancer", "resourceIdentifier": "Name" } }, "CreateLBCookieStickinessPolicy": { "LoadBalancerName": { "resourceName": "LoadBalancer", "resourceIdentifier": "Name" } }, "CreateLoadBalancerPolicy": { "LoadBalancerName": { "resourceName": "LoadBalancer", "resourceIdentifier": "Name" } }, "DetachLoadBalancerFromSubnets": { "LoadBalancerName": { "resourceName": "LoadBalancer", "resourceIdentifier": "Name" } }, "DescribeInstanceHealth": { "LoadBalancerName": { "resourceName": "LoadBalancer", "resourceIdentifier": "Name" } }, "DeleteLoadBalancer": { "LoadBalancerName": { "resourceName": "LoadBalancer", "resourceIdentifier": "Name" } }, "DeleteLoadBalancerListeners": { "LoadBalancerName": { "resourceName": "LoadBalancer", "resourceIdentifier": "Name" } }, "DeleteLoadBalancerPolicy": { "LoadBalancerName": { "resourceName": "LoadBalancer", "resourceIdentifier": "Name" } }, "DescribeTags": { "LoadBalancerNames": { "resourceName": "LoadBalancer", "resourceIdentifier": "Name" } }, "DeregisterInstancesFromLoadBalancer": { "LoadBalancerName": { "resourceName": "LoadBalancer", "resourceIdentifier": "Name" } }, "DescribeLoadBalancerAttributes": { "LoadBalancerName": { "resourceName": "LoadBalancer", "resourceIdentifier": "Name" } }, "DescribeLoadBalancerPolicies": { "LoadBalancerName": { "resourceName": "LoadBalancer", "resourceIdentifier": "Name" } }, "DescribeLoadBalancers": { "LoadBalancerNames": { "resourceName": "LoadBalancer", "resourceIdentifier": "Name" } }, "DisableAvailabilityZonesForLoadBalancer": { "LoadBalancerName": { "resourceName": "LoadBalancer", "resourceIdentifier": "Name" } }, "EnableAvailabilityZonesForLoadBalancer": { "LoadBalancerName": { "resourceName": "LoadBalancer", "resourceIdentifier": "Name" } }, "ModifyLoadBalancerAttributes": { "LoadBalancerName": { "resourceName": "LoadBalancer", "resourceIdentifier": "Name" } }, "RegisterInstancesWithLoadBalancer": { "LoadBalancerName": { "resourceName": "LoadBalancer", "resourceIdentifier": "Name" } }, "RemoveTags": { "LoadBalancerNames": { "resourceName": "LoadBalancer", "resourceIdentifier": "Name" } }, "SetLoadBalancerListenerSSLCertificate": { "LoadBalancerName": { "resourceName": "LoadBalancer", "resourceIdentifier": "Name" } }, "SetLoadBalancerPoliciesForBackendServer": { "LoadBalancerName": { "resourceName": "LoadBalancer", "resourceIdentifier": "Name" } }, "SetLoadBalancerPoliciesOfListener": { "LoadBalancerName": { "resourceName": "LoadBalancer", "resourceIdentifier": "Name" } } }, "resources": { "LoadBalancer": { "operation": "DescribeLoadBalancers", "resourceIdentifier": { "Name": "LoadBalancerDescriptions[].LoadBalancerName" } } } } aws-shell-0.2.1/awsshell/data/glacier/000077500000000000000000000000001335127342700175615ustar00rootroot00000000000000aws-shell-0.2.1/awsshell/data/glacier/2012-06-01/000077500000000000000000000000001335127342700206065ustar00rootroot00000000000000aws-shell-0.2.1/awsshell/data/glacier/2012-06-01/completions-1.json000066400000000000000000000026001335127342700241710ustar00rootroot00000000000000{ "operations": { "CreateVault": { "vaultName": { "resourceName": "Vault", "resourceIdentifier": "Name" }, "accountId": { "resourceName": "Vault", "resourceIdentifier": "AccountId" } }, "DeleteVault": { "vaultName": { "resourceName": "Vault", "resourceIdentifier": "Name" }, "accountId": { "resourceName": "Vault", "resourceIdentifier": "AccountId" } }, "InitiateJob": { "vaultName": { "resourceName": "Vault", "resourceIdentifier": "Name" }, "accountId": { "resourceName": "Vault", "resourceIdentifier": "AccountId" } }, "UploadArchive": { "vaultName": { "resourceName": "Vault", "resourceIdentifier": "Name" }, "accountId": { "resourceName": "Vault", "resourceIdentifier": "AccountId" } }, "InitiateMultipartUpload": { "vaultName": { "resourceName": "Vault", "resourceIdentifier": "Name" }, "accountId": { "resourceName": "Vault", "resourceIdentifier": "AccountId" } } }, "resources": { "Vault": { "operation": "ListVaults", "resourceIdentifier": { "AccountId": "accountId", "Name": "VaultList[].VaultName" } } } } aws-shell-0.2.1/awsshell/data/iam/000077500000000000000000000000001335127342700167215ustar00rootroot00000000000000aws-shell-0.2.1/awsshell/data/iam/2010-05-08/000077500000000000000000000000001335127342700177525ustar00rootroot00000000000000aws-shell-0.2.1/awsshell/data/iam/2010-05-08/completions-1.json000066400000000000000000000124341335127342700233430ustar00rootroot00000000000000{ "operations": { "DeleteInstanceProfile": { "InstanceProfileName": { "resourceName": "InstanceProfile", "resourceIdentifier": "Name" } }, "CreateLoginProfile": { "UserName": { "resourceName": "User", "resourceIdentifier": "Name" } }, "DeletePolicy": { "PolicyArn": { "resourceName": "Policy", "resourceIdentifier": "Arn" } }, "CreateGroup": { "GroupName": { "resourceName": "Group", "resourceIdentifier": "Name" } }, "DetachRolePolicy": { "RoleName": { "resourceName": "Role", "resourceIdentifier": "Name" } }, "EnableMFADevice": { "UserName": { "resourceName": "User", "resourceIdentifier": "Name" } }, "DeleteSAMLProvider": { "SAMLProviderArn": { "resourceName": "SamlProvider", "resourceIdentifier": "Arn" } }, "UpdateUser": { "UserName": { "resourceName": "User", "resourceIdentifier": "Name" } }, "PutGroupPolicy": { "GroupName": { "resourceName": "Group", "resourceIdentifier": "Name" } }, "DeleteVirtualMFADevice": { "SerialNumber": { "resourceName": "VirtualMfaDevice", "resourceIdentifier": "SerialNumber" } }, "DeleteGroup": { "GroupName": { "resourceName": "Group", "resourceIdentifier": "Name" } }, "DeleteRole": { "RoleName": { "resourceName": "Role", "resourceIdentifier": "Name" } }, "AttachGroupPolicy": { "PolicyArn": { "resourceName": "Policy", "resourceIdentifier": "Arn" } }, "CreateAccessKey": { "UserName": { "resourceName": "User", "resourceIdentifier": "Name" } }, "UpdateServerCertificate": { "ServerCertificateName": { "resourceName": "ServerCertificate", "resourceIdentifier": "Name" } }, "AddUserToGroup": { "UserName": { "resourceName": "User", "resourceIdentifier": "Name" } }, "UpdateSAMLProvider": { "SAMLProviderArn": { "resourceName": "SamlProvider", "resourceIdentifier": "Arn" } }, "RemoveRoleFromInstanceProfile": { "InstanceProfileName": { "resourceName": "InstanceProfile", "resourceIdentifier": "Name" } }, "DetachUserPolicy": { "UserName": { "resourceName": "User", "resourceIdentifier": "Name" } }, "DeleteUser": { "UserName": { "resourceName": "User", "resourceIdentifier": "Name" } }, "CreatePolicyVersion": { "PolicyArn": { "resourceName": "Policy", "resourceIdentifier": "Arn" } }, "AttachRolePolicy": { "RoleName": { "resourceName": "Role", "resourceIdentifier": "Name" } }, "RemoveUserFromGroup": { "UserName": { "resourceName": "User", "resourceIdentifier": "Name" } }, "AttachUserPolicy": { "UserName": { "resourceName": "User", "resourceIdentifier": "Name" } }, "PutUserPolicy": { "UserName": { "resourceName": "User", "resourceIdentifier": "Name" } }, "DeleteServerCertificate": { "ServerCertificateName": { "resourceName": "ServerCertificate", "resourceIdentifier": "Name" } }, "UpdateGroup": { "GroupName": { "resourceName": "Group", "resourceIdentifier": "Name" } }, "CreateUser": { "UserName": { "resourceName": "User", "resourceIdentifier": "Name" } }, "DetachGroupPolicy": { "PolicyArn": { "resourceName": "Policy", "resourceIdentifier": "Arn" } }, "AddRoleToInstanceProfile": { "InstanceProfileName": { "resourceName": "InstanceProfile", "resourceIdentifier": "Name" } } }, "resources": { "Group": { "operation": "ListGroups", "resourceIdentifier": { "Name": "Groups[].GroupName" } }, "VirtualMfaDevice": { "operation": "ListVirtualMFADevices", "resourceIdentifier": { "SerialNumber": "VirtualMFADevices[].SerialNumber" } }, "InstanceProfile": { "operation": "ListInstanceProfiles", "resourceIdentifier": { "Name": "InstanceProfiles[].InstanceProfileName" } }, "Role": { "operation": "ListRoles", "resourceIdentifier": { "Name": "Roles[].RoleName" } }, "User": { "operation": "ListUsers", "resourceIdentifier": { "Name": "Users[].UserName" } }, "Policy": { "operation": "ListPolicies", "resourceIdentifier": { "Arn": "Policies[].Arn" } }, "SamlProvider": { "operation": "ListSAMLProviders", "resourceIdentifier": { "Arn": "SAMLProviderList[].Arn" } }, "ServerCertificate": { "operation": "ListServerCertificates", "resourceIdentifier": { "Name": "ServerCertificateMetadataList[].ServerCertificateName" } } } }aws-shell-0.2.1/awsshell/data/kinesis/000077500000000000000000000000001335127342700176205ustar00rootroot00000000000000aws-shell-0.2.1/awsshell/data/kinesis/2013-12-02/000077500000000000000000000000001335127342700206445ustar00rootroot00000000000000aws-shell-0.2.1/awsshell/data/kinesis/2013-12-02/completions-1.json000066400000000000000000000033651335127342700242400ustar00rootroot00000000000000{ "operations": { "AddTagsToStream": { "StreamName": { "resourceName": "Stream", "resourceIdentifier": "Name" } }, "DeleteStream": { "StreamName": { "resourceName": "Stream", "resourceIdentifier": "Name" } }, "DecreaseStreamRetentionPeriod": { "StreamName": { "resourceName": "Stream", "resourceIdentifier": "Name" } }, "DescribeStream": { "StreamName": { "resourceName": "Stream", "resourceIdentifier": "Name" } }, "GetShardIterator": { "StreamName": { "resourceName": "Stream", "resourceIdentifier": "Name" } }, "IncreaseStreamRetentionPeriod": { "StreamName": { "resourceName": "Stream", "resourceIdentifier": "Name" } }, "ListTagsForStream": { "StreamName": { "resourceName": "Stream", "resourceIdentifier": "Name" } }, "MergeShards": { "StreamName": { "resourceName": "Stream", "resourceIdentifier": "Name" } }, "PutRecord": { "StreamName": { "resourceName": "Stream", "resourceIdentifier": "Name" } }, "PutRecords": { "StreamName": { "resourceName": "Stream", "resourceIdentifier": "Name" } }, "RemoveTagsFromStream": { "StreamName": { "resourceName": "Stream", "resourceIdentifier": "Name" } }, "SplitShard": { "StreamName": { "resourceName": "Stream", "resourceIdentifier": "Name" } } }, "resources": { "Stream": { "operation": "ListStreams", "resourceIdentifier": { "Name": "StreamNames[]" } } } } aws-shell-0.2.1/awsshell/data/opsworks/000077500000000000000000000000001335127342700200425ustar00rootroot00000000000000aws-shell-0.2.1/awsshell/data/opsworks/2013-02-18/000077500000000000000000000000001335127342700210745ustar00rootroot00000000000000aws-shell-0.2.1/awsshell/data/opsworks/2013-02-18/completions-1.json000066400000000000000000000006521335127342700244640ustar00rootroot00000000000000{ "operations": { "CreateLayer": { "StackId": { "resourceName": "Stack", "resourceIdentifier": "Id" } }, "DeleteStack": { "StackId": { "resourceName": "Stack", "resourceIdentifier": "Id" } } }, "resources": { "Stack": { "operation": "DescribeStacks", "resourceIdentifier": { "Id": "Stacks[].StackId" } } } }aws-shell-0.2.1/awsshell/data/s3/000077500000000000000000000000001335127342700165005ustar00rootroot00000000000000aws-shell-0.2.1/awsshell/data/s3/2006-03-01/000077500000000000000000000000001335127342700175255ustar00rootroot00000000000000aws-shell-0.2.1/awsshell/data/s3/2006-03-01/completions-1.json000066400000000000000000000012541335127342700231140ustar00rootroot00000000000000{ "operations": { "CreateBucket": { "Bucket": { "resourceName": "Bucket", "resourceIdentifier": "Name" } }, "PutObject": { "Bucket": { "resourceName": "Bucket", "resourceIdentifier": "Name" } }, "DeleteObjects": { "Bucket": { "resourceName": "Bucket", "resourceIdentifier": "Name" } }, "DeleteBucket": { "Bucket": { "resourceName": "Bucket", "resourceIdentifier": "Name" } } }, "resources": { "Bucket": { "operation": "ListBuckets", "resourceIdentifier": { "Name": "Buckets[].Name" } } } }aws-shell-0.2.1/awsshell/data/sns/000077500000000000000000000000001335127342700167565ustar00rootroot00000000000000aws-shell-0.2.1/awsshell/data/sns/2010-03-31/000077500000000000000000000000001335127342700200015ustar00rootroot00000000000000aws-shell-0.2.1/awsshell/data/sns/2010-03-31/completions-1.json000066400000000000000000000042661335127342700233760ustar00rootroot00000000000000{ "operations": { "ConfirmSubscription": { "TopicArn": { "resourceName": "Topic", "resourceIdentifier": "Arn" } }, "SetPlatformApplicationAttributes": { "PlatformApplicationArn": { "resourceName": "PlatformApplication", "resourceIdentifier": "Arn" } }, "Subscribe": { "TopicArn": { "resourceName": "Topic", "resourceIdentifier": "Arn" } }, "SetTopicAttributes": { "TopicArn": { "resourceName": "Topic", "resourceIdentifier": "Arn" } }, "DeletePlatformApplication": { "PlatformApplicationArn": { "resourceName": "PlatformApplication", "resourceIdentifier": "Arn" } }, "SetSubscriptionAttributes": { "SubscriptionArn": { "resourceName": "Subscription", "resourceIdentifier": "Arn" } }, "Publish": { "TopicArn": { "resourceName": "Topic", "resourceIdentifier": "Arn" } }, "AddPermission": { "TopicArn": { "resourceName": "Topic", "resourceIdentifier": "Arn" } }, "Unsubscribe": { "SubscriptionArn": { "resourceName": "Subscription", "resourceIdentifier": "Arn" } }, "RemovePermission": { "TopicArn": { "resourceName": "Topic", "resourceIdentifier": "Arn" } }, "CreatePlatformEndpoint": { "PlatformApplicationArn": { "resourceName": "PlatformApplication", "resourceIdentifier": "Arn" } }, "DeleteTopic": { "TopicArn": { "resourceName": "Topic", "resourceIdentifier": "Arn" } } }, "resources": { "Topic": { "operation": "ListTopics", "resourceIdentifier": { "Arn": "Topics[].TopicArn" } }, "PlatformApplication": { "operation": "ListPlatformApplications", "resourceIdentifier": { "Arn": "PlatformApplications[].PlatformApplicationArn" } }, "Subscription": { "operation": "ListSubscriptions", "resourceIdentifier": { "Arn": "Subscriptions[].SubscriptionArn" } } } }aws-shell-0.2.1/awsshell/data/sqs/000077500000000000000000000000001335127342700167615ustar00rootroot00000000000000aws-shell-0.2.1/awsshell/data/sqs/2012-11-05/000077500000000000000000000000001335127342700200065ustar00rootroot00000000000000aws-shell-0.2.1/awsshell/data/sqs/2012-11-05/completions-1.json000066400000000000000000000027131335127342700233760ustar00rootroot00000000000000{ "operations": { "SetQueueAttributes": { "QueueUrl": { "resourceName": "Queue", "resourceIdentifier": "Url" } }, "SendMessageBatch": { "QueueUrl": { "resourceName": "Queue", "resourceIdentifier": "Url" } }, "DeleteMessageBatch": { "QueueUrl": { "resourceName": "Queue", "resourceIdentifier": "Url" } }, "AddPermission": { "QueueUrl": { "resourceName": "Queue", "resourceIdentifier": "Url" } }, "ChangeMessageVisibilityBatch": { "QueueUrl": { "resourceName": "Queue", "resourceIdentifier": "Url" } }, "SendMessage": { "QueueUrl": { "resourceName": "Queue", "resourceIdentifier": "Url" } }, "DeleteQueue": { "QueueUrl": { "resourceName": "Queue", "resourceIdentifier": "Url" } }, "PurgeQueue": { "QueueUrl": { "resourceName": "Queue", "resourceIdentifier": "Url" } }, "ReceiveMessage": { "QueueUrl": { "resourceName": "Queue", "resourceIdentifier": "Url" } }, "RemovePermission": { "QueueUrl": { "resourceName": "Queue", "resourceIdentifier": "Url" } } }, "resources": { "Queue": { "operation": "ListQueues", "resourceIdentifier": { "Url": "QueueUrls[]" } } } }aws-shell-0.2.1/awsshell/db.py000066400000000000000000000023601335127342700162020ustar00rootroot00000000000000from __future__ import unicode_literals import os import sqlite3 class ConcurrentDBM(object): @classmethod def open(cls, filename, create=False): if create and not os.path.isfile(filename): return cls.create(filename) else: db = sqlite3.connect(filename) return cls(db) @classmethod def create(cls, filename): db = sqlite3.connect(filename) with db: db.execute( 'CREATE TABLE docindex (key TEXT PRIMARY KEY, value TEXT)') return cls(db) def __init__(self, db): self._db = db def __getitem__(self, key): if isinstance(key, bytes): key = key.decode('utf-8') cursor = self._db.cursor() cursor.execute( 'SELECT value FROM docindex WHERE key = :key', {'key': key}) result = cursor.fetchone() if result is not None: return result[0] raise KeyError(key) def __setitem__(self, key, value): with self._db: self._db.execute( 'INSERT OR REPLACE INTO docindex (key, value) ' 'VALUES (:key, :value)', {'key': key, 'value': value}) def close(self): self._db.close() aws-shell-0.2.1/awsshell/docs.py000066400000000000000000000022431335127342700165450ustar00rootroot00000000000000from __future__ import unicode_literals from awsshell import db def load_lazy_doc_index(filename): d = load_doc_db(filename) return DocRetriever(d) def load_doc_db(filename): d = db.ConcurrentDBM.open(filename, create=True) return d class DocRetriever(object): """Retrieve documentation for the AWS CLI.""" def __init__(self, doc_index): # Internally, most of the speedup comes from # the fact that this data is pre-rendered and # indexed. self._doc_index = doc_index self._cache = {} def extract_description(self, dot_cmd): try: docs = self._doc_index[dot_cmd] except KeyError: return u'' index = docs.find('SYNOPSIS') if index > 0: docs = docs[:index] return docs def extract_param(self, dot_cmd, param_name): try: docs = self._doc_index[dot_cmd] except KeyError: return u'' index = docs.find('OPTIONS') param_start_index = docs.find(param_name, index) param_end_index = docs.find('--', param_start_index + 1) return docs[param_start_index:param_end_index] aws-shell-0.2.1/awsshell/fuzzy.py000066400000000000000000000054031335127342700170050ustar00rootroot00000000000000"""Fuzzy finder for AWS Shell. This is a fuzzy finder used for the autocompleter that I've tried to optimize for the AWS CLI set of commands. There doesn't seem to be a generic algorithm that does exactly what I want. Changing any of these heuristics so far means that you have to give up some other component of the idealized matching. The main things I care about: * Special weight is given to subsequences on a word boundary. So "drio" scores higher for "describe-reserved-instances_offering" than "create-spot-datafeed-subscription". * The more of the word you complete, the higher score it has, so given "describe-instance", then "describe-instances" should match higher than "describe-instance-attribute" because the former matches every single character except for one. * Similar to one, "rinstance" should rank "run-instances" higher than "describe-instances" because the "r" falls on a word boundary. High Level Idea =============== The basic idea is to try to calculate a numeric score between 0 and 1 given the users's search string and a possible word in the corpus of words. You calculate the score for each word and then sort them appropriately and return the results in order back to the user. A score of 1 is the highest possible score, it would represent an exact match, and 0 is the lowest meaning these is no possible chance for the word to be a match. """ from __future__ import print_function def fuzzy_search(user_input, corpus): candidates = [] for word in corpus: current_score = calculate_score(user_input, word) if current_score > 0: candidates.append((word, current_score)) return [c[0] for c in sorted(candidates, key=lambda x: x[1], reverse=True)] def calculate_score(search_string, word): """Calculate how well the search string matches the word.""" # See the module docstring for a high level description # of what we're trying to do. # * If the search string is larger than the word, we know # immediately that this can't be a match. if len(search_string) > len(word): return 0 original_word = word score = 1 search_index = 0 while True: scale = 1.0 search_char = search_string[search_index] i = word.find(search_char) if i < 0: return 0 if i > 0 and word[i - 1] == '-': scale = 0.95 else: scale = 1 - (i / float(len(word))) score *= scale word = word[i + 1:] search_index += 1 if search_index >= len(search_string): break # The more characters that matched the word, the better # so prefer more complete matches. completion_scale = 1 - (len(word) / float(len(original_word))) score *= completion_scale return score aws-shell-0.2.1/awsshell/index/000077500000000000000000000000001335127342700163515ustar00rootroot00000000000000aws-shell-0.2.1/awsshell/index/__init__.py000066400000000000000000000103211335127342700204570ustar00rootroot00000000000000"""Subpackage for indexing data. Note that, while there is documentation for all the code in this package, these modules/classes are considered internal and not intended for use outside of the awsshell. It's very likely that these interfaces will change over time and may have backwards incompatible changes as improvements are made. This package contains modules for working with the index data that the AWS Shell uses. It manages the creation and loading of the AWS Shell index files. In this context, an index file is specifically used to speed up various operations performed by the AWS CLI: autocompletion, pulling up docs, server side resource completion, etc. In typically consists of data constructed in such a way to make it quick and easy to answer questions such as "what are the available subcommands? what are the docs for this operation? what arguments does this command take?". The AWS Shell supports has several types of index files: * The local command completion index. * The documentation index. * The server side resource completion index. They're split into separate files because they're used in different scenarios, and generated by different means (command completion is driven by botocore JSON models, and server side completion is driven by boto3 resource models). Couple of things to know about the index files: * The command completion and doc index are indexed by CLI version. Each version of the CLI can potentially add new commands/services, so we want to ensure that we have an index per version so we can offer the correct autocompletion and documentation based on what CLI version you have installed. * Command completion and documentation are automatically generated based on demand. If we notice you don't have an index available for the particular CLI version, we automatically generate the data. * The total amount of documentation for every command for every service is large. We want to avoid loading the entire docset into memory, so we're not using JSON. Instead we're using a DBM interface (via ``shelve``). May also consider sqlite. * Server side resource completion is *not* generated on demand. This package contains code for generating the a "reverse index" derived from boto3 docs. This is primarily done while we explore to what extent the boto3 resource models have the information we need. To be fair, the resource models were never intended to be used in the mechanism in which we're using them, so it's very possible we may need to enhance the models by hand. This is why we pre-generate the index for server side completion up front so we can add hand modifications if needed. It's very possible that we may be able to come up with something sufficiently sophisticated enough where we can change this index to by autogenerated on demand like the other two. * While the indexes are grouped in a single subpackage for discoverability, they (purposefully) do not share the same interface for usage. Each index needs to answer different questions, so it does not make sense for them to share the same interface. Usage ===== Loading the completion index .. code-block:: python # Default usage: loader = CompletionIndex() # Load based on the current CLI version you have installed # (i.e awscli.__version__) index = loader.load_index() # Load from a specific directory. loader = CompletionIndex(cache_dir='/tmp/mycache') index = loader.load_index() # Load a specific version. loader = CompletionIndex() index = loader.load_index(version='1.9.1') # Generating an index. loader = CompletionIndex() # You don't give it a version because we can only # generate an index based on the version we can import. completion_index = loader.generate_index() # You don't need to give a filename, it will write it out # based on the current version. loader.write_index_to_file(completion_index) # Generate and write the index in one step. It will also # return the generated index in case you need it. loader.generate_and_write_index() # This is some autocompleter could use the index loader: loader = CompletionIndex() if not loader.index_exists(): index = loader.generate_and_write_index() else: index = loader.load_index() autocompleter = MyAutoCompleter(index) """ aws-shell-0.2.1/awsshell/index/completion.py000066400000000000000000000065361335127342700211060ustar00rootroot00000000000000"""Module for completion index. Generates, loads, and writes out completion index. Also provides an interface for working with the indexed data. The the subpackage docstring of awsshell.index for a higher level overview. """ import os import json from awsshell.utils import FSLayer, FileReadError, build_config_file_path from awsshell import utils class IndexLoadError(Exception): """Raised when an index could not be loaded.""" class CompletionIndex(object): """Handles working with the local commmand completion index. :type commands: list :param commands: ec2, s3, elb... :type subcommands: list :param subcommands: start-instances, stop-instances, terminate-instances... :type global_opts: list :param global_opts: --profile, --region, --output... :type args_opts: set, to filter out duplicates :param args_opts: ec2 start-instances: --instance-ids, --dry-run... """ # The completion index can read/write to a cache dir # so that it doesn't have to recompute the completion cache # every time the CLI starts up. DEFAULT_CACHE_DIR = build_config_file_path('cache') def __init__(self, cache_dir=DEFAULT_CACHE_DIR, fslayer=None): self._cache_dir = cache_dir if fslayer is None: fslayer = FSLayer() self._fslayer = fslayer self.commands = [] self.subcommands = [] self.global_opts = [] self.args_opts = set() def load_index(self, version_string): """Load the completion index for a given CLI version. :type version_string: str :param version_string: The AWS CLI version, e.g "1.9.2". :raises: :class:`IndexLoadError ` """ filename = self._filename_for_version(version_string) try: contents = self._fslayer.file_contents(filename) except FileReadError as e: raise IndexLoadError(str(e)) return contents def _filename_for_version(self, version_string): return os.path.join( self._cache_dir, 'completions-%s.json' % version_string) def load_completions(self): """Load completions from the completion index. Updates the following attributes: * commands * subcommands * global_opts * args_opts """ try: index_str = self.load_index(utils.AWSCLI_VERSION) except IndexLoadError: return index_str = self.load_index(utils.AWSCLI_VERSION) index_data = json.loads(index_str) index_root = index_data['aws'] # ec2, s3, elb... self.commands = index_root['commands'] # --profile, --region, --output... self.global_opts = index_root['arguments'] for command in self.commands: # ec2: start-instances, stop-instances, terminate-instances... subcommands_current = index_root['children'] \ .get(command)['commands'] self.subcommands.extend(subcommands_current) for subcommand_current in subcommands_current: # start-instances: --instance-ids, --dry-run... args_opts_current = index_root['children'] \ .get(command)['children'] \ .get(subcommand_current)['arguments'] self.args_opts.update(args_opts_current) aws-shell-0.2.1/awsshell/keys.py000066400000000000000000000143671335127342700166020ustar00rootroot00000000000000# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"). You # may not use this file except in compliance with the License. A copy of # the License is located at # # http://aws.amazon.com/apache2.0/ # # or in the "license" file accompanying this file. This file is # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. from prompt_toolkit.key_binding.manager import KeyBindingManager from prompt_toolkit.keys import Keys class KeyManager(object): """A custom :class:`prompt_toolkit.KeyBindingManager`. Handles togging of: * Fuzzy or substring matching. * Vi or Emacs key bindings. * Multi or single columns in the autocompletion menu. * Showing or hiding the help pane. :type manager: :class:`prompt_toolkit.KeyBindingManager` :param manager: A custom `KeyBindingManager`. """ def __init__(self, get_match_fuzzy, set_match_fuzzy, get_enable_vi_bindings, set_enable_vi_bindings, get_show_completion_columns, set_show_completion_columns, get_show_help, set_show_help, stop_input_and_refresh_cli): self.manager = None self._create_key_manager( get_match_fuzzy, set_match_fuzzy, get_enable_vi_bindings, set_enable_vi_bindings, get_show_completion_columns, set_show_completion_columns, get_show_help, set_show_help, stop_input_and_refresh_cli) def _create_key_manager(self, get_match_fuzzy, set_match_fuzzy, get_enable_vi_bindings, set_enable_vi_bindings, get_show_completion_columns, set_show_completion_columns, get_show_help, set_show_help, stop_input_and_refresh_cli): """Create and initialize the keybinding manager. :type get_fuzzy_match: callable :param get_fuzzy_match: Gets the fuzzy matching config. :type set_fuzzy_match: callable :param set_fuzzy_match: Sets the fuzzy matching config. :type get_enable_vi_bindings: callable :param get_enable_vi_bindings: Gets the vi (or emacs) key bindings config. :type set_enable_vi_bindings: callable :param set_enable_vi_bindings: Sets the vi (or emacs) key bindings config. :type get_show_completion_columns: callable :param get_show_completion_columns: Gets the show completions in multiple or single columns config. type set_show_completion_columns: callable :param set_show_completion_columns: Sets the show completions in multiple or single columns config. :type get_show_help: callable :param get_show_help: Gets the show help pane config. :type set_show_help: callable :param set_show_help: Sets the show help pane config. :type stop_input_and_refresh_cli: callable param stop_input_and_refresh_cli: Stops input by raising an `InputInterrupt`, forces a cli refresh to ensure certain options take effect within the current session. :rtype: :class:`prompt_toolkit.KeyBindingManager` :return: A custom `KeyBindingManager`. """ assert callable(get_match_fuzzy) assert callable(set_match_fuzzy) assert callable(get_enable_vi_bindings) assert callable(set_enable_vi_bindings) assert callable(get_show_completion_columns) assert callable(set_show_completion_columns) assert callable(get_show_help) assert callable(set_show_help) assert callable(stop_input_and_refresh_cli) self.manager = KeyBindingManager( enable_search=True, enable_abort_and_exit_bindings=True, enable_system_bindings=True, enable_auto_suggest_bindings=True, enable_open_in_editor=False) @self.manager.registry.add_binding(Keys.F2) def handle_f2(_): """Toggle fuzzy matching. :type _: :class:`prompt_toolkit.Event` :param _: (Unused) """ set_match_fuzzy(not get_match_fuzzy()) @self.manager.registry.add_binding(Keys.F3) def handle_f3(_): """Toggle Vi mode keybindings matching. Disabling Vi keybindings will enable Emacs keybindings. :type _: :class:`prompt_toolkit.Event` :param _: (Unused) """ set_enable_vi_bindings(not get_enable_vi_bindings()) stop_input_and_refresh_cli() @self.manager.registry.add_binding(Keys.F4) def handle_f4(_): """Toggle multiple column completions. :type _: :class:`prompt_toolkit.Event` :param _: (Unused) """ set_show_completion_columns(not get_show_completion_columns()) stop_input_and_refresh_cli() @self.manager.registry.add_binding(Keys.F5) def handle_f5(_): """Toggle the help container. :type _: :class:`prompt_toolkit.Event` :param _: (Unused) """ set_show_help(not get_show_help()) stop_input_and_refresh_cli() @self.manager.registry.add_binding(Keys.F9) def handle_f9(event): """Switch between the default and docs buffers. :type event: :class:`prompt_toolkit.Event` :param event: Contains info about the event, namely the cli which is used to changing which buffer is focused. """ if event.cli.current_buffer_name == u'clidocs': event.cli.focus(u'DEFAULT_BUFFER') else: event.cli.focus(u'clidocs') @self.manager.registry.add_binding(Keys.F10) def handle_f10(event): """Quit when the `F10` key is pressed. :type event: :class:`prompt_toolkit.Event` :param event: Contains info about the event, namely the cli which is used for exiting the app. """ event.cli.set_exit() aws-shell-0.2.1/awsshell/lexer.py000066400000000000000000000041531335127342700167360ustar00rootroot00000000000000# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"). You # may not use this file except in compliance with the License. A copy of # the License is located at # # http://aws.amazon.com/apache2.0/ # # or in the "license" file accompanying this file. This file is # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. from pygments.lexer import RegexLexer from pygments.lexer import words from pygments.token import Keyword, Literal, Name, Operator, Text from awsshell.index.completion import CompletionIndex class ShellLexer(RegexLexer): """Provides highlighting for commands, subcommands, arguments, and options. :type completion_index: :class:`CompletionIndex` :param completion_index: Completion index used to determine commands, subcommands, arguments, and options for highlighting. :type tokens: dict :param tokens: A dict of (`pygments.lexer`, `pygments.token`) used for pygments highlighting. """ completion_index = CompletionIndex() completion_index.load_completions() tokens = { 'root': [ # ec2, s3, elb... (words( tuple(completion_index.commands), prefix=r'\b', suffix=r'\b'), Literal.String), # describe-instances (words( tuple(completion_index.subcommands), prefix=r'\b', suffix=r'\b'), Name.Class), # --instance-ids (words( tuple(list(completion_index.args_opts)), prefix=r'', suffix=r'\b'), Keyword.Declaration), # --profile (words( tuple(completion_index.global_opts), prefix=r'', suffix=r'\b'), Operator.Word), # Everything else (r'.*\n', Text), ] } aws-shell-0.2.1/awsshell/loaders.py000066400000000000000000000006441335127342700172510ustar00rootroot00000000000000import json from awsshell.utils import build_config_file_path class JSONIndexLoader(object): def __init__(self): pass @staticmethod def index_filename(version_string, type_name='completions'): return build_config_file_path( '%s-%s.json' % (version_string, type_name)) def load_index(self, filename): with open(filename, 'r') as f: return json.load(f) aws-shell-0.2.1/awsshell/makeindex.py000066400000000000000000000137301335127342700175650ustar00rootroot00000000000000"""Module for building the autocompletion indices.""" from __future__ import print_function import os import json from six import BytesIO from docutils.core import publish_string from botocore.docs.bcdoc import textwriter import awscli.clidriver from awscli.argprocess import ParamShorthandDocGen from awsshell import determine_doc_index_filename from awsshell.utils import remove_html from awsshell import docs SHORTHAND_DOC = ParamShorthandDocGen() def new_index(): return {'arguments': [], 'argument_metadata': {}, 'commands': [], 'children': {}} def index_command(index_dict, help_command): arg_table = help_command.arg_table for arg in arg_table: arg_obj = arg_table[arg] metadata = { 'required': arg_obj.required, 'type_name': arg_obj.cli_type_name, 'minidoc': '', 'example': '', # The name used in the API call/botocore, # typically CamelCased. 'api_name': getattr(arg_obj, '_serialized_name', '') } if arg_obj.documentation: metadata['minidoc'] = remove_html( arg_obj.documentation.split('\n')[0]) if SHORTHAND_DOC.supports_shorthand(arg_obj.argument_model): service_name, op_name = help_command.event_class.rsplit('.', 1) example = SHORTHAND_DOC.generate_shorthand_example( cli_argument=arg_obj, service_id=service_name, operation_name=op_name, ) metadata['example'] = example index_dict['arguments'].append('--%s' % arg) index_dict['argument_metadata']['--%s' % arg] = metadata for cmd in help_command.command_table: index_dict['commands'].append(cmd) # Each sub command will trigger a recurse. child = new_index() index_dict['children'][cmd] = child sub_command = help_command.command_table[cmd] sub_help_command = sub_command.create_help_command() index_command(child, sub_help_command) def write_index(output_filename=None): driver = awscli.clidriver.create_clidriver() help_command = driver.create_help_command() index = {'aws': new_index()} current = index['aws'] index_command(current, help_command) result = json.dumps(index) if not os.path.isdir(os.path.dirname(output_filename)): os.makedirs(os.path.dirname(output_filename)) with open(output_filename, 'w') as f: f.write(result) def write_doc_index(output_filename=None, db=None, help_command=None): if output_filename is None: output_filename = determine_doc_index_filename() user_provided_db = True if db is None: user_provided_db = False db = docs.load_doc_db(output_filename) if help_command is None: driver = awscli.clidriver.create_clidriver() help_command = driver.create_help_command() should_close = not user_provided_db do_write_doc_index(db, help_command, close_db_on_finish=should_close) def do_write_doc_index(db, help_command, close_db_on_finish): try: _index_docs(db, help_command) db['__complete__'] = 'true' finally: if close_db_on_finish: # If the user provided their own db object, # they are responsible for closing it. # If we created our own db object, we own # closing the db. db.close() def _index_docs(db, help_command): for command_name in help_command.command_table: command = help_command.command_table[command_name] sub_help_command = command.create_help_command() text_docs = render_docs_for_cmd(sub_help_command) dotted_name = '.'.join(['aws'] + command.lineage_names) db[dotted_name] = text_docs _index_docs(db, sub_help_command) def render_docs_for_cmd(help_command): renderer = FileRenderer() help_command.renderer = renderer help_command(None, None) # The report_level override is so that we don't print anything # to stdout/stderr on rendering issues. original_cli_help = renderer.contents.decode('utf-8') text_content = convert_rst_to_basic_text(original_cli_help) index = text_content.find('DESCRIPTION') if index > 0: text_content = text_content[index + len('DESCRIPTION'):] return text_content def convert_rst_to_basic_text(contents): """Convert restructured text to basic text output. This function removes most of the decorations added in restructured text. This function is used to generate documentation we can show to users in a cross platform manner. Basic indentation and list formatting are kept, but many RST features are removed (such as section underlines). """ # The report_level override is so that we don't print anything # to stdout/stderr on rendering issues. converted = publish_string( contents, writer=BasicTextWriter(), settings_overrides={'report_level': 5}) return converted.decode('utf-8') class FileRenderer(object): def __init__(self): self._io = BytesIO() def render(self, contents): self._io.write(contents) @property def contents(self): return self._io.getvalue() class BasicTextWriter(textwriter.TextWriter): def translate(self): visitor = BasicTextTranslator(self.document) self.document.walkabout(visitor) self.output = visitor.body class BasicTextTranslator(textwriter.TextTranslator): def depart_title(self, node): # Make the section titles upper cased, similar to # the man page output. text = ''.join(x[1] for x in self.states.pop() if x[0] == -1) self.stateindent.pop() self.states[-1].append((0, ['', text.upper(), ''])) # The botocore TextWriter has additional formatting # for literals, for the aws-shell docs we don't want any # special processing so these nodes are noops. def visit_literal(self, node): pass def depart_literal(self, node): pass aws-shell-0.2.1/awsshell/resource/000077500000000000000000000000001335127342700170715ustar00rootroot00000000000000aws-shell-0.2.1/awsshell/resource/__init__.py000066400000000000000000000000001335127342700211700ustar00rootroot00000000000000aws-shell-0.2.1/awsshell/resource/index.py000066400000000000000000000254271335127342700205640ustar00rootroot00000000000000"""Index and retrive information from the resource JSON. The classes are organized as follows: * ResourceIndexBuilder - Takes a boto3 resource and converts into the index format we need to do server side completions. * CompleterDescriber - Takes the index from ResourceIndexBuilder and looks up how to perform the autocompletion. Note that this class does *not* actually do the autocompletion. It merely tells you how you _would_ do the autocompletion if you made the appropriate service calls. * ServerSideCompleter - The thing that does the actual autocompletion. You tell it the command/operation/param you're on, and it will return a list of completions for you. """ import os import logging from collections import namedtuple import jmespath from botocore import xform_name from botocore.exceptions import BotoCoreError LOG = logging.getLogger(__name__) # service - The name of the AWS service # operation - The name of the AWS operation # params - A dict of params to send in the request (not implemented yet) # path - A JMESPath expression to select the expected elements. ServerCompletion = namedtuple('ServerCompletion', ['service', 'operation', 'params', 'path']) def extract_field_from_jmespath(expression): result = jmespath.compile(expression) current = result.parsed while current['children']: current = current['children'][0] if current['type'] == 'field': return current['value'] class ResourceIndexBuilder(object): def __init__(self): pass def build_index(self, resource_data): # First we need to go through the 'resources' # key and map all of its actions back to the # resource name. index = { 'operations': {}, 'resources': {}, } service = resource_data['service'] if 'hasMany' in service: for model in service['hasMany'].values(): resource_name = model['resource']['type'] for identifier in model['resource']['identifiers']: first_identifier = model['resource']['identifiers'][0] index['resources'][resource_name] = { 'operation': model['request']['operation'], # TODO: map all the identifiers. # We're only taking the first one for now. 'resourceIdentifier': { first_identifier['target']: first_identifier['path'] } } for resource_name, model in resource_data['resources'].items(): if resource_name not in index['resources']: continue if 'actions' in model: resource_actions = model['actions'] for action_model in resource_actions.values(): op_name = action_model['request']['operation'] current = {} index['operations'][op_name] = current for param in action_model['request']['params']: if param['source'] == 'identifier': field_name = extract_field_from_jmespath( param['target']) current[field_name] = { 'resourceName': resource_name, 'resourceIdentifier': param['name'], } return index class CompleterDescriber(object): """Describes how to autocomplete a resource. You give this class a service/operation/param and it will describe to you how you can autocomplete values for the provided parameter. It's up to the caller to actually take that description and make the appropriate service calls + filtering to extract out the server side values. """ def __init__(self, resource_index): self._index = resource_index def describe_autocomplete(self, service, operation, param): """Describe operation and args needed for server side completion. :type service: str :param service: The AWS service name. :type operation: str :param operation: The AWS operation name. :type param: str :param param: The name of the parameter being completed. This must match the casing in the service model (e.g. InstanceIds, not --instance-ids). :rtype: ServerCompletion :return: A ServerCompletion object that describes what API call to make in order to complete the response. """ service_index = self._index[service] LOG.debug(service_index) if param not in service_index.get('operations', {}).get(operation, {}): LOG.debug("param not in index: %s", param) return None p = service_index['operations'][operation][param] resource_name = p['resourceName'] resource_identifier = p['resourceIdentifier'] resource_index = service_index['resources'][resource_name] completion_operation = resource_index['operation'] path = resource_index['resourceIdentifier'][resource_identifier] return ServerCompletion(service=service, operation=completion_operation, params={}, path=path) class CachedClientCreator(object): def __init__(self, session): #: A botocore.session.Session object. Only the #: create_client() method is used. self._session = session self._client_cache = {} def create_client(self, service_name): if service_name not in self._client_cache: client = self._session.create_client(service_name) self._client_cache[service_name] = client return self._client_cache[service_name] class CompleterDescriberCreator(object): """Create and cache CompleterDescriber objects.""" def __init__(self, loader): #: A botocore.loader.Loader self._loader = loader self._describer_cache = {} self._services_with_completions = None def create_completer_query(self, service_name): """Create a CompleterDescriber for a service. :type service_name: str :param service_name: The name of the service, e.g. 'ec2' :return: A CompleterDescriber object. """ if service_name not in self._describer_cache: query = self._create_completer_query(service_name) self._describer_cache[service_name] = query return self._describer_cache[service_name] def _create_completer_query(self, service_name): completions_model = self._loader.load_service_model( service_name, 'completions-1') cq = CompleterDescriber({service_name: completions_model}) return cq def services_with_completions(self): if self._services_with_completions is not None: return self._services_with_completions self._services_with_completions = set( self._loader.list_available_services(type_name='completions-1')) return self._services_with_completions class ServerSideCompleter(object): def __init__(self, client_creator, describer_creator): self._client_creator = client_creator self._describer_creator = describer_creator def retrieve_candidate_values(self, service, operation, param): """Retrieve server side completions. :type service: str :param service: The service name, e.g. 'ec2', 'iam'. :type operation: str :param operation: The operation name, in the casing used by the CLI (words separated by hyphens), e.g. 'describe-instances', 'delete-user'. :type param: str :param param: The param name, as specified in the service model, e.g. 'InstanceIds', 'UserName'. :rtype: list :return: A list of possible completions for the service/operation/param combination. If no completions were found an empty list is returned. """ # Example call: # service='ec2', # operation='terminate-instances', # param='InstanceIds'. if service not in self._describer_creator.services_with_completions(): return [] try: client = self._client_creator.create_client(service) except BotoCoreError as e: # create_client() could raise an exception if the session # isn't fully configured (say it's missing a region). # However, we don't want to turn off all server side # completions because it's still possible to create # clients for some services without a region, e.g. IAM. LOG.debug("Error when trying to create a client for %s", service, exc_info=True) return [] api_operation_name = client.meta.method_to_api_mapping.get( operation.replace('-', '_')) if api_operation_name is None: return [] # Now we need to convert the param name to the # casing used by the API. completer = self._describer_creator.create_completer_query(service) result = completer.describe_autocomplete( service, api_operation_name, param) if result is None: return try: response = getattr(client, xform_name(result.operation, '_'))() except Exception as e: LOG.debug("Error when calling %s.%s: %s", service, result.operation, e, exc_info=True) return results = jmespath.search(result.path, response) return results def main(): # Generate the latest autocompletion indices from # boto3. You'll need to do this if you pull in # a new boto3 version that has updated resource models. import sys import json import os import boto3.session data_dir = os.path.join( os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'data') if not os.path.isdir(data_dir): os.makedirs(data_dir) session = boto3.session.Session() loader = session._loader builder = ResourceIndexBuilder() for resource_name in session.get_available_resources(): api_version = loader.determine_latest_version( resource_name, 'resources-1') model = loader.load_service_model(resource_name, 'resources-1', api_version) index = builder.build_index(model) output_file = os.path.join(data_dir, resource_name, api_version, 'completions-1.json') if not os.path.isdir(os.path.dirname(output_file)): os.makedirs(os.path.dirname(output_file)) with open(output_file, 'w') as f: f.write(json.dumps(index, indent=2)) if __name__ == '__main__': main() aws-shell-0.2.1/awsshell/shellcomplete.py000066400000000000000000000146721335127342700204660ustar00rootroot00000000000000"""Autocompletion integration with python prompt toolkit. This module integrates the low level autocomplete functionality provided in awsshell.autocomplete and integrates it with the interface required for autocompletion in the python prompt toolkit. If you're interested in the heavy lifting of the autocompletion logic, see awsshell.autocomplete. """ import os import logging import botocore.session from prompt_toolkit.completion import Completer, Completion from awsshell import fuzzy LOG = logging.getLogger(__name__) class AWSShellCompleter(Completer): """Completer class for the aws-shell. This is the completer used specifically for the aws shell. Not to be confused with the AWSCLIModelCompleter, which is more low level, and can be reused in contexts other than the aws shell. """ def __init__(self, completer, server_side_completer=None): self._completer = completer if server_side_completer is None: server_side_completer = self._create_server_side_completer() self._server_side_completer = server_side_completer def _create_server_side_completer(self, session=None): from awsshell.resource import index if session is None: session = botocore.session.Session() loader = session.get_component('data_loader') completions_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), 'data') loader.search_paths.insert(0, completions_path) client_creator = index.CachedClientCreator(session) describer = index.CompleterDescriberCreator(loader) completer = index.ServerSideCompleter(client_creator, describer) return completer def change_profile(self, profile_name): """Change the profile used for server side completions.""" self._server_side_completer = self._create_server_side_completer( session=botocore.session.Session(profile=profile_name)) @property def completer(self): return self._completer @completer.setter def completer(self, value): self._completer = value @property def last_option(self): return self._completer.last_option @property def current_command(self): return u' '.join(self._completer.cmd_path) def _convert_to_prompt_completions(self, low_level_completions, text_before_cursor): # Converts the low level completions from the model autocompleter # and converts them to Completion() objects used by # prompt_toolkit. We also try to enhance the metadata of the # completion by including docs and marking required fields. arg_meta = self._completer.arg_metadata arg_meta.update(self._completer.global_arg_metadata) word_before_cursor = '' if text_before_cursor.strip(): word_before_cursor = text_before_cursor.strip().split()[-1] for completion in low_level_completions: # Go through the completions and add inline docs and # mark which options are required. if completion.startswith('--') and completion in arg_meta: # TODO: Need to handle merging in global options as well. meta = arg_meta[completion] if meta['required']: display_text = '%s (required)' % completion else: display_text = completion type_name = arg_meta[completion]['type_name'] display_meta = '[%s] %s' % (type_name, arg_meta[completion]['minidoc']) else: display_text = completion display_meta = '' if text_before_cursor and text_before_cursor[-1] == ' ': location = 0 else: location = -len(word_before_cursor) yield Completion(completion, location, display=display_text, display_meta=display_meta) def get_completions(self, document, complete_event): text_before_cursor = document.text_before_cursor completions = self._completer.autocomplete(text_before_cursor) prompt_completions = list(self._convert_to_prompt_completions( completions, text_before_cursor)) if (not prompt_completions and self._completer.last_option and len(self._completer.cmd_path) == 3): # If we couldn't complete anything from the JSON model # completer and we're on a cli option (e.g --foo), we # can ask the server side completer if it knows anything # about this resource. LOG.debug("No local autocompletions found, trying " "server side completion.") command = self._completer.cmd_path service = command[1] if service == 's3api': # TODO: we need a more generic way to capture renames # of commands. This currently lives in the CLI # customization code. service = 's3' operation = command[2] param = self._completer.arg_metadata.get( self._completer.last_option, {}).get('api_name') if param is not None: LOG.debug("Trying to retrieve autcompletion for: " "%s, %s, %s", service, operation, param) results = self._server_side_completer\ .retrieve_candidate_values(service, operation, param) LOG.debug("Results for %s, %s, %s: %s", service, operation, param, results) word_before_cursor = text_before_cursor.strip().split()[-1] location = 0 if text_before_cursor[-1] != ' ' and \ word_before_cursor and results: # Filter the results down by fuzzy searching what # the user has provided. results = fuzzy.fuzzy_search(word_before_cursor, results) location = -len(word_before_cursor) if results is not None: for result in results: # Insert at the end yield Completion(result, location, display=result, display_meta='') else: for c in prompt_completions: yield c aws-shell-0.2.1/awsshell/style.py000066400000000000000000000046741335127342700167670ustar00rootroot00000000000000# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"). You # may not use this file except in compliance with the License. A copy of # the License is located at # # http://aws.amazon.com/apache2.0/ # # or in the "license" file accompanying this file. This file is # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. from pygments.token import Token from pygments.util import ClassNotFound from pygments.styles import get_style_by_name from prompt_toolkit.styles import default_style_extensions, style_from_dict class StyleFactory(object): """Provide styles for the autocomplete menu and the toolbar. :type style: :class:`pygments.style.StyleMeta` :param style: Contains pygments style info. """ def __init__(self, style_name): self.style = self.style_factory(style_name) def style_factory(self, style_name): """Retrieve the specified pygments style. If the specified style is not found, the vim style is returned. :type style_name: str :param style_name: The pygments style name. :rtype: :class:`pygments.style.StyleMeta` :return: Pygments style info. """ try: style = get_style_by_name(style_name) except ClassNotFound: style = get_style_by_name('vim') # Create a style dictionary. styles = {} styles.update(style.styles) styles.update(default_style_extensions) t = Token styles.update({ t.Menu.Completions.Completion.Current: 'bg:#00aaaa #000000', t.Menu.Completions.Completion: 'bg:#008888 #ffffff', t.Menu.Completions.Meta.Current: 'bg:#00aaaa #000000', t.Menu.Completions.Meta: 'bg:#00aaaa #ffffff', t.Scrollbar.Button: 'bg:#003333', t.Scrollbar: 'bg:#00aaaa', t.Toolbar: 'bg:#222222 #cccccc', t.Toolbar.Off: 'bg:#222222 #696969', t.Toolbar.On: 'bg:#222222 #ffffff', t.Toolbar.Search: 'noinherit bold', t.Toolbar.Search.Text: 'nobold', t.Toolbar.System: 'noinherit bold', t.Toolbar.Arg: 'noinherit bold', t.Toolbar.Arg.Text: 'nobold' }) return style_from_dict(styles) aws-shell-0.2.1/awsshell/substring.py000066400000000000000000000020721335127342700176350ustar00rootroot00000000000000# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"). You # may not use this file except in compliance with the License. A copy of # the License is located at # # http://aws.amazon.com/apache2.0/ # # or in the "license" file accompanying this file. This file is # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. def substring_search(word, collection): """Find all matches in the `collection` for the specified `word`. If `word` is empty, returns all items in `collection`. :type word: str :param word: The substring to search for. :type collection: collection, usually a list :param collection: A collection of words to match. :rtype: list of strings :return: A sorted list of matching words from collection. """ return [item for item in sorted(collection) if item.startswith(word)] aws-shell-0.2.1/awsshell/toolbar.py000066400000000000000000000100541335127342700172560ustar00rootroot00000000000000# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"). You # may not use this file except in compliance with the License. A copy of # the License is located at # # http://aws.amazon.com/apache2.0/ # # or in the "license" file accompanying this file. This file is # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. from pygments.token import Token class Toolbar(object): """Show information about the aws-shell in a tool bar. :type handler: callable :param handler: Wraps the callable `get_toolbar_items`. """ def __init__(self, get_match_fuzzy, get_enable_vi_bindings, get_show_completion_columns, get_show_help): self.handler = self._create_toolbar_handler( get_match_fuzzy, get_enable_vi_bindings, get_show_completion_columns, get_show_help) def _create_toolbar_handler(self, get_match_fuzzy, get_enable_vi_bindings, get_show_completion_columns, get_show_help): """Create the toolbar handler. :type get_fuzzy_match: callable :param fuzzy_match: Gets the fuzzy matching config. :type get_enable_vi_bindings: callable :param get_enable_vi_bindings: Gets the vi (or emacs) key bindings config. :type get_show_completion_columns: callable :param get_show_completion_columns: Gets the show completions in multiple or single columns config. :type get_show_help: callable :param get_show_help: Gets the show help pane config. :rtype: callable :returns: get_toolbar_items. """ assert callable(get_match_fuzzy) assert callable(get_enable_vi_bindings) assert callable(get_show_completion_columns) assert callable(get_show_help) def get_toolbar_items(cli): """Return the toolbar items. :type cli: :class:`prompt_toolkit.Cli` :param cli: The command line interface from prompt_toolkit :rtype: list :return: A list of (pygments.Token.Toolbar, str). """ if get_match_fuzzy(): match_fuzzy_token = Token.Toolbar.On match_fuzzy_cfg = 'ON' else: match_fuzzy_token = Token.Toolbar.Off match_fuzzy_cfg = 'OFF' if get_enable_vi_bindings(): enable_vi_bindings_token = Token.Toolbar.On enable_vi_bindings_cfg = 'Vi' else: enable_vi_bindings_token = Token.Toolbar.On enable_vi_bindings_cfg = 'Emacs' if get_show_completion_columns(): show_columns_token = Token.Toolbar.On show_columns_cfg = 'Multi' else: show_columns_token = Token.Toolbar.On show_columns_cfg = 'Single' if get_show_help(): show_help_token = Token.Toolbar.On show_help_cfg = 'ON' else: show_help_token = Token.Toolbar.Off show_help_cfg = 'OFF' if cli.current_buffer_name == 'DEFAULT_BUFFER': show_buffer_name = 'cli' else: show_buffer_name = 'doc' return [ (match_fuzzy_token, ' [F2] Fuzzy: {0} '.format(match_fuzzy_cfg)), (enable_vi_bindings_token, ' [F3] Keys: {0} '.format(enable_vi_bindings_cfg)), (show_columns_token, ' [F4] {0} Column '.format(show_columns_cfg)), (show_help_token, ' [F5] Help: {0} '.format(show_help_cfg)), (Token.Toolbar, ' [F9] Focus: {0} '.format(show_buffer_name)), (Token.Toolbar, ' [F10] Exit ') ] return get_toolbar_items aws-shell-0.2.1/awsshell/ui.py000066400000000000000000000224371335127342700162410ustar00rootroot00000000000000from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER from prompt_toolkit.filters import IsDone, HasFocus, Always, \ RendererHeightIsKnown, to_cli_filter, Filter from prompt_toolkit.layout import Window, HSplit, VSplit, FloatContainer, Float from prompt_toolkit.layout.containers import ConditionalContainer from prompt_toolkit.layout.controls import BufferControl, \ TokenListControl, FillControl from prompt_toolkit.layout.dimension import LayoutDimension from prompt_toolkit.layout.menus import CompletionsMenu, \ MultiColumnCompletionsMenu from prompt_toolkit.layout.processors import PasswordProcessor, \ HighlightSearchProcessor, HighlightSelectionProcessor, \ ConditionalProcessor, AppendAutoSuggestion from prompt_toolkit.layout.prompt import DefaultPrompt from prompt_toolkit.layout.screen import Char from prompt_toolkit.layout.toolbars import ValidationToolbar, \ SystemToolbar, ArgToolbar, SearchToolbar from prompt_toolkit.layout.utils import explode_tokens from prompt_toolkit.layout.lexers import PygmentsLexer from pygments.token import Token from pygments.lexer import Lexer from awsshell.compat import text_type # This is borrowed from prompt_toolkit because we actually # need to mess with the layouts to get documentation pulled up. def create_default_layout(app, message='', lexer=None, is_password=False, reserve_space_for_menu=False, get_prompt_tokens=None, get_bottom_toolbar_tokens=None, display_completions_in_columns=False, extra_input_processors=None, multiline=False): """ Generate default layout. Returns a ``Layout`` instance. :param message: Text to be used as prompt. :param lexer: Lexer to be used for the highlighting. :param is_password: `bool` or `CLIFilter`. When True, display input as '*'. :param reserve_space_for_menu: When True, make sure that a minimal height is allocated in the terminal, in order to display the completion menu. :param get_prompt_tokens: An optional callable that returns the tokens to be shown in the menu. (To be used instead of a `message`.) :param get_bottom_toolbar_tokens: An optional callable that returns the tokens for a toolbar at the bottom. :param display_completions_in_columns: `bool` or `CLIFilter`. Display the completions in multiple columns. :param multiline: `bool` or `CLIFilter`. When True, prefer a layout that is more adapted for multiline input. Text after newlines is automatically indented, and search/arg input is shown below the input, instead of replacing the prompt. """ assert isinstance(message, text_type) assert (get_bottom_toolbar_tokens is None or callable(get_bottom_toolbar_tokens)) assert get_prompt_tokens is None or callable(get_prompt_tokens) assert not (message and get_prompt_tokens) display_completions_in_columns = to_cli_filter( display_completions_in_columns) multiline = to_cli_filter(multiline) if get_prompt_tokens is None: get_prompt_tokens = lambda _: [(Token.Prompt, message)] get_prompt_tokens_1, get_prompt_tokens_2 = _split_multiline_prompt( get_prompt_tokens) # `lexer` is supposed to be a `Lexer` instance. But if a Pygments lexer # class is given, turn it into a PygmentsLexer. (Important for # backwards-compatibility.) try: if issubclass(lexer, Lexer): lexer = PygmentsLexer(lexer) except TypeError: # Happens when lexer is `None` or an instance of something else. pass # Create processors list. # (DefaultPrompt should always be at the end.) input_processors = [ ConditionalProcessor( # By default, only highlight search when the search # input has the focus. (Note that this doesn't mean # there is no search: the Vi 'n' binding for instance # still allows to jump to the next match in # navigation mode.) HighlightSearchProcessor(preview_search=Always()), HasFocus(SEARCH_BUFFER)), HighlightSelectionProcessor(), ConditionalProcessor( AppendAutoSuggestion(), HasFocus(DEFAULT_BUFFER) & ~IsDone()), ConditionalProcessor(PasswordProcessor(), is_password) ] if extra_input_processors: input_processors.extend(extra_input_processors) # Show the prompt before the input (using the DefaultPrompt processor. # This also replaces it with reverse-i-search and 'arg' when required. # (Only for single line mode.) input_processors.append(ConditionalProcessor( DefaultPrompt(get_prompt_tokens), ~multiline)) # Create bottom toolbar. if get_bottom_toolbar_tokens: toolbars = [ConditionalContainer( Window(TokenListControl(get_bottom_toolbar_tokens, default_char=Char(' ', Token.Toolbar)), height=LayoutDimension.exact(1)), filter=~IsDone() & RendererHeightIsKnown())] else: toolbars = [] def get_height(cli): # If there is an autocompletion menu to be shown, make sure that our # layout has at least a minimal height in order to display it. if reserve_space_for_menu and not cli.is_done: return LayoutDimension(min=8) else: return LayoutDimension() def separator(): return ConditionalContainer( content=Window(height=LayoutDimension.exact(1), content=FillControl(u'\u2500', token=Token.Separator)), filter=HasDocumentation(app) & ~IsDone()) # Create and return Layout instance. return HSplit([ ConditionalContainer( Window( TokenListControl(get_prompt_tokens_1), dont_extend_height=True), filter=multiline, ), VSplit([ # In multiline mode, the prompt is displayed in a left pane. ConditionalContainer( Window( TokenListControl(get_prompt_tokens_2), dont_extend_width=True, ), filter=multiline, ), # The main input, with completion menus floating on top of it. FloatContainer( Window( BufferControl( input_processors=input_processors, lexer=lexer, # Enable preview_search, we want to have immediate # feedback in reverse-i-search mode. preview_search=Always(), focus_on_click=True, ), get_height=get_height, ), [ Float(xcursor=True, ycursor=True, content=CompletionsMenu( max_height=16, scroll_offset=1, extra_filter=(HasFocus(DEFAULT_BUFFER) & ~display_completions_in_columns))), Float(xcursor=True, ycursor=True, content=MultiColumnCompletionsMenu( extra_filter=(HasFocus(DEFAULT_BUFFER) & display_completions_in_columns), show_meta=Always())) ] ), ]), separator(), ConditionalContainer( content=Window( BufferControl( focus_on_click=True, buffer_name=u'clidocs', ), height=LayoutDimension(max=15)), filter=HasDocumentation(app) & ~IsDone(), ), separator(), ValidationToolbar(), SystemToolbar(), # In multiline mode, we use two toolbars for 'arg' and 'search'. ConditionalContainer(ArgToolbar(), multiline), ConditionalContainer(SearchToolbar(), multiline), ] + toolbars) def _split_multiline_prompt(get_prompt_tokens): """Split prompt tokens into two multiline prompt token functions. Take a `get_prompt_tokens` function. and return two new functions instead. One that returns the tokens to be shown on the lines above the input, and another one with the tokens to be shown at the first line of the input. """ def before(cli): result = [] found_nl = False for token, char in reversed(explode_tokens(get_prompt_tokens(cli))): if char == '\n': found_nl = True elif found_nl: result.insert(0, (token, char)) return result def first_input_line(cli): result = [] for token, char in reversed(explode_tokens(get_prompt_tokens(cli))): if char == '\n': break else: result.insert(0, (token, char)) return result return before, first_input_line class HasDocumentation(Filter): def __init__(self, app): self._app = app def __call__(self, cli): return bool(self._app.current_docs) aws-shell-0.2.1/awsshell/utils.py000066400000000000000000000056211335127342700167600ustar00rootroot00000000000000"""Utility module for misc aws shell functions.""" from __future__ import print_function import os import contextlib import tempfile import uuid import awscli from awsshell.compat import HTMLParser AWSCLI_VERSION = awscli.__version__ class FileReadError(Exception): pass def remove_html(html): s = DataOnly() s.feed(html) return s.get_data() def build_config_file_path(file_name): return os.path.join(os.path.expanduser('~'), '.aws', 'shell', file_name) @contextlib.contextmanager def temporary_file(mode): """Cross platform temporary file creation. This is an alternative to ``tempfile.NamedTemporaryFile`` that also works on windows and avoids the "file being used by another process" error. """ tempdir = tempfile.gettempdir() basename = 'tmpfile-%s' % (uuid.uuid4()) full_filename = os.path.join(tempdir, basename) if 'w' not in mode: # We need to create the file before we can open # it in 'r' mode. open(full_filename, 'w').close() try: with open(full_filename, mode) as f: yield f finally: os.remove(f.name) class DataOnly(HTMLParser): def __init__(self): # HTMLParser is an old-style class, which can't be used with super() HTMLParser.__init__(self) self.reset() self.lines = [] def handle_data(self, data): self.lines.append(data) def get_data(self): return ''.join(self.lines) class FSLayer(object): """Abstraction over common OS commands. Provides a simpler interface given the operations needed by the AWS Shell. """ def file_contents(self, filename, binary=False): """Return the file for a given filename. If you want binary content use ``mode='rb'``. """ if binary: mode = 'rb' else: mode = 'r' try: with open(filename, mode) as f: return f.read() except (OSError, IOError) as e: raise FileReadError(str(e)) def file_exists(self, filename): """Check if a file exists. This method returns true if: * The file exists. * The filename is a file (not a directory). """ return os.path.isfile(filename) class InMemoryFSLayer(object): """Same interface as FSLayer with an in memory implementation.""" def __init__(self, file_mapping): # path -> file_contents # file_contents are expected to be text, not # binary. self._file_mapping = file_mapping def file_contents(self, filename, binary=False): try: contents = self._file_mapping[filename] except KeyError: raise FileReadError(filename) if binary: contents = contents.encode('utf-8') return contents def file_exists(self, filename): return filename in self._file_mapping aws-shell-0.2.1/requirements-dev.txt000066400000000000000000000001161335127342700174560ustar00rootroot00000000000000doc8==0.6.0 flake8==2.5.0 pep257==0.7.0 pylint==1.7.2 -rrequirements-test.txt aws-shell-0.2.1/requirements-test.txt000066400000000000000000000003341335127342700176610ustar00rootroot00000000000000pytest==3.0.2 pytest-cov==2.3.1 mock==1.3.0 tox==2.2.1 configobj==5.0.6 # Note you need at least pip --version of 6.0 or # higher to be able to pick on these version specifiers. unittest2==1.1.0; python_version == '2.6' aws-shell-0.2.1/scripts/000077500000000000000000000000001335127342700151075ustar00rootroot00000000000000aws-shell-0.2.1/scripts/ci/000077500000000000000000000000001335127342700155025ustar00rootroot00000000000000aws-shell-0.2.1/scripts/ci/install000077500000000000000000000012611335127342700170760ustar00rootroot00000000000000#!/usr/bin/env python import os import sys from subprocess import check_call import shutil _dname = os.path.dirname REPO_ROOT = _dname(_dname(_dname(os.path.abspath(__file__)))) os.chdir(REPO_ROOT) def run(command): return check_call(command, shell=True) try: # Has the form "major.minor" python_version = os.environ['PYTHON_VERSION'] except KeyError: python_version = '.'.join([str(i) for i in sys.version_info[:2]]) run('pip install -r requirements-test.txt') if os.path.isdir('dist') and os.listdir('dist'): shutil.rmtree('dist') run('python setup.py bdist_wheel') wheel_dist = os.listdir('dist')[0] run('pip install %s' % (os.path.join('dist', wheel_dist))) aws-shell-0.2.1/scripts/ci/run-integ-tests000077500000000000000000000007611335127342700205040ustar00rootroot00000000000000#!/usr/bin/env python # Don't run tests from the root repo dir. # We want to ensure we're importing from the installed # binary package not from the CWD. import os from subprocess import check_call _dname = os.path.dirname REPO_ROOT = _dname(_dname(_dname(os.path.abspath(__file__)))) os.chdir(os.path.join(REPO_ROOT, 'tests')) def run(command): return check_call(command, shell=True) run('py.test --cov awsshell --junitxml=./pytests.xml --cov-report term-missing' ' integration/') aws-shell-0.2.1/scripts/ci/run-tests000077500000000000000000000007521335127342700174000ustar00rootroot00000000000000#!/usr/bin/env python # Don't run tests from the root repo dir. # We want to ensure we're importing from the installed # binary package not from the CWD. import os from subprocess import check_call _dname = os.path.dirname REPO_ROOT = _dname(_dname(_dname(os.path.abspath(__file__)))) os.chdir(os.path.join(REPO_ROOT, 'tests')) def run(command): return check_call(command, shell=True) run('py.test --cov awsshell --junitxml=./pytests.xml --cov-report term-missing' ' unit/') aws-shell-0.2.1/scripts/new-change000077500000000000000000000156051335127342700170600ustar00rootroot00000000000000#!/usr/bin/env python """Generate a new changelog entry. Usage ===== To generate a new changelog entry:: scripts/new-change This will open up a file in your editor (via the ``EDITOR`` env var). You'll see this template:: # Type should be one of: feature, bugfix type: # Category is the high level feature area. # This can be a service identifier (e.g ``s3``), # or something like: Paginator. category: # A brief description of the change. You can # use github style references to issues such as # "fixes #489", "boto/boto3#100", etc. These # will get automatically replaced with the correct # link. description: Fill in the appropriate values, save and exit the editor. Make sure to commit these changes as part of your pull request. If, when your editor is open, you decide don't don't want to add a changelog entry, save an empty file and no entry will be generated. You can then use the ``scripts/render-change`` to generate the CHANGELOG.rst file. """ import os import re import sys import json import string import random import tempfile import subprocess import argparse VALID_CHARS = set(string.ascii_letters + string.digits) CHANGES_DIR = os.path.join( os.path.dirname(os.path.dirname(os.path.abspath(__file__))), '.changes' ) TEMPLATE = """\ # Type should be one of: feature, bugfix, enhancement, api-change # feature: A larger feature or change in behavior, usually resulting in a # minor version bump. # bugfix: Fixing a bug in an existing code path. # enhancment: Small change to an underlying implementation detail. # api-change: Changes to a modeled API. type: {change_type} # Category is the high level feature area. # This can be a service identifier (e.g ``s3``), # or something like: Paginator. category: {category} # A brief description of the change. You can # use github style references to issues such as # "fixes #489", "boto/boto3#100", etc. These # will get automatically replaced with the correct # link. description: {description} """ def new_changelog_entry(args): # Changelog values come from one of two places. # Either all values are provided on the command line, # or we open a text editor and let the user provide # enter their values. if all_values_provided(args): parsed_values = { 'type': args.change_type, 'category': args.category, 'description': args.description, } else: parsed_values = get_values_from_editor(args) if has_empty_values(parsed_values): sys.stderr.write( "Empty changelog values received, skipping entry creation.\n") return 1 replace_issue_references(parsed_values, args.repo) write_new_change(parsed_values) return 0 def has_empty_values(parsed_values): return not (parsed_values.get('type') and parsed_values.get('category') and parsed_values.get('description')) def all_values_provided(args): return args.change_type and args.category and args.description def get_values_from_editor(args): with tempfile.NamedTemporaryFile('w') as f: contents = TEMPLATE.format( change_type=args.change_type, category=args.category, description=args.description, ) f.write(contents) f.flush() env = os.environ editor = env.get('VISUAL', env.get('EDITOR', 'vim')) p = subprocess.Popen('%s %s' % (editor, f.name), shell=True) p.communicate() with open(f.name) as f: filled_in_contents = f.read() parsed_values = parse_filled_in_contents(filled_in_contents) return parsed_values def replace_issue_references(parsed, repo_name): description = parsed['description'] def linkify(match): number = match.group()[1:] return ( '`%s `__' % ( match.group(), repo_name, number)) new_description = re.sub('#\d+', linkify, description) parsed['description'] = new_description def write_new_change(parsed_values): if not os.path.isdir(CHANGES_DIR): os.makedirs(CHANGES_DIR) # Assume that new changes go into the next release. dirname = os.path.join(CHANGES_DIR, 'next-release') if not os.path.isdir(dirname): os.makedirs(dirname) # Need to generate a unique filename for this change. # We'll try a couple things until we get a unique match. category = parsed_values['category'] short_summary = ''.join(filter(lambda x: x in VALID_CHARS, category)) filename = '{type_name}-{summary}'.format( type_name=parsed_values['type'], summary=short_summary) possible_filename = os.path.join( dirname, '%s-%s.json' % (filename, str(random.randint(1, 100000)))) while os.path.isfile(possible_filename): possible_filename = os.path.join( dirname, '%s-%s.json' % (filename, str(random.randint(1, 100000)))) with open(possible_filename, 'w') as f: f.write(json.dumps(parsed_values, indent=2) + "\n") def parse_filled_in_contents(contents): """Parse filled in file contents and returns parsed dict. Return value will be:: { "type": "bugfix", "category": "category", "description": "This is a description" } """ if not contents.strip(): return {} parsed = {} lines = iter(contents.splitlines()) for line in lines: line = line.strip() if line.startswith('#'): continue if 'type' not in parsed and line.startswith('type:'): parsed['type'] = line.split(':')[1].strip() elif 'category' not in parsed and line.startswith('category:'): parsed['category'] = line.split(':')[1].strip() elif 'description' not in parsed and line.startswith('description:'): # Assume that everything until the end of the file is part # of the description, so we can break once we pull in the # remaining lines. first_line = line.split(':')[1].strip() full_description = '\n'.join([first_line] + list(lines)) parsed['description'] = full_description.strip() break return parsed def main(): parser = argparse.ArgumentParser() parser.add_argument('-t', '--type', dest='change_type', default='', choices=('bugfix', 'feature', 'enhancement', 'api-change')) parser.add_argument('-c', '--category', dest='category', default='') parser.add_argument('-d', '--description', dest='description', default='') parser.add_argument('-r', '--repo', default='awslabs/aws-shell', help='Optional repo name, e.g: awslabs/aws-shell') args = parser.parse_args() sys.exit(new_changelog_entry(args)) if __name__ == '__main__': main() aws-shell-0.2.1/setup.cfg000066400000000000000000000000341335127342700152360ustar00rootroot00000000000000[bdist_wheel] universal = 1 aws-shell-0.2.1/setup.py000066400000000000000000000032671335127342700151420ustar00rootroot00000000000000#!/usr/bin/env python import re import ast from setuptools import setup, find_packages requires = [ 'awscli>=1.16.10,<2.0.0', 'prompt-toolkit>=1.0.0,<1.1.0', 'boto3>=1.9.0,<2.0.0', 'configobj>=5.0.6,<6.0.0', 'Pygments>=2.1.3,<3.0.0', ] with open('awsshell/__init__.py', 'r') as f: version = str( ast.literal_eval( re.search( r'__version__\s+=\s+(.*)', f.read()).group(1))) setup( name='aws-shell', version=version, description='AWS Shell', long_description=open('README.rst').read(), author='James Saryerwinnie', url='https://github.com/awslabs/aws-shell', packages=find_packages(exclude=['tests*']), include_package_data=True, package_data={'awsshell': ['data/*/*.json', 'awsshellrc']}, install_requires=requires, entry_points={ 'console_scripts': [ 'aws-shell = awsshell:main', 'aws-shell-mkindex = awsshell.makeindex:main', ] }, license="Apache License 2.0", classifiers=( 'Development Status :: 3 - Alpha', 'Intended Audience :: Developers', 'Intended Audience :: System Administrators', 'Natural Language :: English', 'License :: OSI Approved :: Apache Software License', 'Programming Language :: Python', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', ), ) aws-shell-0.2.1/tests/000077500000000000000000000000001335127342700145625ustar00rootroot00000000000000aws-shell-0.2.1/tests/__init__.py000066400000000000000000000001161335127342700166710ustar00rootroot00000000000000try: import unittest2 as unittest except ImportError: import unittest aws-shell-0.2.1/tests/compat.py000066400000000000000000000012321335127342700164150ustar00rootroot00000000000000# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"). You # may not use this file except in compliance with the License. A copy of # the License is located at # # http://aws.amazon.com/apache2.0/ # # or in the "license" file accompanying this file. This file is # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. import sys if sys.version_info < (2, 7): import unittest2 as unittest else: import unittest aws-shell-0.2.1/tests/integration/000077500000000000000000000000001335127342700171055ustar00rootroot00000000000000aws-shell-0.2.1/tests/integration/__init__.py000066400000000000000000000000001335127342700212040ustar00rootroot00000000000000aws-shell-0.2.1/tests/integration/test_config.py000066400000000000000000000045361335127342700217730ustar00rootroot00000000000000# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"). You # may not use this file except in compliance with the License. A copy of # the License is located at # # http://aws.amazon.com/apache2.0/ # # or in the "license" file accompanying this file. This file is # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. import mock import os import unittest from awsshell.app import AWSShell from awsshell.config import Config from awsshell.utils import build_config_file_path class ConfigTest(unittest.TestCase): def test_config_off(self): try: os.remove(build_config_file_path('test-awsshellrc')) except OSError: pass self.aws_shell = AWSShell(None, mock.Mock(), mock.Mock()) self.aws_shell.model_completer.match_fuzzy = False self.aws_shell.enable_vi_bindings = False self.aws_shell.show_completion_columns = False self.aws_shell.show_help = False self.aws_shell.theme = 'none' self.aws_shell.save_config() self.aws_shell.load_config() assert self.aws_shell.model_completer.match_fuzzy == False assert self.aws_shell.enable_vi_bindings == False assert self.aws_shell.show_completion_columns == False assert self.aws_shell.show_help == False assert self.aws_shell.theme == 'none' def test_config_on(self): self.aws_shell = AWSShell(None, mock.Mock(), mock.Mock()) self.aws_shell.model_completer.match_fuzzy = True self.aws_shell.enable_vi_bindings = True self.aws_shell.show_completion_columns = True self.aws_shell.show_help = True self.aws_shell.theme = 'vim' self.aws_shell.save_config() self.aws_shell.load_config() assert self.aws_shell.config_section.as_bool('match_fuzzy') == True assert self.aws_shell.config_section.as_bool( 'enable_vi_bindings') == True assert self.aws_shell.config_section.as_bool( 'show_completion_columns') == True assert self.aws_shell.config_section.as_bool('show_help') == True assert self.aws_shell.config_section['theme'] == 'vim' aws-shell-0.2.1/tests/integration/test_db.py000066400000000000000000000024321335127342700211040ustar00rootroot00000000000000from awsshell import db import pytest @pytest.fixture def shell_db(tmpdir): filename = tmpdir.join('docs.db').strpath d = db.ConcurrentDBM.create(filename) return d def test_can_get_and_set_value(shell_db): shell_db['foo'] = 'bar' assert shell_db['foo'] == 'bar' def test_raise_key_error_when_no_key_exists(shell_db): with pytest.raises(KeyError) as e: shell_db['foo'] assert 'foo' in str(e.value) def test_can_set_multiple_values(shell_db): shell_db['foo'] = 'a' shell_db['bar'] = 'b' assert shell_db['foo'] == 'a' assert shell_db['bar'] == 'b' def test_can_change_existing_value(shell_db): shell_db['foo'] = 'first' shell_db['foo'] = 'second' assert shell_db['foo'] == 'second' def test_can_update_multiple_times(shell_db): for i in range(100): shell_db['foo'] = str(i) assert shell_db['foo'] == '99' def test_can_handle_unicode(shell_db): shell_db['foo'] = u'\u2713' assert shell_db['foo'] == u'\u2713' def test_can_create_and_open_db(tmpdir): filename = tmpdir.join('foo.db').strpath d = db.ConcurrentDBM.create(filename) d['foo'] = 'bar' d.close() # Should be able to reopen the database and look up 'foo'. d = db.ConcurrentDBM.open(filename) assert d['foo'] == 'bar' aws-shell-0.2.1/tests/integration/test_keys.py000066400000000000000000000047671335127342700215070ustar00rootroot00000000000000# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"). You # may not use this file except in compliance with the License. A copy of # the License is located at # # http://aws.amazon.com/apache2.0/ # # or in the "license" file accompanying this file. This file is # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. import mock from prompt_toolkit.input import PipeInput from prompt_toolkit.output import DummyOutput from prompt_toolkit.key_binding.input_processor import KeyPress from prompt_toolkit.keys import Keys from tests.compat import unittest from awsshell.app import AWSShell, InputInterrupt class KeysTest(unittest.TestCase): def setUp(self): self.input = PipeInput() output = DummyOutput() self.aws_shell = AWSShell(None, mock.Mock(), mock.Mock(), input=self.input, output=output) self.processor = self.aws_shell.cli.input_processor def tearDown(self): self.input.close() def feed_key(self, key): self.processor.feed(KeyPress(key, u'')) self.processor.process_keys() def test_F2(self): match_fuzzy = self.aws_shell.model_completer.match_fuzzy self.feed_key(Keys.F2) assert match_fuzzy != self.aws_shell.model_completer.match_fuzzy def test_F3(self): enable_vi_bindings = self.aws_shell.enable_vi_bindings with self.assertRaises(InputInterrupt): self.feed_key(Keys.F3) assert enable_vi_bindings != self.aws_shell.enable_vi_bindings def test_F4(self): show_completion_columns = self.aws_shell.show_completion_columns with self.assertRaises(InputInterrupt): self.feed_key(Keys.F4) assert show_completion_columns != \ self.aws_shell.show_completion_columns def test_F5(self): show_help = self.aws_shell.show_help with self.assertRaises(InputInterrupt): self.feed_key(Keys.F5) assert show_help != self.aws_shell.show_help def test_F9(self): assert self.aws_shell.cli.current_buffer_name == u'DEFAULT_BUFFER' self.feed_key(Keys.F9) assert self.aws_shell.cli.current_buffer_name == u'clidocs' def test_F10(self): self.feed_key(Keys.F10) assert self.aws_shell.cli.is_exiting aws-shell-0.2.1/tests/integration/test_makeindex.py000066400000000000000000000035631335127342700224720ustar00rootroot00000000000000import awscli.clidriver from awsshell import makeindex import pytest @pytest.fixture def cloudformation_command(): driver = awscli.clidriver.create_clidriver() cmd = driver.create_help_command() cfn = cmd.command_table['cloudformation'] return cfn def test_can_write_doc_index_for_single_operation(cloudformation_command): # We don't want to try to generate the entire doc index # for all commands. We're just trying to ensure we're # integrating with the AWS CLi's help commands properly # so we're going to pick a single operation to document. create_stack = cloudformation_command.create_help_command()\ .command_table['create-stack'] help_command = create_stack.create_help_command() rendered = makeindex.render_docs_for_cmd(help_command=help_command) # We *really* don't want these to fail when the wording # changes so I'm purposefully not picking long phrases. assert 'Creates a stack' in rendered # Should also see sections in the rendered content. assert 'SYNOPSIS' in rendered assert 'EXAMPLES' in rendered assert 'OUTPUT' in rendered # Should also see a parameter. assert '--stack-name' in rendered def test_can_document_all_service_commands(cloudformation_command): db = {} help_command = cloudformation_command.create_help_command() makeindex.write_doc_index(db=db, help_command=help_command) # Again, we don't want these to fail when cloudformation has # API updates so I don't have very strict checking. assert 'aws.cloudformation.create-stack' in db assert 'aws.cloudformation.delete-stack' in db assert 'SYNOPSIS' in db['aws.cloudformation.create-stack'] def test_can_index_a_command(cloudformation_command): help_command = cloudformation_command.create_help_command() index = makeindex.new_index() makeindex.index_command(index, help_command) aws-shell-0.2.1/tests/unit/000077500000000000000000000000001335127342700155415ustar00rootroot00000000000000aws-shell-0.2.1/tests/unit/__init__.py000066400000000000000000000000001335127342700176400ustar00rootroot00000000000000aws-shell-0.2.1/tests/unit/index/000077500000000000000000000000001335127342700166505ustar00rootroot00000000000000aws-shell-0.2.1/tests/unit/index/__init__.py000066400000000000000000000000001335127342700207470ustar00rootroot00000000000000aws-shell-0.2.1/tests/unit/index/test_completions.py000066400000000000000000000017231335127342700226200ustar00rootroot00000000000000from tests import unittest from awsshell.index import completion from awsshell.utils import InMemoryFSLayer class TestCompletionIndex(unittest.TestCase): def setUp(self): # filename -> file content self.files = {} self.fslayer = InMemoryFSLayer(self.files) def test_can_load_index(self): c = completion.CompletionIndex(cache_dir='/tmp/cache', fslayer=self.fslayer) self.files['/tmp/cache/completions-1.9.1.json'] = '{}' try: c.load_index('1.9.1') except completion.IndexLoadError as e: self.fail("Expected to load index for '1.9.1', " "but was unable.") def test_index_does_not_exist_raises_error(self): c = completion.CompletionIndex(cache_dir='/tmp/cache', fslayer=self.fslayer) with self.assertRaises(completion.IndexLoadError): c.load_index('1.9.1') aws-shell-0.2.1/tests/unit/test_app.py000066400000000000000000000134671335127342700177450ustar00rootroot00000000000000import pytest import mock from awsshell import app from awsshell import shellcomplete from awsshell import compat @pytest.fixture def errstream(): return compat.StringIO() def test_can_dispatch_dot_commands(): call_args = [] class CustomHandler(object): def run(self, command, context): call_args.append((command, context)) handler = app.DotCommandHandler() handler.HANDLER_CLASSES['foo'] = CustomHandler context = object() handler.handle_cmd('.foo a b c', context) assert call_args == [(['.foo', 'a', 'b', 'c'], context)] class PopenLogger(object): def __call__(self, cmd): self.cmd = cmd filename = cmd[1] with open(filename, 'r') as f: self.contents = f.read() return mock.Mock() def test_edit_handler(): env = {'EDITOR': 'my-editor'} popen_cls = mock.Mock() application = mock.Mock() application.history = [ '!ls', '.edit', 'aws ec2 describe-instances', 'aws ec2 allocate-hosts', ] popen = PopenLogger() handler = app.EditHandler(popen, env) handler.run(['.edit'], application) # Ensure our editor was called with some arbitrary temp filename. command_run = popen.cmd assert len(command_run) == 2 assert command_run[0] == 'my-editor' # Ensure the contents of the temp file are correct expected_contents = 'aws ec2 describe-instances\naws ec2 allocate-hosts' assert popen.contents == expected_contents def test_error_msg_printed_on_error_handler(errstream): env = {'EDITOR': 'my-editor'} popen_cls = mock.Mock() popen_cls.side_effect = OSError() context = mock.Mock() context.history = [] handler = app.EditHandler(popen_cls, env, errstream) handler.run(['.edit'], context) # Then we should not propagate an exception, and we # should print a helpful error message. assert 'Unable to launch editor: my-editor' in errstream.getvalue() def test_profile_handler_prints_profile(): shell = mock.Mock(spec=app.AWSShell) shell.profile = 'myprofile' stdout = compat.StringIO() handler = app.ProfileHandler(stdout) handler.run(['.profile'], shell) assert stdout.getvalue().strip() == 'Current shell profile: myprofile' def test_profile_handler_when_no_profile_configured(): shell = mock.Mock(spec=app.AWSShell) shell.profile = None stdout = compat.StringIO() handler = app.ProfileHandler(stdout) handler.run(['.profile'], shell) assert stdout.getvalue() == ( 'Current shell profile: no profile configured\n' 'You can change profiles using: .profile profile-name\n' ) def test_profile_command_changes_profile(): shell = mock.Mock(spec=app.AWSShell) shell.profile = 'myprofile' stdout = compat.StringIO() handler = app.ProfileHandler(stdout) handler.run(['.profile', 'newprofile'], shell) assert shell.profile == 'newprofile' def test_profile_prints_error_on_bad_syntax(): stderr = compat.StringIO() handler = app.ProfileHandler(None, stderr) handler.run(['.profile', 'a', 'b', 'c'], None) # We don't really care about the exact usage message here, # we just want to ensure usage was written to stderr. assert 'Usage' in stderr.getvalue() def test_prints_error_message_on_unknown_dot_command(errstream): handler = app.DotCommandHandler(err=errstream) handler.handle_cmd(".unknown foo bar", None) assert errstream.getvalue() == "Unknown dot command: .unknown\n" def test_delegates_to_complete_changing_profile(): completer = mock.Mock(spec=shellcomplete.AWSShellCompleter) shell = app.AWSShell(completer, mock.Mock(), mock.Mock()) shell.profile = 'mynewprofile' assert completer.change_profile.call_args == mock.call('mynewprofile') assert shell.profile == 'mynewprofile' def test_cd_handler_can_chdir(): chdir = mock.Mock() handler = app.ChangeDirHandler(chdir=chdir) handler.run(['.cd', 'foo/bar'], None) assert chdir.call_args == mock.call('foo/bar') def test_chdir_syntax_error_prints_err_msg(errstream): chdir = mock.Mock() handler = app.ChangeDirHandler(err=errstream, chdir=chdir) handler.run(['.cd'], None) assert 'invalid syntax' in errstream.getvalue() assert not chdir.called def test_error_displayed_when_chdir_fails(errstream): chdir = mock.Mock() chdir.side_effect = OSError("FAILED") handler = app.ChangeDirHandler(err=errstream, chdir=chdir) handler.run(['.cd', 'foo'], None) assert 'FAILED' in errstream.getvalue() def test_history_stored_correctly(): mock_prompter = mock.Mock() mock_prompter.buffers = {'clidocs': mock.Mock()} # Simulate the user entering various commands quit_document = mock.Mock() quit_document.text = '.quit' command_document = mock.Mock() command_document.text = 'ec2 describe-instances' mock_prompter.run.side_effect = [command_document, quit_document] shell = app.AWSShell(mock.Mock(), mock.Mock(), mock.Mock(), popen_cls=mock.Mock()) shell.create_cli_interface = mock.Mock(return_value=mock_prompter) shell.run() # two calls should have been made, history should have added aws assert mock_prompter.run.call_count == 2 assert list(shell.history) == ['aws ec2 describe-instances'] def test_exit_dot_command_exits_shell(): mock_prompter = mock.Mock() # Simulate the user entering '.quit' fake_document = mock.Mock() fake_document.text = '.quit' mock_prompter.run.return_value = fake_document shell = app.AWSShell(mock.Mock(), mock.Mock(), mock.Mock()) shell.create_cli_interface = mock.Mock(return_value=mock_prompter) shell.run() # Should have only called run() once. As soon as we # see the .quit command, we immediately exit and stop prompting # for more shell commands. assert mock_prompter.run.call_count == 1 aws-shell-0.2.1/tests/unit/test_autocomplete.py000066400000000000000000000313641335127342700216620ustar00rootroot00000000000000import pytest from awsshell.autocomplete import AWSCLIModelCompleter @pytest.fixture def index_data(): return { 'aws': { 'argument_metadata': {}, 'arguments': [], 'commands': [], 'children': {}, } } def test_completes_service_names(index_data): index_data['aws']['commands'] = ['first', 'second'] completer = AWSCLIModelCompleter(index_data) assert completer.autocomplete('fi') == ['first'] def test_completes_service_names_substring(index_data): index_data['aws']['commands'] = ['foo', 'bar foo'] completer = AWSCLIModelCompleter(index_data) completer.match_fuzzy = False assert completer.autocomplete('fo') == ['foo'] def test_completes_multiple_service_names(index_data): index_data['aws']['commands'] = ['abc', 'acd', 'b'] completer = AWSCLIModelCompleter(index_data) assert completer.autocomplete('a') == ['abc', 'acd'] def test_no_completion(index_data): index_data['aws']['commands'] = ['foo', 'bar'] completer = AWSCLIModelCompleter(index_data) assert completer.autocomplete('baz') == [] def test_can_complete_subcommands(index_data): index_data['aws']['commands'] = ['ec2'] index_data['aws']['children'] = { 'ec2': { 'arguments': [], 'commands': ['copy-image', 'copy-snapshot', 'other'], 'children': {}, } } completer = AWSCLIModelCompleter(index_data) # The completer tracks state to optimize lookups, # so we simulate exactly how it's called. completer.autocomplete('e') completer.autocomplete('ec') completer.autocomplete('ec2') completer.autocomplete('ec2 ') completer.autocomplete('ec2 c') completer.autocomplete('ec2 co') assert completer.autocomplete('ec2 cop') == ['copy-image', 'copy-snapshot'] def test_everything_completed_on_space(index_data): # Right after "aws ec2" all the operations should be # autocompleted. index_data['aws']['commands'] = ['ec2'] index_data['aws']['children'] = { 'ec2': { 'arguments': [], 'commands': ['copy-image', 'copy-snapshot', 'other'], 'children': {}, } } completer = AWSCLIModelCompleter(index_data) completer.autocomplete('e') completer.autocomplete('ec') completer.autocomplete('ec2') assert completer.autocomplete('ec2 ') == ['copy-image', 'copy-snapshot', 'other'] def test_autocomplete_top_leve_services_on_space(index_data): index_data['aws']['commands'] = ['first', 'second'] completer = AWSCLIModelCompleter(index_data) assert completer.autocomplete(' ') == ['first', 'second'] def test_reset_auto_complete(index_data): index_data['aws']['commands'] = ['first', 'second'] completer = AWSCLIModelCompleter(index_data) completer.autocomplete('f') completer.autocomplete('fi') completer.autocomplete('fir') # Then the user hits enter. # Now they've moved on to the next command. assert completer.autocomplete('d') == ['second'] def test_reset_after_subcommand_completion(index_data): index_data['aws']['commands'] = ['ec2', 's3'] index_data['aws']['children'] = { 'ec2': { 'arguments': [], 'commands': ['copy-image', 'copy-snapshot', 'other'], 'children': {}, } } completer = AWSCLIModelCompleter(index_data) # The completer tracks state to optimize lookups, # so we simulate exactly how it's called. completer.autocomplete('e') completer.autocomplete('ec') completer.autocomplete('ec2') completer.autocomplete('ec2 ') completer.autocomplete('ec2 c') completer.autocomplete('ec2 co') # The user hits enter and auto completes copy-snapshot. # The next request should be to auto complete # top level commands: assert completer.autocomplete('s') == ['s3'] def test_backspace_should_complete_previous_command(index_data): pass def test_can_handle_entire_word_deleted(index_data): pass def test_can_handle_entire_line_deleted(index_data): index_data['aws']['commands'] = ['ec2', 's3'] index_data['aws']['children'] = { 'ec2': { 'arguments': [], 'commands': ['copy-image', 'copy-snapshot', 'other'], 'children': {}, } } completer = AWSCLIModelCompleter(index_data) c = completer.autocomplete c('e') c('ec') c('ec2') c('ec2 ') c('ec2 c') c('ec2 co') # Use hits backspace a few times. c('ec2 c') c('ec2 ') c('ec2') # Now we should be auto completing 'ec2' assert c('ec') == ['ec2'] def test_autocompletes_argument_names(index_data): index_data['aws']['arguments'] = ['--query', '--debug'] completer = AWSCLIModelCompleter(index_data) # These should only appear once in the output. So we need # to know if we're a top level argument or not. assert completer.autocomplete('-') == ['--query', '--debug'] assert completer.autocomplete('--q') == ['--query'] def test_autocompletes_argument_names_substring(index_data): index_data['aws']['arguments'] = ['--foo', '--bar foo'] completer = AWSCLIModelCompleter(index_data) completer.match_fuzzy = False # These should only appear once in the output. So we need # to know if we're a top level argument or not. assert completer.autocomplete('--f') == ['--foo'] def test_autocompletes_global_and_service_args(index_data): index_data['aws']['arguments'] = ['--query', '--debug'] index_data['aws']['commands'] = ['ec2'] index_data['aws']['children'] = { 'ec2': { 'arguments': ['--query-ec2', '--instance-id'], 'commands': [], 'children': {}, } } completer = AWSCLIModelCompleter(index_data) c = completer.autocomplete c('e') c('ec') c('ec2') c('ec2 ') c('ec2 -') c('ec2 --') assert c('ec2 --q') == ['--query', '--query-ec2'] def test_can_mix_options_and_commands(index_data): index_data['aws']['arguments'] = ['--no-validate-ssl'] index_data['aws']['commands'] = ['ec2'] index_data['aws']['children'] = { 'ec2': { 'argument_metadata': {}, 'arguments': ['--query-ec2', '--instance-id'], 'commands': ['create-tags', 'describe-instances'], 'children': {}, } } completer = AWSCLIModelCompleter(index_data) c = completer.autocomplete partial_cmd = 'ec2 --no-validate-ssl' for i in range(1, len(partial_cmd)): c(partial_cmd[:i]) assert c('ec2 --no-validate-ssl ') == ['create-tags', 'describe-instances'] c('ec2 --no-validate-ssl c') c('ec2 --no-validate-ssl cr') c('ec2 --no-validate-ssl cre') c('ec2 --no-validate-ssl crea') assert c('ec2 --no-validate-ssl creat') == ['create-tags'] def test_only_change_context_when_in_index(index_data): index_data['aws']['arguments'] = ['--region'] index_data['aws']['commands'] = ['ec2'] index_data['aws']['children'] = { 'ec2': { 'commands': ['create-tags', 'describe-instances'], 'children': {}, 'argument_metadata': {}, 'arguments': [], } } completer = AWSCLIModelCompleter(index_data) c = completer.autocomplete partial_cmd = 'ec2 --region us-west-2' for i in range(1, len(partial_cmd)): c(partial_cmd[:i]) # We should ignore "us-west-2" because it's not a child # of ec2. assert c('ec2 --region us-west-2 ') == ['create-tags', 'describe-instances'] def test_can_handle_skips_in_completion(index_data): # Normally, completion is always requested char by char. # Typing "ec2 describe-inst" # will subsequent calls to the autocompleter: # 'e', 'ec', 'ec2', 'ec2 ', 'ec2 d', 'ec2 de' ... all the way # up to 'ec2 describe-inst'. # However, the autocompleter should gracefully handle when there's # skips, so two subsequent calls are 'ec' and then 'ec2 describe-ta', # the autocompleter should still do the right thing. The tradeoff # will just be that this case will be slower than the common case # of char by char additions. index_data['aws']['commands'] = ['ec2'] index_data['aws']['children'] = { 'ec2': { 'commands': ['create-tags', 'describe-instances'], 'argument_metadata': {}, 'arguments': [], 'children': { 'create-tags': { 'argument_metadata': { '--resources': {'example': '', 'minidoc': 'foo'}, '--tags': {'example': 'bar', 'minidoc': 'baz'}, }, 'arguments': ['--resources', '--tags'], } }, } } completer = AWSCLIModelCompleter(index_data) c = completer.autocomplete result = c('ec2 create-ta') assert result == ['create-tags'] def test_cmd_path_updated_on_completions(index_data): index_data['aws']['commands'] = ['ec2'] index_data['aws']['children'] = { 'ec2': { 'commands': ['create-tags', 'describe-instances'], 'argument_metadata': {}, 'arguments': [], 'children': { 'create-tags': { 'commands': [], 'argument_metadata': { '--resources': {'example': '', 'minidoc': 'foo'}, '--tags': {'example': 'bar', 'minidoc': 'baz'}, }, 'arguments': ['--resources', '--tags'], } } } } completer = AWSCLIModelCompleter(index_data) c = completer.autocomplete result = c('ec2 create-tags ') assert result == [] assert completer.cmd_path == ['aws', 'ec2', 'create-tags'] assert completer.arg_metadata == { '--resources': {'example': '', 'minidoc': 'foo'}, '--tags': {'example': 'bar', 'minidoc': 'baz'}, } def test_last_option_updated_up_releated_api_params(index_data): index_data['aws']['commands'] = ['ec2'] index_data['aws']['children'] = { 'ec2': { 'commands': ['create-tags'], 'argument_metadata': {}, 'arguments': [], 'children': { 'create-tags': { 'commands': [], 'argument_metadata': { '--resources': {'example': '', 'minidoc': 'foo'}, '--tags': {'example': 'bar', 'minidoc': 'baz'}, }, 'arguments': ['--resources', '--tags'], 'children': {}, } } } } completer = AWSCLIModelCompleter(index_data) completer.autocomplete('ec2 create-tags --resources ') assert completer.last_option == '--resources' completer.autocomplete('ec2 create-tags --resources f --tags ') # last_option should be updated. assert completer.last_option == '--tags' def test_last_option_is_updated_on_global_options(index_data): index_data['aws']['arguments'] = ['--no-sign-request'] index_data['aws']['commands'] = ['ec2'] index_data['aws']['children'] = { 'ec2': { 'commands': ['create-tags'], 'argument_metadata': {}, 'arguments': [], 'children': { 'create-tags': { 'commands': [], 'argument_metadata': { '--resources': {'example': '', 'minidoc': 'foo'}, }, 'arguments': ['--resources'], 'children': {}, } } } } completer = AWSCLIModelCompleter(index_data) completer.autocomplete('ec2 create-tags --resources ') assert completer.last_option == '--resources' completer.autocomplete('ec2 create-tags --resources f --no-sign-request ') assert completer.last_option == '--no-sign-request' def test_can_handle_autocompleting_same_string_twice(index_data): index_data['aws']['commands'] = ['first', 'second'] completer = AWSCLIModelCompleter(index_data) completer.autocomplete('f') assert completer.autocomplete('f') == ['first'] def test_can_handle_autocomplete_empty_string_twice(index_data): # Sometimes prompt_toolkit will try to autocomplete # the empty string multiple times. We need to handle this # gracefully. index_data['aws']['commands'] = ['first', 'second'] completer = AWSCLIModelCompleter(index_data) assert completer.autocomplete('') == [] assert completer.autocomplete('') == [] def test_global_arg_metadata_property(index_data): index_data['aws']['argument_metadata'] = { '--global1': {}, '--global2': {}, } completer = AWSCLIModelCompleter(index_data) assert '--global1' in completer.global_arg_metadata aws-shell-0.2.1/tests/unit/test_docs.py000066400000000000000000000006061335127342700201040ustar00rootroot00000000000000from awsshell import docs from awsshell import db def test_lazy_doc_factory(tmpdir): filename = tmpdir.join('foo.db').strpath doc_index = docs.load_lazy_doc_index(filename) assert isinstance(doc_index, docs.DocRetriever) def test_load_doc_db(tmpdir): filename = tmpdir.join("foo.db").strpath d = docs.load_doc_db(filename) assert isinstance(d, db.ConcurrentDBM) aws-shell-0.2.1/tests/unit/test_fuzzy.py000066400000000000000000000011651335127342700203440ustar00rootroot00000000000000import pytest from awsshell.fuzzy import fuzzy_search @pytest.mark.parametrize("search,corpus,expected", [ ('foo', ['foobar', 'foobaz'], ['foobar', 'foobaz']), ('f', ['foo', 'foobar', 'bar'], ['foo', 'foobar']), ('fbb', ['foo-bar-baz', 'fo-ba-baz', 'bar'], ['foo-bar-baz', 'fo-ba-baz']), ('fff', ['fi-fi-fi', 'fo'], ['fi-fi-fi']), # The more chars it matches, the higher the score. ('pre', ['prefix', 'pre', 'not'], ['pre', 'prefix']), ('nomatch', ['noma', 'nomatccc'], []), ]) def test_subsequences(search, corpus, expected): actual = fuzzy_search(search, corpus) assert actual == expected aws-shell-0.2.1/tests/unit/test_load_completions.py000066400000000000000000000036201335127342700225060ustar00rootroot00000000000000# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"). You # may not use this file except in compliance with the License. A copy of # the License is located at # # http://aws.amazon.com/apache2.0/ # # or in the "license" file accompanying this file. This file is # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. import unittest from awsshell.index.completion import CompletionIndex class LoadCompletionsTest(unittest.TestCase): def setUp(self): self.completion_index = CompletionIndex() # This would probably be cleaner with a pytest.fixture like # test_completions.index_data DATA = ( '{"aws": ' '{"commands": ["devicefarm", "foo"], ' '"arguments": ["--debug", "--endpoint-url"], ' '"children": {"devicefarm": ' '{"commands": ["create-device-pool"], ' '"children": {"create-device-pool": ' '{"commands": [], ' '"arguments": ["--project-arn", "--name"]}}}, ' '"foo": ' '{"commands": ["bar"], ' '"children": {"bar": ' '{"commands": [], "arguments": ["--baz"]}}}}}}' ) self.completion_index.load_index = lambda x: DATA self.completion_index.load_completions() def test_load_completions(self): assert self.completion_index.commands == [ 'devicefarm', 'foo'] assert self.completion_index.subcommands == [ 'create-device-pool', 'bar'] assert self.completion_index.global_opts == [ '--debug', '--endpoint-url'] assert self.completion_index.args_opts == set([ '--project-arn', '--name', '--baz']) aws-shell-0.2.1/tests/unit/test_makeindex.py000066400000000000000000000010061335127342700211140ustar00rootroot00000000000000import textwrap from awsshell import makeindex def test_can_convert_rst_text(): content = textwrap.dedent("""\ MySection ========= This is some text. Here's a list: * foo * bar Literal text: ``--foo-bar`` """) converted = makeindex.convert_rst_to_basic_text(content) assert converted == textwrap.dedent("""\ MYSECTION This is some text. Here's a list: * foo * bar Literal text: --foo-bar """) aws-shell-0.2.1/tests/unit/test_resources.py000066400000000000000000000200221335127342700211600ustar00rootroot00000000000000"""Index and retrive information from the resource JSON.""" import pytest import mock from botocore.exceptions import NoRegionError from awsshell.resource import index @pytest.fixture def describer_creator(): class FakeDescriberCreator(object): SERVICES = ['ec2'] def services_with_completions(self): return self.SERVICES return FakeDescriberCreator() def test_build_from_has_many(): resource = { 'service': { 'hasMany': { 'Tables': { 'request': {'operation': 'ListTables'}, 'resource': { 'type': 'Table', 'identifiers': [ {'target': 'Name', 'source': 'response', 'path': 'TableNames[]', } ] } } } }, 'resources': { 'Table': { 'actions': { 'Delete': { 'request': { 'operation': 'DeleteTable', 'params': [ {'target': 'TableName', 'source': 'identifier', 'name': 'Name'}, ] } } } } } } builder = index.ResourceIndexBuilder() built_index = builder.build_index(resource) assert built_index == { 'operations': { 'DeleteTable': { 'TableName': { 'resourceName': 'Table', 'resourceIdentifier': 'Name', } } }, 'resources': { 'Table': { 'operation': 'ListTables', 'resourceIdentifier': { 'Name': 'TableNames[]', } } } } def test_removes_jmespath_expressions_from_targets(): resource = { 'service': { 'hasMany': { 'Instances': { 'request': {'operation': 'DescribeInstances'}, 'resource': { 'type': 'Instance', 'identifiers': [ {'target': 'Id', 'source': 'response', 'path': 'Reservations[].Instances[].InstanceId', } ] } } } }, 'resources': { 'Instance': { 'actions': { 'Terminate': { 'request': { 'operation': 'TerminateInstances', 'params': [ {'target': 'InstanceIds[0]', 'source': 'identifier', 'name': 'Id'}, ] } } } } } } builder = index.ResourceIndexBuilder() built_index = builder.build_index(resource) assert built_index == { 'operations': { 'TerminateInstances': { 'InstanceIds': { 'resourceName': 'Instance', 'resourceIdentifier': 'Id', } } }, 'resources': { 'Instance': { 'operation': 'DescribeInstances', 'resourceIdentifier': { 'Id': 'Reservations[].Instances[].InstanceId', } } } } def test_resource_not_included_if_no_has_many(): # This is something we can fix, but for now the resource # must be in the hasMany. resource = { 'service': { 'hasMany': {} }, 'resources': { 'Tag': { 'actions': { 'Delete': { 'request': { 'operation': 'DeleteTags', 'params': [ {'target': 'Resources[0]', 'source': 'identifier', 'name': 'ResourceId'}, ] } } } } } } builder = index.ResourceIndexBuilder() built_index = builder.build_index(resource) # The index is empty because there was not matching # hasMany resource. assert built_index == { 'operations': {}, 'resources': {}, } def test_can_complete_query(): built_index = { 'dynamodb': { 'operations': { 'DeleteTable': { 'TableName': { 'resourceName': 'Table', 'resourceIdentifier': 'Name', } } }, 'resources': { 'Table': { 'operation': 'ListTables', 'resourceIdentifier': { 'Name': 'TableNames[]', } } } } } q = index.CompleterDescriber(built_index) result = q.describe_autocomplete( 'dynamodb', 'DeleteTable', 'TableName') assert result.service == 'dynamodb' assert result.operation == 'ListTables' assert result.params == {} assert result.path == 'TableNames[]' def test_cached_client_creator_returns_same_instance(): class FakeSession(object): def create_client(self, service_name): return object() cached_creator = index.CachedClientCreator(FakeSession()) ec2 = cached_creator.create_client('ec2') s3 = cached_creator.create_client('s3') assert ec2 != s3 # However, asking for a client we've already created # should return the exact same instance. assert cached_creator.create_client('ec2') == ec2 def test_can_create_service_completers_from_cache(): class FakeDescriberCreator(object): def load_service_model(self, service_name, type_name): assert type_name == 'completions-1' return "fake_completions_for_%s" % service_name def services_with_completions(self): return [] loader = FakeDescriberCreator() factory = index.CompleterDescriberCreator(loader) result = factory.create_completer_query('ec2') assert isinstance(result, index.CompleterDescriber) assert factory.create_completer_query('ec2') == result def test_empty_results_returned_when_no_completion_data_exists(describer_creator): describer_creator.SERVICES = [] completer = index.ServerSideCompleter( client_creator=None, describer_creator=describer_creator, ) assert completer.retrieve_candidate_values( 'ec2', 'run-instances', 'ImageId') == [] def test_no_completions_when_cant_create_client(describer_creator): client_creator = mock.Mock(spec=index.CachedClientCreator) # This is raised when you don't have a region configured via config file # env var or manually via a session. client_creator.create_client.side_effect = NoRegionError() completer = index.ServerSideCompleter( client_creator=client_creator, describer_creator=describer_creator) assert completer.retrieve_candidate_values( 'ec2', 'foo', 'Bar') == [] def test_no_completions_returned_on_unknown_operation(describer_creator): client = mock.Mock() client_creator = mock.Mock(spec=index.CachedClientCreator) client_creator.create_client.return_value = client client.meta.method_to_api_mapping = { 'describe_foo': 'DescribeFoo' } completer = index.ServerSideCompleter( client_creator=client_creator, describer_creator=describer_creator) assert completer.retrieve_candidate_values( 'ec2', 'not_describe_foo', 'Bar') == [] aws-shell-0.2.1/tests/unit/test_substring.py000066400000000000000000000017051335127342700211750ustar00rootroot00000000000000# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"). You # may not use this file except in compliance with the License. A copy of # the License is located at # # http://aws.amazon.com/apache2.0/ # # or in the "license" file accompanying this file. This file is # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. import pytest from awsshell.substring import substring_search @pytest.mark.parametrize("search,corpus,expected", [ ('foo', ['foobar', 'foobaz'], ['foobar', 'foobaz']), ('f', ['foo', 'foobar', 'bar'], ['foo', 'foobar']), ('z', ['foo', 'foobar', 'bar'], []), ]) def test_subsequences(search, corpus, expected): actual = substring_search(search, corpus) assert actual == expected aws-shell-0.2.1/tests/unit/test_toolbar.py000066400000000000000000000045661335127342700206270ustar00rootroot00000000000000# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"). You # may not use this file except in compliance with the License. A copy of # the License is located at # # http://aws.amazon.com/apache2.0/ # # or in the "license" file accompanying this file. This file is # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. import mock import unittest from pygments.token import Token from awsshell.app import AWSShell from awsshell.toolbar import Toolbar class ToolbarTest(unittest.TestCase): def setUp(self): self.aws_shell = AWSShell(mock.Mock(), mock.Mock(), mock.Mock()) self.cli = mock.Mock() self.toolbar = Toolbar( lambda: self.aws_shell.model_completer.match_fuzzy, lambda: self.aws_shell.enable_vi_bindings, lambda: self.aws_shell.show_completion_columns, lambda: self.aws_shell.show_help) def test_toolbar_on(self): self.aws_shell.model_completer.match_fuzzy = True self.aws_shell.enable_vi_bindings = True self.aws_shell.show_completion_columns = True self.aws_shell.show_help = True expected = [ (Token.Toolbar.On, ' [F2] Fuzzy: ON '), (Token.Toolbar.On, ' [F3] Keys: Vi '), (Token.Toolbar.On, ' [F4] Multi Column '), (Token.Toolbar.On, ' [F5] Help: ON '), (Token.Toolbar, ' [F9] Focus: doc '), (Token.Toolbar, ' [F10] Exit ')] assert expected == self.toolbar.handler(self.cli) def test_toolbar_off(self): self.aws_shell.model_completer.match_fuzzy = False self.aws_shell.enable_vi_bindings = False self.aws_shell.show_completion_columns = False self.aws_shell.show_help = False self.cli.current_buffer_name = 'DEFAULT_BUFFER' expected = [ (Token.Toolbar.Off, ' [F2] Fuzzy: OFF '), (Token.Toolbar.On, ' [F3] Keys: Emacs '), (Token.Toolbar.On, ' [F4] Single Column '), (Token.Toolbar.Off, ' [F5] Help: OFF '), (Token.Toolbar, ' [F9] Focus: cli '), (Token.Toolbar, ' [F10] Exit ')] assert expected == self.toolbar.handler(self.cli) aws-shell-0.2.1/tests/unit/test_utils.py000066400000000000000000000062331335127342700203160ustar00rootroot00000000000000from tests import unittest import os import tempfile import shutil from awsshell.utils import FSLayer from awsshell.utils import InMemoryFSLayer from awsshell.utils import FileReadError from awsshell.utils import temporary_file class TestFSLayer(unittest.TestCase): # TestFSLayer provides abstractions over the OS. # It is one of the only exceptions in the AWS Shell # code where it's ok to test by using actual files. # All other test code should use FSLayer. def setUp(self): self.tempdir = tempfile.mkdtemp() self.temporary_filename = os.path.join( self.tempdir, 'tempfilefoo') self.fslayer = FSLayer() def tearDown(self): shutil.rmtree(self.tempdir) def test_can_read_file_contents(self): with open(self.temporary_filename, 'w') as f: f.write('helloworld') self.assertEqual( self.fslayer.file_contents(self.temporary_filename), 'helloworld') self.assertEqual( self.fslayer.file_contents(self.temporary_filename, binary=True), b'helloworld') def test_file_exists(self): self.assertFalse(self.fslayer.file_exists(self.temporary_filename)) with open(self.temporary_filename, 'w') as f: pass self.assertTrue(self.fslayer.file_exists(self.temporary_filename)) def test_file_does_not_exist_error(self): with self.assertRaises(FileReadError): self.fslayer.file_contents('/tmp/thisdoesnot-exist.asdf') class TestInMemoryFSLayer(unittest.TestCase): def setUp(self): self.file_mapping = {} self.fslayer = InMemoryFSLayer(self.file_mapping) def test_file_exists(self): self.file_mapping['/my/fake/path'] = 'file contents' self.assertTrue(self.fslayer.file_exists('/my/fake/path')) def test_can_read_file_contents(self): self.file_mapping['/myfile'] = 'helloworld' self.assertEqual(self.fslayer.file_contents('/myfile'), 'helloworld') self.assertEqual(self.fslayer.file_contents('/myfile', binary=True), b'helloworld') def test_file_does_not_exist_error(self): with self.assertRaises(FileReadError): self.fslayer.file_contents('/tmp/thisdoesnot-exist.asdf') class TestTemporaryFile(unittest.TestCase): def test_can_use_as_context_manager(self): with temporary_file('w') as f: filename = f.name f.write("foobar") f.flush() self.assertEqual(open(filename).read(), "foobar") def test_is_removed_after_exiting_context(self): with temporary_file('w') as f: filename = f.name f.write("foobar") f.flush() self.assertFalse(os.path.isfile(filename)) def test_can_open_in_read(self): with temporary_file('r') as f: filename = f.name assert f.read() == '' # Verify we can open the file again # in another file descriptor. with open(filename, 'w') as f2: f2.write("foobar") f.seek(0) assert f.read() == "foobar" self.assertFalse(os.path.isfile(filename)) aws-shell-0.2.1/tox.ini000066400000000000000000000001531335127342700147320ustar00rootroot00000000000000[tox] envlist = py26,py27,py33,py34,py35,py36 [testenv] commands = py.test deps = -rrequirements-test.txt