├── .gitchangelog.rc ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CHANGELOG.rst ├── CHECKLIST.md ├── CONTRIBUTING.md ├── Dockerfile ├── INSTALLATION.md ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── appveyor.yml ├── docs ├── Makefile ├── make.bat └── source │ ├── README.rst │ ├── conf.py │ ├── index.rst │ ├── modules.rst │ └── saws.rst ├── requirements-dev.txt ├── saws ├── __init__.py ├── commands.py ├── completer.py ├── config.py ├── data │ ├── OPTIONS.txt │ ├── RESOURCES_SAMPLE.txt │ └── SOURCES.txt ├── data_util.py ├── keys.py ├── lexer.py ├── logger.py ├── main.py ├── options.py ├── resource │ ├── __init__.py │ ├── bucket.py │ ├── bucket_names.py │ ├── bucket_uris.py │ ├── instance_ids.py │ ├── instance_tag_keys.py │ ├── instance_tag_values.py │ └── resource.py ├── resources.py ├── saws.py ├── saws.shortcuts ├── sawsrc ├── style.py ├── toolbar.py └── utils.py ├── scripts ├── create_changelog.sh ├── create_readme_rst.sh ├── run_code_checks.sh ├── set_changelog_as_readme.sh ├── set_changelog_as_readme_undo.sh ├── update_docs.sh └── upload_pypi.sh ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── compat.py ├── run_tests.py ├── test_cli.py ├── test_commands.py ├── test_completer.py ├── test_keys.py ├── test_options.py ├── test_resources.py ├── test_saws.py └── test_toolbar.py └── tox.ini /.gitchangelog.rc: -------------------------------------------------------------------------------- 1 | ## 2 | ## Format 3 | ## 4 | ## ACTION: [AUDIENCE:] COMMIT_MSG [!TAG ...] 5 | ## 6 | ## Description 7 | ## 8 | ## ACTION is one of 'chg', 'fix', 'new' 9 | ## 10 | ## Is WHAT the change is about. 11 | ## 12 | ## 'chg' is for refactor, small improvement, cosmetic changes... 13 | ## 'fix' is for bug fixes 14 | ## 'new' is for new features, big improvement 15 | ## 16 | ## AUDIENCE is optional and one of 'dev', 'usr', 'pkg', 'test', 'doc' 17 | ## 18 | ## Is WHO is concerned by the change. 19 | ## 20 | ## 'dev' is for developpers (API changes, refactors...) 21 | ## 'usr' is for final users (UI changes) 22 | ## 'pkg' is for packagers (packaging changes) 23 | ## 'test' is for testers (test only related changes) 24 | ## 'doc' is for doc guys (doc only changes) 25 | ## 26 | ## COMMIT_MSG is ... well ... the commit message itself. 27 | ## 28 | ## TAGs are additionnal adjective as 'refactor' 'minor' 'cosmetic' 29 | ## 30 | ## They are preceded with a '!' or a '@' (prefer the former, as the 31 | ## latter is wrongly interpreted in github.) Commonly used tags are: 32 | ## 33 | ## 'refactor' is obviously for refactoring code only 34 | ## 'minor' is for a very meaningless change (a typo, adding a comment) 35 | ## 'cosmetic' is for cosmetic driven change (re-indentation, 80-col...) 36 | ## 'wip' is for partial functionality but complete subfunctionality. 37 | ## 38 | ## Example: 39 | ## 40 | ## new: usr: support of bazaar implemented 41 | ## chg: re-indentend some lines !cosmetic 42 | ## new: dev: updated code to be compatible with last version of killer lib. 43 | ## fix: pkg: updated year of licence coverage. 44 | ## new: test: added a bunch of test around user usability of feature X. 45 | ## fix: typo in spelling my name in comment. !minor 46 | ## 47 | ## Please note that multi-line commit message are supported, and only the 48 | ## first line will be considered as the "summary" of the commit message. So 49 | ## tags, and other rules only applies to the summary. The body of the commit 50 | ## message will be displayed in the changelog without reformatting. 51 | 52 | 53 | ## 54 | ## ``ignore_regexps`` is a line of regexps 55 | ## 56 | ## Any commit having its full commit message matching any regexp listed here 57 | ## will be ignored and won't be reported in the changelog. 58 | ## 59 | ignore_regexps = [ 60 | r'@minor', r'!minor', 61 | r'@cosmetic', r'!cosmetic', 62 | r'@refactor', r'!refactor', 63 | r'@wip', r'!wip', 64 | r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[p|P]kg:', 65 | r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[d|D]ev:', 66 | r'^(.{3,3}\s*:)?\s*[fF]irst commit.?\s*$', 67 | ] 68 | 69 | 70 | ## ``section_regexps`` is a list of 2-tuples associating a string label and a 71 | ## list of regexp 72 | ## 73 | ## Commit messages will be classified in sections thanks to this. Section 74 | ## titles are the label, and a commit is classified under this section if any 75 | ## of the regexps associated is matching. 76 | ## 77 | section_regexps = [ 78 | ('New', [ 79 | r'^[nN]ew\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$', 80 | ]), 81 | ('Changes', [ 82 | r'^[cC]hg\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$', 83 | ]), 84 | ('Fix', [ 85 | r'^[fF]ix\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$', 86 | ]), 87 | 88 | ('Other', None ## Match all lines 89 | ), 90 | 91 | ] 92 | 93 | 94 | ## ``body_process`` is a callable 95 | ## 96 | ## This callable will be given the original body and result will 97 | ## be used in the changelog. 98 | ## 99 | ## Available constructs are: 100 | ## 101 | ## - any python callable that take one txt argument and return txt argument. 102 | ## 103 | ## - ReSub(pattern, replacement): will apply regexp substitution. 104 | ## 105 | ## - Indent(chars=" "): will indent the text with the prefix 106 | ## Please remember that template engines gets also to modify the text and 107 | ## will usually indent themselves the text if needed. 108 | ## 109 | ## - Wrap(regexp=r"\n\n"): re-wrap text in separate paragraph to fill 80-Columns 110 | ## 111 | ## - noop: do nothing 112 | ## 113 | ## - ucfirst: ensure the first letter is uppercase. 114 | ## (usually used in the ``subject_process`` pipeline) 115 | ## 116 | ## - final_dot: ensure text finishes with a dot 117 | ## (usually used in the ``subject_process`` pipeline) 118 | ## 119 | ## - strip: remove any spaces before or after the content of the string 120 | ## 121 | ## Additionally, you can `pipe` the provided filters, for instance: 122 | #body_process = Wrap(regexp=r'\n(?=\w+\s*:)') | Indent(chars=" ") 123 | #body_process = Wrap(regexp=r'\n(?=\w+\s*:)') 124 | #body_process = noop 125 | body_process = ReSub(r'((^|\n)[A-Z]\w+(-\w+)*: .*(\n\s+.*)*)+$', r'') | strip 126 | 127 | 128 | ## ``subject_process`` is a callable 129 | ## 130 | ## This callable will be given the original subject and result will 131 | ## be used in the changelog. 132 | ## 133 | ## Available constructs are those listed in ``body_process`` doc. 134 | subject_process = (strip | 135 | ReSub(r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n@]*)(@[a-z]+\s+)*$', r'\4') | 136 | ucfirst | final_dot) 137 | 138 | 139 | ## ``tag_filter_regexp`` is a regexp 140 | ## 141 | ## Tags that will be used for the changelog must match this regexp. 142 | ## 143 | tag_filter_regexp = r'^[0-9]+\.[0-9]+(\.[0-9]+)?$' 144 | 145 | 146 | ## ``unreleased_version_label`` is a string 147 | ## 148 | ## This label will be used as the changelog Title of the last set of changes 149 | ## between last valid tag and HEAD if any. 150 | unreleased_version_label = "%%version%% (unreleased)" 151 | 152 | 153 | ## ``output_engine`` is a callable 154 | ## 155 | ## This will change the output format of the generated changelog file 156 | ## 157 | ## Available choices are: 158 | ## 159 | ## - rest_py 160 | ## 161 | ## Legacy pure python engine, outputs ReSTructured text. 162 | ## This is the default. 163 | ## 164 | ## - mustache() 165 | ## 166 | ## Template name could be any of the available templates in 167 | ## ``templates/mustache/*.tpl``. 168 | ## Requires python package ``pystache``. 169 | ## Examples: 170 | ## - mustache("markdown") 171 | ## - mustache("restructuredtext") 172 | ## 173 | ## - makotemplate() 174 | ## 175 | ## Template name could be any of the available templates in 176 | ## ``templates/mako/*.tpl``. 177 | ## Requires python package ``mako``. 178 | ## Examples: 179 | ## - makotemplate("restructuredtext") 180 | ## 181 | output_engine = rest_py 182 | #output_engine = mustache("restructuredtext") 183 | #output_engine = mustache("markdown") 184 | #output_engine = makotemplate("restructuredtext") 185 | 186 | 187 | ## ``include_merge`` is a boolean 188 | ## 189 | ## This option tells git-log whether to include merge commits in the log. 190 | ## The default is to include them. 191 | include_merge = True -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | eggs 11 | parts 12 | bin 13 | var 14 | sdist 15 | develop-eggs 16 | .installed.cfg 17 | lib 18 | lib64 19 | __pycache__ 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Sphinx documentation 33 | docs/build/ 34 | 35 | # Mr Developer 36 | .mr.developer.cfg 37 | .project 38 | .pydevproject 39 | 40 | .idea/ 41 | *.pyc 42 | *.egg-info/ 43 | *env/ 44 | .tox/ 45 | dist/ 46 | .DS_Store 47 | 48 | screenshots/ 49 | scratch/ 50 | saws/data/RESOURCES*.txt 51 | CHANGELOG_DRAFT 52 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | matrix: 3 | include: 4 | - python: 2.7 5 | env: TOXENV=py27 6 | - python: 3.6 7 | env: TOXENV=py36 8 | install: 9 | - travis_retry pip install tox codecov 10 | script: 11 | - tox 12 | after_success: 13 | - codecov 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ![](http://i.imgur.com/vzC5zmA.gif) 2 | 3 | [![Build Status](https://travis-ci.org/donnemartin/saws.svg?branch=master)](https://travis-ci.org/donnemartin/saws) [![Documentation Status](https://readthedocs.org/projects/saws/badge/?version=latest)](http://saws.readthedocs.org/en/latest/?badge=latest) [![Dependency Status](https://gemnasium.com/donnemartin/saws.svg)](https://gemnasium.com/donnemartin/saws) 4 | 5 | [![PyPI version](https://badge.fury.io/py/saws.svg)](http://badge.fury.io/py/saws) [![PyPI](https://img.shields.io/pypi/pyversions/saws.svg)](https://pypi.python.org/pypi/saws/) [![License](http://img.shields.io/:license-apache-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0.html) 6 | 7 | SAWS 8 | ==== 9 | 10 | To view the latest `README`, `docs`, and `code` visit the GitHub repo: 11 | 12 | https://github.com/donnemartin/saws 13 | 14 | To submit bugs or feature requests, visit the issue tracker: 15 | 16 | https://github.com/donnemartin/saws/issues 17 | 18 | Changelog 19 | ========= 20 | 21 | 0.4.2 (2017-04-08) 22 | ------------------ 23 | 24 | ### Bug Fixes 25 | 26 | * [#90](https://github.com/donnemartin/saws/pull/90) - Fix `Sphinx` document generation issues. 27 | 28 | ### Updates 29 | 30 | * Update list of commands. 31 | * [#92](https://github.com/donnemartin/saws/pull/92) - Update `feed_key to feed and process_keys for `prompt-toolkit` v1.0.1+. 32 | 33 | 0.4.1 (2015-05-31) 34 | ------------------ 35 | 36 | ### Bug Fixes 37 | 38 | * [#83](https://github.com/donnemartin/saws/pull/83) - Update to `prompt-toolkit` 1.0.0, which includes a number of performance improvements (especially noticeable on Windows) and bug fixes. 39 | 40 | ### Updates 41 | 42 | * [#75](https://github.com/donnemartin/saws/pull/75), [#76](https://github.com/donnemartin/saws/pull/76) - Fix groff install and follow Dockerfile best practices. 43 | * [#85](https://github.com/donnemartin/saws/pull/85) - Update packaging dependencies based on semantic versioning. 44 | * [#86](https://github.com/donnemartin/saws/pull/86) - Fix linter issues regarding imports. 45 | * Update list of commands. 46 | * Update INSTALLATION: 47 | * Add install from SOURCE. 48 | * Add note about OS X 10.11 pip issue (now also in README). 49 | * Update intro. 50 | * Update link to style guide in CONTRIBUTING. 51 | 52 | 0.4.0 (2015-12-08) 53 | ------------------ 54 | 55 | ### Features 56 | 57 | * Implemented [#67](https://github.com/donnemartin/saws/issues/67): Add Fish-style auto suggestions. 58 | 59 | ### Bug Fixes 60 | 61 | * Fixed [#71](https://github.com/donnemartin/saws/issues/71): Disable color output for shell commands. 62 | * Fixed [#72](https://github.com/donnemartin/saws/issues/72): Exiting with `F10` does not clear the menu bar. 63 | 64 | ### Updates 65 | 66 | * Updated list of commands. 67 | * Updated repo `README`. 68 | * Added auto suggestions. 69 | * Fixed [#66](https://github.com/donnemartin/saws/issues/38): Removed `docs/build` from source repo. 70 | 71 | 0.3.2 (2015-10-16) 72 | ------------------ 73 | 74 | ### Features 75 | 76 | * Resolved [#38](https://github.com/donnemartin/saws/issues/38): Added `Docker` installation support, by [frosforever](https://github.com/frosforever). 77 | * Resolved [#39](https://github.com/donnemartin/saws/issues/39): Changed completion matching to ignore case. 78 | * Resolved [#40](https://github.com/donnemartin/saws/issues/40): Added `emr --cluster-states` completions. 79 | * Resolved [#52](https://github.com/donnemartin/saws/issues/52) and [#58](https://github.com/donnemartin/saws/issues/58): Updated list of auto-completed commands and subcommands. 80 | * Resolved [#53](https://github.com/donnemartin/saws/issues/53): Moved shortcuts out of `~/.sawsrc` to a new file `~/.saws.shortcuts` to simplify managing shortcuts. 81 | 82 | ### Bug Fixes 83 | 84 | * Fixed [#22](https://github.com/donnemartin/saws/issues/22) and [#26](https://github.com/donnemartin/saws/issues/26): 85 | * `ordereddict` is now only installed with Python 2.6. 86 | * `enum34` is now only installed with Python 3.3 and below. 87 | * Fixed [#29](https://github.com/donnemartin/saws/issues/29): `SAWS` is now compatible with `prompt_toolkit` version 0.52, by [jonathanslenders](https://github.com/jonathanslenders). 88 | * Fixed [#33](https://github.com/donnemartin/saws/issues/29): `SAWS` will no longer exit on keyboard interrupt such as `Ctrl-C`, which can be useful to terminate long-running `aws-cli` commands. 89 | * Fixed [#35](https://github.com/donnemartin/saws/issues/35): Grep now works consistently with shortcuts, by [mlimaloureiro](https://github.com/mlimaloureiro). 90 | * Fixed [#41](https://github.com/donnemartin/saws/issues/41): Blank entry is no longer shown in list of completion if there is no optional value set for a given tag's key. 91 | * Fixed [#60](https://github.com/donnemartin/saws/issues/60): Running an empty command no longer results in a pygmentize syntax error. 92 | * Fixed [#61](https://github.com/donnemartin/saws/issues/61): Refreshing resources multiple times no longer results in an exception. 93 | 94 | ### Updates 95 | 96 | * Added PyPI keywords for easier searching. 97 | * Updated PyPI `README`. 98 | * Added GitHub repo link, issue tracker, and repo gif. 99 | * Added `INSTALLATION` doc, with the following updates: 100 | * Added `virtualenv` installation section. 101 | * Added `Pipsi` installation section [#44](https://github.com/donnemartin/saws/issues/44), by [svieira](https://github.com/svieira). 102 | * Added `Docker` installation section [#38](https://github.com/donnemartin/saws/issues/38), by [frosforever](https://github.com/frosforever). 103 | * Updated repo `README`. 104 | * Updated discussion of shortcuts with the new `~/.saws.shortcuts` file. 105 | * Added Command History section. 106 | * Updated AWS Credentials and Named Profiles section. 107 | * Added command to run `SAWS` in the Developer Installation section. 108 | * Updated Motivation section to include fuzzy shortcut completion, toolbar options, execution and piping of shell commands. and history of commands. 109 | * Mentioned initial testing of Python 3.5 support. 110 | * Added install from GitHub source instructions to get the latest dev release 111 | * Updated docs. 112 | 113 | 0.2.1 (2015-09-23) 114 | ------------------ 115 | 116 | ### Bug Fixes 117 | 118 | - Fixed [#29](https://github.com/donnemartin/saws/issues/29): Dependency on python-prompt-toolkit > 0.50 breaks saws. 119 | 120 | 0.2.0 (2015-09-22) 121 | ------------------ 122 | 123 | ### Features 124 | 125 | - Added support for [#18](https://github.com/donnemartin/saws/issues/18): Multiple syntax highlighting themes. 126 | 127 | - Added improved support for [#17](https://github.com/donnemartin/saws/issues/17): Execute shell commands within `SAWS`, including piping. 128 | 129 | ### Bug Fixes 130 | 131 | - Fixed [#21](https://github.com/donnemartin/saws/issues/21): Current command is overwritten on screen when refreshing resources with F5, by [jonathanslenders](https://github.com/jonathanslenders). 132 | 133 | ### Updates 134 | 135 | - Updated `README` installation section with: 136 | 137 | * `Virtualenv` instructions. 138 | * Details on how to run AWS named profiles/credentials. 139 | * Supported/tested platforms. 140 | 141 | - Updated `README` developer installation section with a new command to build the docs. 142 | 143 | - Updated docs. 144 | 145 | 0.1.1 (2015-09-21) 146 | ------------------ 147 | 148 | ### Bug Fixes 149 | 150 | - Fixed [#14](https://github.com/donnemartin/saws/issues/14): Fuzzy completions are sometimes showing incorrect 151 | completions for built-in commands and subcommands. 152 | 153 | ### Updates 154 | 155 | - Updated `README` installation section on how to run `SAWS`. 156 | 157 | - Updated docs. 158 | 159 | - Updated description, download url, license, and classifiers in 160 | setup.py. 161 | 162 | 0.1.0 (2015-09-21) 163 | ------------------ 164 | 165 | - Initial release. 166 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | .. figure:: http://i.imgur.com/vzC5zmA.gif 2 | :alt: 3 | 4 | |Build Status| |Documentation Status| |Dependency Status| 5 | 6 | |PyPI version| |PyPI| |License| 7 | 8 | SAWS 9 | ==== 10 | 11 | To view the latest ``README``, ``docs``, and ``code`` visit the GitHub 12 | repo: 13 | 14 | https://github.com/donnemartin/saws 15 | 16 | To submit bugs or feature requests, visit the issue tracker: 17 | 18 | https://github.com/donnemartin/saws/issues 19 | 20 | Changelog 21 | ========= 22 | 23 | 0.4.2 (2017-04-08) 24 | ------------------ 25 | 26 | Bug Fixes 27 | ~~~~~~~~~ 28 | 29 | - `#90 `__ - Fix 30 | ``Sphinx`` document generation issues. 31 | 32 | Updates 33 | ~~~~~~~ 34 | 35 | - Update list of commands. 36 | - `#92 `__ - Update 37 | ``feed_key to feed and process_keys for``\ prompt-toolkit\` v1.0.1+. 38 | 39 | 0.4.1 (2015-05-31) 40 | ------------------ 41 | 42 | Bug Fixes 43 | ~~~~~~~~~ 44 | 45 | - `#83 `__ - Update to 46 | ``prompt-toolkit`` 1.0.0, which includes a number of performance 47 | improvements (especially noticeable on Windows) and bug fixes. 48 | 49 | Updates 50 | ~~~~~~~ 51 | 52 | - `#75 `__, 53 | `#76 `__ - Fix groff 54 | install and follow Dockerfile best practices. 55 | - `#85 `__ - Update 56 | packaging dependencies based on semantic versioning. 57 | - `#86 `__ - Fix linter 58 | issues regarding imports. 59 | - Update list of commands. 60 | - Update INSTALLATION: 61 | 62 | - Add install from SOURCE. 63 | - Add note about OS X 10.11 pip issue (now also in README). 64 | - Update intro. 65 | 66 | - Update link to style guide in CONTRIBUTING. 67 | 68 | 0.4.0 (2015-12-08) 69 | ------------------ 70 | 71 | Features 72 | ~~~~~~~~ 73 | 74 | - Implemented `#67 `__: 75 | Add Fish-style auto suggestions. 76 | 77 | Bug Fixes 78 | ~~~~~~~~~ 79 | 80 | - Fixed `#71 `__: 81 | Disable color output for shell commands. 82 | - Fixed `#72 `__: 83 | Exiting with ``F10`` does not clear the menu bar. 84 | 85 | Updates 86 | ~~~~~~~ 87 | 88 | - Updated list of commands. 89 | - Updated repo ``README``. 90 | 91 | - Added auto suggestions. 92 | 93 | - Fixed `#66 `__: 94 | Removed ``docs/build`` from source repo. 95 | 96 | 0.3.2 (2015-10-16) 97 | ------------------ 98 | 99 | Features 100 | ~~~~~~~~ 101 | 102 | - Resolved `#38 `__: 103 | Added ``Docker`` installation support, by 104 | `frosforever `__. 105 | - Resolved `#39 `__: 106 | Changed completion matching to ignore case. 107 | - Resolved `#40 `__: 108 | Added ``emr --cluster-states`` completions. 109 | - Resolved `#52 `__ and 110 | `#58 `__: Updated list 111 | of auto-completed commands and subcommands. 112 | - Resolved `#53 `__: 113 | Moved shortcuts out of ``~/.sawsrc`` to a new file 114 | ``~/.saws.shortcuts`` to simplify managing shortcuts. 115 | 116 | Bug Fixes 117 | ~~~~~~~~~ 118 | 119 | - Fixed `#22 `__ and 120 | `#26 `__: 121 | 122 | - ``ordereddict`` is now only installed with Python 2.6. 123 | - ``enum34`` is now only installed with Python 3.3 and below. 124 | 125 | - Fixed `#29 `__: 126 | ``SAWS`` is now compatible with ``prompt_toolkit`` version 0.52, by 127 | `jonathanslenders `__. 128 | - Fixed `#33 `__: 129 | ``SAWS`` will no longer exit on keyboard interrupt such as 130 | ``Ctrl-C``, which can be useful to terminate long-running ``aws-cli`` 131 | commands. 132 | - Fixed `#35 `__: Grep 133 | now works consistently with shortcuts, by 134 | `mlimaloureiro `__. 135 | - Fixed `#41 `__: Blank 136 | entry is no longer shown in list of completion if there is no 137 | optional value set for a given tag's key. 138 | - Fixed `#60 `__: 139 | Running an empty command no longer results in a pygmentize syntax 140 | error. 141 | - Fixed `#61 `__: 142 | Refreshing resources multiple times no longer results in an 143 | exception. 144 | 145 | Updates 146 | ~~~~~~~ 147 | 148 | - Added PyPI keywords for easier searching. 149 | - Updated PyPI ``README``. 150 | 151 | - Added GitHub repo link, issue tracker, and repo gif. 152 | 153 | - Added ``INSTALLATION`` doc, with the following updates: 154 | 155 | - Added ``virtualenv`` installation section. 156 | - Added ``Pipsi`` installation section 157 | `#44 `__, by 158 | `svieira `__. 159 | - Added ``Docker`` installation section 160 | `#38 `__, by 161 | `frosforever `__. 162 | 163 | - Updated repo ``README``. 164 | 165 | - Updated discussion of shortcuts with the new ``~/.saws.shortcuts`` 166 | file. 167 | - Added Command History section. 168 | - Updated AWS Credentials and Named Profiles section. 169 | - Added command to run ``SAWS`` in the Developer Installation 170 | section. 171 | - Updated Motivation section to include fuzzy shortcut completion, 172 | toolbar options, execution and piping of shell commands. and 173 | history of commands. 174 | - Mentioned initial testing of Python 3.5 support. 175 | - Added install from GitHub source instructions to get the latest 176 | dev release 177 | 178 | - Updated docs. 179 | 180 | 0.2.1 (2015-09-23) 181 | ------------------ 182 | 183 | Bug Fixes 184 | ~~~~~~~~~ 185 | 186 | - Fixed `#29 `__: 187 | Dependency on python-prompt-toolkit > 0.50 breaks saws. 188 | 189 | 0.2.0 (2015-09-22) 190 | ------------------ 191 | 192 | Features 193 | ~~~~~~~~ 194 | 195 | - Added support for 196 | `#18 `__: Multiple 197 | syntax highlighting themes. 198 | 199 | - Added improved support for 200 | `#17 `__: Execute 201 | shell commands within ``SAWS``, including piping. 202 | 203 | Bug Fixes 204 | ~~~~~~~~~ 205 | 206 | - Fixed `#21 `__: 207 | Current command is overwritten on screen when refreshing resources 208 | with F5, by 209 | `jonathanslenders `__. 210 | 211 | Updates 212 | ~~~~~~~ 213 | 214 | - Updated ``README`` installation section with: 215 | 216 | - ``Virtualenv`` instructions. 217 | - Details on how to run AWS named profiles/credentials. 218 | - Supported/tested platforms. 219 | 220 | - Updated ``README`` developer installation section with a new command 221 | to build the docs. 222 | 223 | - Updated docs. 224 | 225 | 0.1.1 (2015-09-21) 226 | ------------------ 227 | 228 | Bug Fixes 229 | ~~~~~~~~~ 230 | 231 | - Fixed `#14 `__: Fuzzy 232 | completions are sometimes showing incorrect completions for built-in 233 | commands and subcommands. 234 | 235 | Updates 236 | ~~~~~~~ 237 | 238 | - Updated ``README`` installation section on how to run ``SAWS``. 239 | 240 | - Updated docs. 241 | 242 | - Updated description, download url, license, and classifiers in 243 | setup.py. 244 | 245 | 0.1.0 (2015-09-21) 246 | ------------------ 247 | 248 | - Initial release. 249 | 250 | .. |Build Status| image:: https://travis-ci.org/donnemartin/saws.svg?branch=master 251 | :target: https://travis-ci.org/donnemartin/saws 252 | .. |Documentation Status| image:: https://readthedocs.org/projects/saws/badge/?version=latest 253 | :target: http://saws.readthedocs.org/en/latest/?badge=latest 254 | .. |Dependency Status| image:: https://gemnasium.com/donnemartin/saws.svg 255 | :target: https://gemnasium.com/donnemartin/saws 256 | .. |PyPI version| image:: https://badge.fury.io/py/saws.svg 257 | :target: http://badge.fury.io/py/saws 258 | .. |PyPI| image:: https://img.shields.io/pypi/pyversions/saws.svg 259 | :target: https://pypi.python.org/pypi/saws/ 260 | .. |License| image:: http://img.shields.io/:license-apache-blue.svg 261 | :target: http://www.apache.org/licenses/LICENSE-2.0.html 262 | -------------------------------------------------------------------------------- /CHECKLIST.md: -------------------------------------------------------------------------------- 1 | Release Checklist 2 | ================= 3 | 4 | A. Install in a new venv and run unit tests 5 | 6 | Note, you can't seem to script the virtualenv calls, see: 7 | https://bitbucket.org/dhellmann/virtualenvwrapper/issues/219/cant-deactivate-active-virtualenv-from 8 | 9 | $ deactivate 10 | $ rmvirtualenv saws 11 | $ mkvirtualenv saws 12 | $ pip install -e . 13 | $ pip install -r requirements-dev.txt 14 | $ rm -rf .tox && tox 15 | 16 | B. Run code checks 17 | 18 | $ scripts/run_code_checks.sh 19 | 20 | C. Run manual [smoke tests](#smoke-tests) on Mac, Ubuntu, Windows 21 | 22 | D. Update and review `README.rst` and `Sphinx` docs, then check saws/docs/build/html/index.html 23 | 24 | $ scripts/update_docs.sh 25 | 26 | E. Push changes 27 | 28 | F. Review Travis, Codecov, and Gemnasium 29 | 30 | G. Start a new release branch 31 | 32 | $ git flow release start x.y.z 33 | 34 | H. Increment the version number in `saws/__init__.py` 35 | 36 | I. Update and review `CHANGELOG.md`, then run: 37 | 38 | $ scripts/create_changelog.sh 39 | 40 | J. Commit the changes 41 | 42 | K. Finish the release branch 43 | 44 | $ git flow release finish 'x.y.z' 45 | 46 | L. Input a tag 47 | 48 | $ vx.y.z 49 | 50 | M. Push tagged release to develop and master 51 | 52 | N. Set CHANGELOG as `README.md` 53 | 54 | $ scripts/set_changelog_as_readme.sh 55 | 56 | O. Register package with PyPi 57 | 58 | $ python setup.py register -r pypi 59 | 60 | P. Upload to PyPi 61 | 62 | $ python setup.py sdist upload -r pypi 63 | 64 | Q. Upload Sphinx docs to PyPi (requires Python2) 65 | 66 | $ python setup.py upload_sphinx 67 | 68 | R. Restore `README.md` 69 | 70 | $ scripts/set_changelog_as_readme_undo.sh 71 | 72 | S. Review newly released package from PyPi 73 | 74 | T. Release on GitHub: https://github.com/donnemartin/saws/tags 75 | 76 | 1. Click "Add release notes" for latest release 77 | 2. Copy release notes from `CHANGELOG.md` 78 | 3. Click "Publish release" 79 | 80 | U. Install in a new venv and run manual [smoke tests](#smoke-tests) on Mac, Ubuntu, Windows 81 | 82 | ## Smoke Tests 83 | 84 | Run the following on Python 2.7 and Python 3.4: 85 | 86 | * Craete a new `virtualenv` 87 | * Pip install `SAWS` into new `virtualenv` 88 | * Run `SAWS` 89 | * Check initial resource load 90 | * Check resource load from cache 91 | * Force refresh of resources 92 | * Toggle toolbar options 93 | * Verify toolbar options are saved across sessions 94 | * Test the following commands 95 | * Blank 96 | * aws 97 | * aws elb 98 | * aws s3api get-bucket-acl --bucket 99 | * aws ec2 describe-instances --instance-ids 100 | * aws ecls 101 | * aws ec2 ls | grep InstanceId 102 | * aws ectagk 103 | * aws ectagv 104 | * aws ecstate 105 | * aws emrls 106 | * aws s3 ls s3: 107 | * aws s3 ls docs 108 | * cd .. 109 | * cd saws 110 | * Run targeted tests based on recent code changes 111 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Contributions are welcome! 5 | 6 | **Please carefully read this page to make the code review process go as smoothly as possible and to maximize the likelihood of your contribution being merged.** 7 | 8 | ## Bug Reports 9 | 10 | For bug reports or requests [submit an issue](https://github.com/donnemartin/saws/issues). 11 | 12 | ## Pull Requests 13 | 14 | The preferred way to contribute is to fork the 15 | [main repository](https://github.com/donnemartin/saws) on GitHub. 16 | 17 | 1. Fork the [main repository](https://github.com/donnemartin/saws). Click on the 'Fork' button near the top of the page. This creates a copy of the code under your account on the GitHub server. 18 | 19 | 2. Clone this copy to your local disk: 20 | 21 | $ git clone git@github.com:YourLogin/saws.git 22 | $ cd saws 23 | 24 | 3. Create a branch to hold your changes and start making changes. Don't work in the `master` branch! 25 | 26 | $ git checkout -b my-feature 27 | 28 | 4. Work on this copy on your computer using Git to do the version control. When you're done editing, run the following to record your changes in Git: 29 | 30 | $ git add modified_files 31 | $ git commit 32 | 33 | 5. Push your changes to GitHub with: 34 | 35 | $ git push -u origin my-feature 36 | 37 | 6. Finally, go to the web page of your fork of the `SAWS` repo and click 'Pull Request' to send your changes for review. 38 | 39 | ### GitHub Pull Requests Docs 40 | 41 | If you are not familiar with pull requests, review the [pull request docs](https://help.github.com/articles/using-pull-requests/). 42 | 43 | ### Code Quality 44 | 45 | Ensure your pull request satisfies all of the following, where applicable: 46 | 47 | * Is covered by [unit tests](https://github.com/donnemartin/saws#unit-tests-and-code-coverage) 48 | * Passes [continuous integration](https://github.com/donnemartin/saws#continuous-integration) 49 | * Is covered by [documentation](https://github.com/donnemartin/saws#documentation) 50 | 51 | Review the following [style guide](https://google.github.io/styleguide/pyguide.html). 52 | 53 | Run code checks and fix any issues: 54 | 55 | $ scripts/run_code_checks.sh 56 | 57 | ### Installation 58 | 59 | Refer to the [Installation](https://github.com/donnemartin/saws#installation) and [Developer Installation](https://github.com/donnemartin/saws#developer-installation) sections. 60 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:latest 2 | 3 | RUN apt-get update && apt-get install -y \ 4 | groff \ 5 | python-pip \ 6 | && apt-get clean \ 7 | && rm -rf /var/lib/apt/lists/* 8 | 9 | RUN pip install saws 10 | 11 | ENTRYPOINT ["saws"] 12 | -------------------------------------------------------------------------------- /INSTALLATION.md: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | ## Pip Installation 5 | 6 | [![PyPI version](https://badge.fury.io/py/saws.svg)](http://badge.fury.io/py/saws) [![PyPI](https://img.shields.io/pypi/pyversions/saws.svg)](https://pypi.python.org/pypi/saws/) 7 | 8 | `SAWS` is hosted on [PyPI](https://pypi.python.org/pypi/saws). The following command will install `SAWS` along with dependencies such as the [AWS CLI](https://github.com/aws/aws-cli): 9 | 10 | $ pip install saws 11 | 12 | You can also install the latest `SAWS` from GitHub source which can contain changes not yet pushed to PyPI: 13 | 14 | $ pip install git+https://github.com/donnemartin/saws.git 15 | 16 | If you are not installing in a [virtualenv](#virtual-environment-installation), run: 17 | 18 | $ sudo pip install saws 19 | 20 | Once installed, start `SAWS`: 21 | 22 | $ saws 23 | 24 | ## Virtual Environment Installation 25 | 26 | It is recommended that you install Python packages in a [virtualenv](http://docs.python-guide.org/en/latest/dev/virtualenvs/) to avoid potential [issues with dependencies or permissions](https://github.com/donnemartin/saws/issues/15). 27 | 28 | If you are a Windows user or if you would like more details on `virtualenv`, check out this [guide](http://docs.python-guide.org/en/latest/dev/virtualenvs/). 29 | 30 | Install `virtualenv` and `virtualenvwrapper`, or check out the [Pipsi Installation](#pipsi-installation) section below: 31 | 32 | pip install virtualenv 33 | pip install virtualenvwrapper 34 | export WORKON_HOME=~/.virtualenvs 35 | source /usr/local/bin/virtualenvwrapper.sh 36 | 37 | Create a `SAWS` `virtualenv` and install `SAWS`: 38 | 39 | mkvirtualenv saws 40 | pip install saws 41 | 42 | If you want to activate the `saws` `virtualenv` again later, run: 43 | 44 | workon saws 45 | 46 | ### Pipsi Installation 47 | 48 | [Pipsi](https://github.com/mitsuhiko/pipsi) simplifies the `virtualenv` setup. 49 | 50 | Install `pipsi`: 51 | 52 | pip install pipsi 53 | 54 | Create a `virtualenv` and install `SAWS`: 55 | 56 | pipsi install saws 57 | 58 | For Python 3: 59 | 60 | pipsi install --python=python3 saws 61 | 62 | Note: [Pipsi might not be fully supported on Windows](https://github.com/mitsuhiko/pipsi/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+windows). 63 | 64 | ## Docker Installation 65 | 66 | `SAWS` can be run from docker without additional dependencies. Assuming docker is installed and configured, the docker image can be built by running the following in a directory containing the [Dockerfile](https://github.com/donnemartin/saws/blob/master/Dockerfile): 67 | 68 | docker build -t saws . 69 | 70 | `SAWS` can then be run by: 71 | 72 | docker run -it -e AWS_ACCESS_KEY_ID= -e AWS_SECRET_ACCESS_KEY= -e AWS_DEFAULT_REGION= saws 73 | 74 | Or by mounting a local `.aws` configuration directory: 75 | 76 | docker run -it -v path/to/.aws/:/root/.aws:ro saws 77 | 78 | ## Mac OS X 10.11 El Capitan Users 79 | 80 | 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 saws, you can run: 81 | 82 | $ sudo pip install saws --upgrade --ignore-installed six 83 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | I am providing code and resources in this repository to you under an open source 2 | license. Because this is my personal repository, the license you receive to my 3 | code and resources is from me and not my employer (Facebook). 4 | 5 | Copyright 2015 Donne Martin. All Rights Reserved. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"). You 8 | may not use this file except in compliance with the License. A copy of 9 | the License is located at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | or in the "license" file accompanying this file. This file is 14 | distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 15 | ANY KIND, either express or implied. See the License for the specific 16 | language governing permissions and limitations under the License. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE.txt 3 | include requirements-dev.txt 4 | include saws/data/SOURCES.txt 5 | include saws/data/RESOURCES_SAMPLE.txt 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](http://i.imgur.com/vzC5zmA.gif) 2 | 3 | SAWS 4 | ============ 5 | 6 | [![Build Status](https://travis-ci.org/donnemartin/saws.svg?branch=master)](https://travis-ci.org/donnemartin/saws) [![Documentation Status](https://readthedocs.org/projects/saws/badge/?version=latest)](http://saws.readthedocs.org/en/latest/?badge=latest) [![Dependency Status](https://gemnasium.com/donnemartin/saws.svg)](https://gemnasium.com/donnemartin/saws) 7 | 8 | [![PyPI version](https://badge.fury.io/py/saws.svg)](http://badge.fury.io/py/saws) [![PyPI](https://img.shields.io/pypi/pyversions/saws.svg)](https://pypi.python.org/pypi/saws/) [![License](http://img.shields.io/:license-apache-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0.html) 9 | 10 | ## Motivation 11 | 12 | ### AWS CLI 13 | 14 | Although the [AWS CLI](https://github.com/aws/aws-cli) is a great resource to manage your AWS-powered services, it's **tough to remember usage** of: 15 | 16 | * 70+ top-level commands 17 | * 2000+ subcommands 18 | * Countless command-specific options 19 | * Resources such as instance tags and buckets 20 | 21 | ### SAWS: A Supercharged AWS CLI 22 | 23 | `SAWS` aims to **supercharge** the AWS CLI with features focusing on: 24 | 25 | * **Improving ease-of-use** 26 | * **Increasing productivity** 27 | 28 | Under the hood, `SAWS` is **powered by the AWS CLI** and supports the **same commands** and **command structure**. 29 | 30 | `SAWS` and `AWS CLI` Usage: 31 | 32 | aws [parameters] [options] 33 | 34 | `SAWS` features: 35 | 36 | * Auto-completion of: 37 | * Commands 38 | * Subcommands 39 | * Options 40 | * Auto-completion of resources: 41 | * Bucket names 42 | * Instance ids 43 | * Instance tags 44 | * [More coming soon!](#todo-add-more-resources) 45 | * Customizable shortcuts 46 | * Fuzzy completion of resources and shortcuts 47 | * Fish-style auto-suggestions 48 | * Syntax and output highlighting 49 | * Execution of shell commands 50 | * Command history 51 | * Contextual help 52 | * Toolbar options 53 | 54 | `SAWS` is available for Mac, Linux, Unix, and [Windows](#windows-support). 55 | 56 | ![](http://i.imgur.com/Eo12q9T.png) 57 | 58 | ## Index 59 | 60 | ### Features 61 | 62 | * [Syntax and Output Highlighting](#syntax-and-output-highlighting) 63 | * [Auto-Completion of Commands, Subcommands, and Options](#auto-completion-of-commands-subcommands-and-options) 64 | * [Auto-Completion of AWS Resources](#auto-completion-of-aws-resources) 65 | * [S3 Buckets](#s3-buckets) 66 | * [EC2 Instance Ids](#ec2-instance-ids) 67 | * [EC2 Instance Tags](#ec2-instance-tags) 68 | * [TODO: Add More Resources](#todo-add-more-resources) 69 | * [Customizable Shortcuts](#customizable-shortcuts) 70 | * [Fuzzy Resource and Shortcut Completion](#fuzzy-resource-and-shortcut-completion) 71 | * [Fish-Style Auto-Suggestions](#fish-style-auto-suggestions) 72 | * [Executing Shell Commands](#executing-shell-commands) 73 | * [Command History](#command-history) 74 | * [Contextual Help](#contextual-help) 75 | * [Contextual Command Line Help](#contextual-command-line-help) 76 | * [Contextual Web Docs](#contextual-web-docs) 77 | * [Toolbar Options](#toolbar-options) 78 | * [Windows Support](#windows-support) 79 | 80 | ### Installation and Tests 81 | 82 | * [Installation](#installation) 83 | * [Pip Installation](#pip-installation) 84 | * [Virtual Environment and Docker Installation](#virtual-environment-and-docker-installation) 85 | * [AWS Credentials and Named Profiles](#aws-credentials-and-named-profiles) 86 | * [Supported Python Versions](#supported-python-versions) 87 | * [Supported Platforms](#supported-platforms) 88 | * [Developer Installation](#developer-installation) 89 | * [Continuous Integration](#continuous-integration) 90 | * [Dependencies Management](#dependencies-management) 91 | * [Unit Tests and Code Coverage](#unit-tests-and-code-coverage) 92 | * [Documentation](#documentation) 93 | 94 | ### Misc 95 | 96 | * [Contributing](#contributing) 97 | * [Credits](#credits) 98 | * [Contact Info](#contact-info) 99 | * [License](#license) 100 | 101 | ## Syntax and Output Highlighting 102 | 103 | ![](http://i.imgur.com/xQDpw70.png) 104 | 105 | You can control which theme to load for syntax highlighting by updating your [~/.sawsrc](https://github.com/donnemartin/saws/blob/master/saws/sawsrc) file: 106 | 107 | ``` 108 | # Visual theme. Possible values: manni, igor, xcode, vim, autumn, vs, rrt, 109 | # native, perldoc, borland, tango, emacs, friendly, monokai, paraiso-dark, 110 | # colorful, murphy, bw, pastie, paraiso-light, trac, default, fruity 111 | theme = vim 112 | ``` 113 | 114 | ## Auto-Completion of Commands, Subcommands, and Options 115 | 116 | `SAWS` provides smart autocompletion as you type. Entering the following command will interactively list and auto-complete all subcommands **specific only** to `ec2`: 117 | 118 | aws ec2 119 | 120 | ![](http://i.imgur.com/P2tL9vW.png) 121 | 122 | ## Auto-Completion of AWS Resources 123 | 124 | In addition to the default commands, subcommands, and options the AWS CLI provides, `SAWS` supports auto-completion of your AWS resources. Currently, bucket names, instance ids, and instance tags are included, with additional support for more resources [under development](#todo-add-more-resources). 125 | 126 | ### S3 Buckets 127 | 128 | Option for `s3api`: 129 | 130 | --bucket 131 | 132 | Sample Usage: 133 | 134 | aws s3api get-bucket-acl --bucket 135 | 136 | Syntax for `s3`: 137 | 138 | s3:// 139 | 140 | Sample Usage: 141 | 142 | aws s3 ls s3:// 143 | 144 | Note: The example below demonstrates the use of [fuzzy resource completion](fuzzy-resource-and-shortcutcompletion): 145 | 146 | ![](http://i.imgur.com/39CAS5T.png) 147 | 148 | ### EC2 Instance Ids 149 | 150 | Option for `ec2`: 151 | 152 | --instance-ids 153 | 154 | Sample Usage: 155 | 156 | aws ec2 describe-instances --instance-ids 157 | aws ec2 ls --instance-ids 158 | 159 | Note: The `ls` command demonstrates the use of [customizable shortcuts](#customizable-shortcuts): 160 | 161 | ![](http://i.imgur.com/jFyCSXl.png) 162 | 163 | ### EC2 Instance Tags 164 | 165 | Option for `ec2`: 166 | 167 | --ec2-tag-key 168 | --ec2-tag-value 169 | 170 | Sample Usage: 171 | 172 | aws ec2 ls --ec2-tag-key 173 | aws ec2 ls --ec2-tag-value 174 | 175 | **Tags support wildcards** with the `*` character. 176 | 177 | Note: `ls`, `--ec2-tag-value`, and `--ec2-tag-key` demonstrate the use of [customizable shortcuts](#customizable-shortcuts): 178 | 179 | ![](http://i.imgur.com/VIKwG3Z.png) 180 | 181 | ### TODO: Add More Resources 182 | 183 | Feel free to [submit an issue or a pull request](#contributions) if you'd like support for additional resources. 184 | 185 | ## Customizable Shortcuts 186 | 187 | The [~/.saws.shortcuts](https://github.com/donnemartin/saws/blob/master/saws/saws.shortcuts) file contains shortcuts that you can modify. It comes pre-populated with several [handy shortcuts](https://github.com/donnemartin/saws/blob/master/saws/saws.shortcuts) out of the box. You can combine shortcuts with [fuzzy completion](#fuzzy-resource-and-shortcut-completion) for even less keystrokes. Below are a few examples. 188 | 189 | List all EC2 instances: 190 | 191 | aws ec2 ls 192 | 193 | List all running EC2 instances: 194 | 195 | aws ec2 ls --ec2-state running # fuzzy shortcut: aws ecstate 196 | 197 | ![](http://i.imgur.com/jYFEsoM.png) 198 | 199 | List all EC2 instances with a matching tag (supports wildcards `*`): 200 | 201 | aws ec2 ls --ec2-tag-key # fuzzy shortcut: aws ectagk 202 | aws ec2 ls --ec2-tag-value # fuzzy shortcut: aws ectagv 203 | 204 | ![](http://i.imgur.com/PSuwUIw.png) 205 | 206 | List EC2 instance with matching id: 207 | 208 | aws ec2 ls --instance-ids # fuzzy shortcut: aws eclsi 209 | 210 | ![](http://i.imgur.com/wGcUCsa.png) 211 | 212 | List all DynamoDB tables: 213 | 214 | aws dynamodb ls # fuzzy shortcut: aws dls 215 | 216 | List all EMR clusters: 217 | 218 | aws emr ls # fuzzy shortcut: aws emls 219 | 220 | Add/remove/modify shortcuts in your [~/.saws.shortcuts](https://github.com/donnemartin/saws/blob/master/saws/shortcuts) file to suit your needs. 221 | 222 | Feel free to submit: 223 | 224 | * An issue to request additional shortcuts 225 | * A pull request if you'd like to share your shortcuts (see [contributing guidelines](#contributions)) 226 | 227 | ### Fuzzy Resource and Shortcut Completion 228 | 229 | To toggle fuzzy completion of AWS resources and shortcuts, use `F3` key. 230 | 231 | Sample fuzzy shortcuts to start and stop EC2 instances: 232 | 233 | aws ecstop 234 | aws ecstart 235 | 236 | Note: Fuzzy completion currently only works with AWS [resources](#auto-completion-of-aws-resources) and [shortcuts](customizable-shortcuts). 237 | 238 | ![](http://i.imgur.com/7OvFHCw.png) 239 | 240 | ## Fish-Style Auto-Suggestions 241 | 242 | `SAWS` supports Fish-style auto-suggestions. Use the `right arrow` key to complete a suggestion. 243 | 244 | ![](http://i.imgur.com/t5200q1.png) 245 | 246 | ## Executing Shell Commands 247 | 248 | `SAWS` allows you to execute shell commands from the `saws>` prompt. 249 | 250 | ![](http://i.imgur.com/FiSn6b2.png) 251 | 252 | ## Command History 253 | 254 | `SAWS` keeps track of commands you enter and stores them in `~/.saws-history`. Use the up and down arrow keys to cycle through the command history. 255 | 256 | ![](http://i.imgur.com/z8RrDQB.png) 257 | 258 | ## Contextual Help 259 | 260 | `SAWS` supports contextual command line `help` and contextual web `docs`. 261 | 262 | ### Contextual Command Line Help 263 | 264 | The `help` command is powered by the AWS CLI and outputs help within the command line. 265 | 266 | Usage: 267 | 268 | aws help 269 | 270 | ![](http://i.imgur.com/zSkzt6y.png) 271 | 272 | ### Contextual Web Docs 273 | 274 | Sometimes you're not quite sure what specific command/subcommand/option combination you need to use. In such cases, browsing through several combinations with the `help` command line is cumbersome versus browsing the online AWS CLI docs through a web browser. 275 | 276 | `SAWS` supports contextual web docs with the `docs` command or the `F9` key. `SAWS` will display the web docs specific to the currently entered command and subcommand. 277 | 278 | Usage: 279 | 280 | aws docs 281 | 282 | ![](http://i.imgur.com/zK4IJYp.png) 283 | 284 | ## Toolbar Options 285 | 286 | `SAWS` supports a number of toolbar options: 287 | 288 | * `F2` toggles [output syntax highlighting](#syntax-and-output-highlighting) 289 | * `F3` toggles [fuzzy completion of AWS resources and shortcuts](#fuzzy-resource-and-shortcut-completion) 290 | * `F4` toggles [completion of shortcuts](#customizable-shortcuts) 291 | * `F5` refreshes [resources for auto-completion](#auto-completion-of-aws-resources) 292 | * `F9` displays the [contextual web docs](#contextual-web-docs) 293 | * `F10` or `control d` exits `SAWS` 294 | 295 | ![](http://i.imgur.com/7vz8OSc.png) 296 | 297 | ## Windows Support 298 | 299 | `SAWS` has been tested on Windows 7 and Windows 10. 300 | 301 | On Windows, the [.sawsrc](https://github.com/donnemartin/saws/blob/master/saws/sawsrc) file can be found in `%userprofile%`. For example: 302 | 303 | C:\Users\dmartin\.sawsrc 304 | 305 | Although you can use the standard Windows command prompt, you'll probably have a better experience with either [cmder](https://github.com/cmderdev/cmder) or [conemu](https://github.com/Maximus5/ConEmu). 306 | 307 | ![](http://i.imgur.com/pUwJWck.png) 308 | 309 | ## Installation 310 | 311 | ### Pip Installation 312 | 313 | [![PyPI version](https://badge.fury.io/py/saws.svg)](http://badge.fury.io/py/saws) [![PyPI](https://img.shields.io/pypi/pyversions/saws.svg)](https://pypi.python.org/pypi/saws/) 314 | 315 | `SAWS` is hosted on [PyPI](https://pypi.python.org/pypi/saws). The following command will install `SAWS` along with dependencies such as the [AWS CLI](https://github.com/aws/aws-cli): 316 | 317 | $ pip install saws 318 | 319 | You can also install the latest `SAWS` from GitHub source which can contain changes not yet pushed to PyPI: 320 | 321 | $ pip install git+https://github.com/donnemartin/saws.git 322 | 323 | If you are not installing in a [virtualenv](#virtual-environment-and-docker-installation), run with `sudo`: 324 | 325 | $ sudo pip install saws 326 | 327 | Once installed, start `SAWS`: 328 | 329 | $ saws 330 | 331 | ### Virtual Environment and Docker Installation 332 | 333 | It is recommended that you install Python packages in a [virtualenv](http://docs.python-guide.org/en/latest/dev/virtualenvs/) to avoid potential [issues with dependencies or permissions](https://github.com/donnemartin/saws/issues/15). 334 | 335 | To view `SAWS` `virtualenv` and [Docker](https://www.docker.com/) installation instructions, click [here](https://github.com/donnemartin/saws/blob/master/INSTALLATION.md). 336 | 337 | ### Mac OS X 10.11 El Capitan Users 338 | 339 | 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 saws, you can run: 340 | 341 | $ sudo pip install saws --upgrade --ignore-installed six 342 | 343 | ### AWS Credentials and Named Profiles 344 | 345 | [Configure your credentials](https://github.com/aws/aws-cli#getting-started) with the AWS CLI: 346 | 347 | $ aws configure 348 | 349 | If you'd like to use a specific named profile with `SAWS`, run the following commands on OS X, Linux, or Unix: 350 | 351 | $ export AWS_DEFAULT_PROFILE=user1 352 | $ saws 353 | 354 | Or as a one-liner: 355 | 356 | $ AWS_DEFAULT_PROFILE=user1 saws 357 | 358 | Windows users can run the following commands: 359 | 360 | > set AWS_DEFAULT_PROFILE=user1 361 | > saws 362 | 363 | Command line options for starting `SAWS` with a specific profile are [under development](https://github.com/donnemartin/saws/issues/16). For more details on how to install and configure the AWS CLI, refer to the following [documentation](http://docs.aws.amazon.com/cli/latest/userguide/installing.html). 364 | 365 | ### Supported Python Versions 366 | 367 | * Python 2.6 368 | * Python 2.7 369 | * Python 3.3 370 | * Python 3.4 371 | * Pypy 372 | 373 | Light testing indicates that `SAWS` also seems to be compatible with Python 3.5. 374 | 375 | Pypy3 is not supported due to [lack of support](https://github.com/boto/botocore/issues/622) from [boto](https://github.com/boto/boto). 376 | 377 | ### Supported Platforms 378 | 379 | * Mac OS X 380 | * Tested on OS X 10.10 381 | * Linux, Unix 382 | * Tested on Ubuntu 14.04 LTS 383 | * Windows 384 | * Tested on Windows 7 and 10 385 | 386 | ## Developer Installation 387 | 388 | If you're interested in contributing to `SAWS`, run the following commands: 389 | 390 | $ git clone https://github.com/donnemartin/saws.git 391 | $ pip install -e . 392 | $ pip install -r requirements-dev.txt 393 | $ saws 394 | 395 | ### Continuous Integration 396 | 397 | [![Build Status](https://travis-ci.org/donnemartin/saws.svg?branch=master)](https://travis-ci.org/donnemartin/saws) 398 | 399 | Continuous integration details are available on [Travis CI](https://travis-ci.org/donnemartin/saws). 400 | 401 | ### Dependencies Management 402 | 403 | [![Dependency Status](https://gemnasium.com/donnemartin/saws.svg)](https://gemnasium.com/donnemartin/saws) 404 | 405 | Dependencies management details are available on [Gemnasium](https://gemnasium.com/donnemartin/saws). 406 | 407 | ### Unit Tests and Code Coverage 408 | 409 | Run unit tests in your active Python environment: 410 | 411 | $ python tests/run_tests.py 412 | 413 | Run unit tests with [tox](https://pypi.python.org/pypi/tox) on multiple Python environments: 414 | 415 | $ tox 416 | 417 | ### Documentation 418 | 419 | [![Documentation Status](https://readthedocs.org/projects/saws/badge/?version=latest)](http://saws.readthedocs.org/en/latest/?badge=latest) 420 | 421 | Source code documentation is available on [Readthedocs.org](http://saws.readthedocs.org/en/latest/?badge=latest). 422 | 423 | Run the following to build the docs: 424 | 425 | $ scripts/update_docs.sh 426 | 427 | ## Contributing 428 | 429 | Contributions are welcome! 430 | 431 | Review the [Contributing Guidelines](https://github.com/donnemartin/saws/blob/master/CONTRIBUTING.md) for details on how to: 432 | 433 | * Submit issues 434 | * Submit pull requests 435 | 436 | ## Credits 437 | 438 | * [AWS CLI](https://github.com/aws/aws-cli) by [AWS](https://github.com/aws) for powering `SAWS` under the hood 439 | * [Python Prompt Toolkit](https://github.com/jonathanslenders/python-prompt-toolkit) by [jonathanslenders](https://github.com/jonathanslenders) for simplifying the creation of `SAWS` 440 | * [Wharfee](https://github.com/j-bennet/wharfee) by [j-bennet](https://github.com/j-bennet) for inspiring the creation of `SAWS` and for some handy utility functions 441 | 442 | ## Contact Info 443 | 444 | Feel free to contact me to discuss any issues, questions, or comments. 445 | 446 | * Email: [donne.martin@gmail.com](mailto:donne.martin@gmail.com) 447 | * Twitter: [donne_martin](https://twitter.com/donne_martin) 448 | * GitHub: [donnemartin](https://github.com/donnemartin) 449 | * LinkedIn: [donnemartin](https://www.linkedin.com/in/donnemartin) 450 | * Website: [donnemartin.com](http://donnemartin.com) 451 | 452 | ## License 453 | 454 | *I am providing code and resources in this repository to you under an open source license. Because this is my personal repository, the license you receive to my code and resources is from me and not my employer (Facebook).* 455 | 456 | Copyright 2015 Donne Martin 457 | 458 | Licensed under the Apache License, Version 2.0 (the "License"); 459 | you may not use this file except in compliance with the License. 460 | You may obtain a copy of the License at 461 | 462 | http://www.apache.org/licenses/LICENSE-2.0 463 | 464 | Unless required by applicable law or agreed to in writing, software 465 | distributed under the License is distributed on an "AS IS" BASIS, 466 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 467 | See the License for the specific language governing permissions and 468 | limitations under the License. 469 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # What Python version is installed where: 2 | # http://www.appveyor.com/docs/installed-software#python 3 | 4 | environment: 5 | matrix: 6 | - PYTHON: "C:\\Python27" 7 | TOX_ENV: "py27" 8 | 9 | - PYTHON: "C:\\Python33" 10 | TOX_ENV: "py33" 11 | 12 | - PYTHON: "C:\\Python34" 13 | TOX_ENV: "py34" 14 | 15 | - PYTHON: "C:\\Python35" 16 | TOX_ENV: "py35" 17 | 18 | 19 | init: 20 | - "%PYTHON%/python -V" 21 | - "%PYTHON%/python -c \"import struct;print( 8 * struct.calcsize(\'P\'))\"" 22 | 23 | install: 24 | - "%PYTHON%/Scripts/easy_install -U pip" 25 | - "%PYTHON%/Scripts/pip install tox" 26 | - "%PYTHON%/Scripts/pip install wheel" 27 | 28 | build: false # Not a C# project, build stuff at the test step instead. 29 | 30 | test_script: 31 | - "%PYTHON%/Scripts/tox -e %TOX_ENV%" 32 | 33 | after_test: 34 | - "%PYTHON%/python setup.py bdist_wheel" 35 | - ps: "ls dist" 36 | 37 | artifacts: 38 | - path: dist\* 39 | 40 | #on_success: 41 | # - TODO: upload the content of dist/*.whl to a public wheelhouse -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/saws.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/saws.qhc" 93 | 94 | applehelp: 95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 96 | @echo 97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 98 | @echo "N.B. You won't be able to view it unless you put it in" \ 99 | "~/Library/Documentation/Help or install it in your application" \ 100 | "bundle." 101 | 102 | devhelp: 103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 104 | @echo 105 | @echo "Build finished." 106 | @echo "To view the help file:" 107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/saws" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/saws" 109 | @echo "# devhelp" 110 | 111 | epub: 112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 113 | @echo 114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 115 | 116 | latex: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo 119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 121 | "(use \`make latexpdf' here to do that automatically)." 122 | 123 | latexpdf: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo "Running LaTeX files through pdflatex..." 126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 128 | 129 | latexpdfja: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo "Running LaTeX files through platex and dvipdfmx..." 132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 134 | 135 | text: 136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 137 | @echo 138 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 139 | 140 | man: 141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 142 | @echo 143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 144 | 145 | texinfo: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo 148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 149 | @echo "Run \`make' in that directory to run these through makeinfo" \ 150 | "(use \`make info' here to do that automatically)." 151 | 152 | info: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo "Running Texinfo files through makeinfo..." 155 | make -C $(BUILDDIR)/texinfo info 156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 157 | 158 | gettext: 159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 160 | @echo 161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 162 | 163 | changes: 164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 165 | @echo 166 | @echo "The overview file is in $(BUILDDIR)/changes." 167 | 168 | linkcheck: 169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 170 | @echo 171 | @echo "Link check complete; look for any errors in the above output " \ 172 | "or in $(BUILDDIR)/linkcheck/output.txt." 173 | 174 | doctest: 175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 176 | @echo "Testing of doctests in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/doctest/output.txt." 178 | 179 | coverage: 180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 181 | @echo "Testing of coverage in the sources finished, look at the " \ 182 | "results in $(BUILDDIR)/coverage/python.txt." 183 | 184 | xml: 185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 186 | @echo 187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 188 | 189 | pseudoxml: 190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 191 | @echo 192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 193 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source 10 | set I18NSPHINXOPTS=%SPHINXOPTS% source 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | echo. coverage to run coverage check of the documentation if enabled 41 | goto end 42 | ) 43 | 44 | if "%1" == "clean" ( 45 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 46 | del /q /s %BUILDDIR%\* 47 | goto end 48 | ) 49 | 50 | 51 | REM Check if sphinx-build is available and fallback to Python version if any 52 | %SPHINXBUILD% 2> nul 53 | if errorlevel 9009 goto sphinx_python 54 | goto sphinx_ok 55 | 56 | :sphinx_python 57 | 58 | set SPHINXBUILD=python -m sphinx.__init__ 59 | %SPHINXBUILD% 2> nul 60 | if errorlevel 9009 ( 61 | echo. 62 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 63 | echo.installed, then set the SPHINXBUILD environment variable to point 64 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 65 | echo.may add the Sphinx directory to PATH. 66 | echo. 67 | echo.If you don't have Sphinx installed, grab it from 68 | echo.http://sphinx-doc.org/ 69 | exit /b 1 70 | ) 71 | 72 | :sphinx_ok 73 | 74 | 75 | if "%1" == "html" ( 76 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 80 | goto end 81 | ) 82 | 83 | if "%1" == "dirhtml" ( 84 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 88 | goto end 89 | ) 90 | 91 | if "%1" == "singlehtml" ( 92 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 93 | if errorlevel 1 exit /b 1 94 | echo. 95 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 96 | goto end 97 | ) 98 | 99 | if "%1" == "pickle" ( 100 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 101 | if errorlevel 1 exit /b 1 102 | echo. 103 | echo.Build finished; now you can process the pickle files. 104 | goto end 105 | ) 106 | 107 | if "%1" == "json" ( 108 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 109 | if errorlevel 1 exit /b 1 110 | echo. 111 | echo.Build finished; now you can process the JSON files. 112 | goto end 113 | ) 114 | 115 | if "%1" == "htmlhelp" ( 116 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 117 | if errorlevel 1 exit /b 1 118 | echo. 119 | echo.Build finished; now you can run HTML Help Workshop with the ^ 120 | .hhp project file in %BUILDDIR%/htmlhelp. 121 | goto end 122 | ) 123 | 124 | if "%1" == "qthelp" ( 125 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 129 | .qhcp project file in %BUILDDIR%/qthelp, like this: 130 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\saws.qhcp 131 | echo.To view the help file: 132 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\saws.ghc 133 | goto end 134 | ) 135 | 136 | if "%1" == "devhelp" ( 137 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. 141 | goto end 142 | ) 143 | 144 | if "%1" == "epub" ( 145 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 149 | goto end 150 | ) 151 | 152 | if "%1" == "latex" ( 153 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 157 | goto end 158 | ) 159 | 160 | if "%1" == "latexpdf" ( 161 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 162 | cd %BUILDDIR%/latex 163 | make all-pdf 164 | cd %~dp0 165 | echo. 166 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdfja" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf-ja 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "text" ( 181 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 182 | if errorlevel 1 exit /b 1 183 | echo. 184 | echo.Build finished. The text files are in %BUILDDIR%/text. 185 | goto end 186 | ) 187 | 188 | if "%1" == "man" ( 189 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 190 | if errorlevel 1 exit /b 1 191 | echo. 192 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 193 | goto end 194 | ) 195 | 196 | if "%1" == "texinfo" ( 197 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 198 | if errorlevel 1 exit /b 1 199 | echo. 200 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 201 | goto end 202 | ) 203 | 204 | if "%1" == "gettext" ( 205 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 206 | if errorlevel 1 exit /b 1 207 | echo. 208 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 209 | goto end 210 | ) 211 | 212 | if "%1" == "changes" ( 213 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 214 | if errorlevel 1 exit /b 1 215 | echo. 216 | echo.The overview file is in %BUILDDIR%/changes. 217 | goto end 218 | ) 219 | 220 | if "%1" == "linkcheck" ( 221 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 222 | if errorlevel 1 exit /b 1 223 | echo. 224 | echo.Link check complete; look for any errors in the above output ^ 225 | or in %BUILDDIR%/linkcheck/output.txt. 226 | goto end 227 | ) 228 | 229 | if "%1" == "doctest" ( 230 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Testing of doctests in the sources finished, look at the ^ 234 | results in %BUILDDIR%/doctest/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "coverage" ( 239 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of coverage in the sources finished, look at the ^ 243 | results in %BUILDDIR%/coverage/python.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "xml" ( 248 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 252 | goto end 253 | ) 254 | 255 | if "%1" == "pseudoxml" ( 256 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 257 | if errorlevel 1 exit /b 1 258 | echo. 259 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 260 | goto end 261 | ) 262 | 263 | :end 264 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # saws documentation build configuration file, created by 4 | # sphinx-quickstart on Fri Sep 11 16:09:11 2015. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | import shlex 18 | 19 | 20 | # If extensions (or modules to document with autodoc) are in another directory, 21 | # add these directories to sys.path here. If the directory is relative to the 22 | # documentation root, use os.path.abspath to make it absolute, like shown here. 23 | sys.path.insert(0, os.path.abspath('../..')) 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | #needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'sphinx.ext.autodoc', 35 | ] 36 | 37 | # Add any paths that contain templates here, relative to this directory. 38 | templates_path = ['_templates'] 39 | 40 | # The suffix(es) of source filenames. 41 | # You can specify multiple suffix as a list of string: 42 | # source_suffix = ['.rst', '.md'] 43 | source_suffix = '.rst' 44 | 45 | # The encoding of source files. 46 | #source_encoding = 'utf-8-sig' 47 | 48 | # The master toctree document. 49 | master_doc = 'index' 50 | 51 | # General information about the project. 52 | project = u'saws' 53 | copyright = u'2015, Donne Martin' 54 | author = u'Donne Martin' 55 | 56 | # The version info for the project you're documenting, acts as replacement for 57 | # |version| and |release|, also used in various other places throughout the 58 | # built documents. 59 | # 60 | # The short X.Y version. 61 | version = '0.1.0' 62 | # The full version, including alpha/beta/rc tags. 63 | release = '0.1.0' 64 | 65 | # The language for content autogenerated by Sphinx. Refer to documentation 66 | # for a list of supported languages. 67 | # 68 | # This is also used if you do content translation via gettext catalogs. 69 | # Usually you set "language" from the command line for these cases. 70 | language = None 71 | 72 | # There are two options for replacing |today|: either, you set today to some 73 | # non-false value, then it is used: 74 | #today = '' 75 | # Else, today_fmt is used as the format for a strftime call. 76 | #today_fmt = '%B %d, %Y' 77 | 78 | # List of patterns, relative to source directory, that match files and 79 | # directories to ignore when looking for source files. 80 | exclude_patterns = [] 81 | 82 | # The reST default role (used for this markup: `text`) to use for all 83 | # documents. 84 | #default_role = None 85 | 86 | # If true, '()' will be appended to :func: etc. cross-reference text. 87 | #add_function_parentheses = True 88 | 89 | # If true, the current module name will be prepended to all description 90 | # unit titles (such as .. function::). 91 | #add_module_names = True 92 | 93 | # If true, sectionauthor and moduleauthor directives will be shown in the 94 | # output. They are ignored by default. 95 | #show_authors = False 96 | 97 | # The name of the Pygments (syntax highlighting) style to use. 98 | pygments_style = 'sphinx' 99 | 100 | # A list of ignored prefixes for module index sorting. 101 | #modindex_common_prefix = [] 102 | 103 | # If true, keep warnings as "system message" paragraphs in the built documents. 104 | #keep_warnings = False 105 | 106 | # If true, `todo` and `todoList` produce output, else they produce nothing. 107 | todo_include_todos = False 108 | 109 | 110 | # -- Options for HTML output ---------------------------------------------- 111 | 112 | # The theme to use for HTML and HTML Help pages. See the documentation for 113 | # a list of builtin themes. 114 | html_theme = 'alabaster' 115 | 116 | # Theme options are theme-specific and customize the look and feel of a theme 117 | # further. For a list of options available for each theme, see the 118 | # documentation. 119 | #html_theme_options = {} 120 | 121 | # Add any paths that contain custom themes here, relative to this directory. 122 | #html_theme_path = [] 123 | 124 | # The name for this set of Sphinx documents. If None, it defaults to 125 | # " v documentation". 126 | #html_title = None 127 | 128 | # A shorter title for the navigation bar. Default is the same as html_title. 129 | #html_short_title = None 130 | 131 | # The name of an image file (relative to this directory) to place at the top 132 | # of the sidebar. 133 | #html_logo = None 134 | 135 | # The name of an image file (within the static path) to use as favicon of the 136 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 137 | # pixels large. 138 | #html_favicon = None 139 | 140 | # Add any paths that contain custom static files (such as style sheets) here, 141 | # relative to this directory. They are copied after the builtin static files, 142 | # so a file named "default.css" will overwrite the builtin "default.css". 143 | html_static_path = ['_static'] 144 | 145 | # Add any extra paths that contain custom files (such as robots.txt or 146 | # .htaccess) here, relative to this directory. These files are copied 147 | # directly to the root of the documentation. 148 | #html_extra_path = [] 149 | 150 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 151 | # using the given strftime format. 152 | #html_last_updated_fmt = '%b %d, %Y' 153 | 154 | # If true, SmartyPants will be used to convert quotes and dashes to 155 | # typographically correct entities. 156 | #html_use_smartypants = True 157 | 158 | # Custom sidebar templates, maps document names to template names. 159 | #html_sidebars = {} 160 | 161 | # Additional templates that should be rendered to pages, maps page names to 162 | # template names. 163 | #html_additional_pages = {} 164 | 165 | # If false, no module index is generated. 166 | #html_domain_indices = True 167 | 168 | # If false, no index is generated. 169 | #html_use_index = True 170 | 171 | # If true, the index is split into individual pages for each letter. 172 | #html_split_index = False 173 | 174 | # If true, links to the reST sources are added to the pages. 175 | #html_show_sourcelink = True 176 | 177 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 178 | #html_show_sphinx = True 179 | 180 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 181 | #html_show_copyright = True 182 | 183 | # If true, an OpenSearch description file will be output, and all pages will 184 | # contain a tag referring to it. The value of this option must be the 185 | # base URL from which the finished HTML is served. 186 | #html_use_opensearch = '' 187 | 188 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 189 | #html_file_suffix = None 190 | 191 | # Language to be used for generating the HTML full-text search index. 192 | # Sphinx supports the following languages: 193 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 194 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 195 | #html_search_language = 'en' 196 | 197 | # A dictionary with options for the search language support, empty by default. 198 | # Now only 'ja' uses this config value 199 | #html_search_options = {'type': 'default'} 200 | 201 | # The name of a javascript file (relative to the configuration directory) that 202 | # implements a search results scorer. If empty, the default will be used. 203 | #html_search_scorer = 'scorer.js' 204 | 205 | # Output file base name for HTML help builder. 206 | htmlhelp_basename = 'sawsdoc' 207 | 208 | # -- Options for LaTeX output --------------------------------------------- 209 | 210 | latex_elements = { 211 | # The paper size ('letterpaper' or 'a4paper'). 212 | #'papersize': 'letterpaper', 213 | 214 | # The font size ('10pt', '11pt' or '12pt'). 215 | #'pointsize': '10pt', 216 | 217 | # Additional stuff for the LaTeX preamble. 218 | #'preamble': '', 219 | 220 | # Latex figure (float) alignment 221 | #'figure_align': 'htbp', 222 | } 223 | 224 | # Grouping the document tree into LaTeX files. List of tuples 225 | # (source start file, target name, title, 226 | # author, documentclass [howto, manual, or own class]). 227 | latex_documents = [ 228 | (master_doc, 'saws.tex', u'saws Documentation', 229 | u'Donne Martin', 'manual'), 230 | ] 231 | 232 | # The name of an image file (relative to this directory) to place at the top of 233 | # the title page. 234 | #latex_logo = None 235 | 236 | # For "manual" documents, if this is true, then toplevel headings are parts, 237 | # not chapters. 238 | #latex_use_parts = False 239 | 240 | # If true, show page references after internal links. 241 | #latex_show_pagerefs = False 242 | 243 | # If true, show URL addresses after external links. 244 | #latex_show_urls = False 245 | 246 | # Documents to append as an appendix to all manuals. 247 | #latex_appendices = [] 248 | 249 | # If false, no module index is generated. 250 | #latex_domain_indices = True 251 | 252 | 253 | # -- Options for manual page output --------------------------------------- 254 | 255 | # One entry per manual page. List of tuples 256 | # (source start file, name, description, authors, manual section). 257 | man_pages = [ 258 | (master_doc, 'saws', u'saws Documentation', 259 | [author], 1) 260 | ] 261 | 262 | # If true, show URL addresses after external links. 263 | #man_show_urls = False 264 | 265 | 266 | # -- Options for Texinfo output ------------------------------------------- 267 | 268 | # Grouping the document tree into Texinfo files. List of tuples 269 | # (source start file, target name, title, author, 270 | # dir menu entry, description, category) 271 | texinfo_documents = [ 272 | (master_doc, 'saws', u'saws Documentation', 273 | author, 'saws', 'One line description of project.', 274 | 'Miscellaneous'), 275 | ] 276 | 277 | # Documents to append as an appendix to all manuals. 278 | #texinfo_appendices = [] 279 | 280 | # If false, no module index is generated. 281 | #texinfo_domain_indices = True 282 | 283 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 284 | #texinfo_show_urls = 'footnote' 285 | 286 | # If true, do not generate a @detailmenu in the "Top" node's menu. 287 | #texinfo_no_detailmenu = False 288 | 289 | # http://stackoverflow.com/questions/12772927/specifying-an-online-image-in-sphinx-restructuredtext-format 290 | # "Monkey patch" sphinx to omit 'nonlocal image URI found' 291 | import sphinx.environment 292 | from docutils.utils import get_source_line 293 | 294 | def _warn_node(self, msg, node, **kwargs): 295 | if not msg.startswith('nonlocal image URI found:'): 296 | self._warnfunc(msg, '%s:%s' % get_source_line(node), **kwargs) 297 | 298 | sphinx.environment.BuildEnvironment.warn_node = _warn_node 299 | 300 | # http://stackoverflow.com/questions/5599254/how-to-use-sphinxs-autodoc-to-document-a-classs-init-self-method 301 | # Autodoc a class's __init__(self) method 302 | def skip(app, what, name, obj, skip, options): 303 | if name == "__init__": 304 | return False 305 | return skip 306 | 307 | def setup(app): 308 | app.connect("autodoc-skip-member", skip) -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. saws documentation master file, created by 2 | sphinx-quickstart on Fri Sep 11 16:09:11 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | SAWS: A Supercharged AWS Command Line Interface (CLI) 7 | ===================================================== 8 | 9 | Source Code 10 | ----------- 11 | 12 | * `GitHub Repo `__ 13 | * :ref:`genindex` 14 | * :ref:`modindex` 15 | * :ref:`search` 16 | | 17 | 18 | .. include:: README.rst 19 | 20 | .. toctree:: 21 | :maxdepth: 2 22 | -------------------------------------------------------------------------------- /docs/source/modules.rst: -------------------------------------------------------------------------------- 1 | saws 2 | ==== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | saws 8 | -------------------------------------------------------------------------------- /docs/source/saws.rst: -------------------------------------------------------------------------------- 1 | saws package 2 | ============ 3 | 4 | Submodules 5 | ---------- 6 | 7 | saws.commands module 8 | -------------------- 9 | 10 | .. automodule:: saws.commands 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | saws.completer module 16 | --------------------- 17 | 18 | .. automodule:: saws.completer 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | saws.config module 24 | ------------------ 25 | 26 | .. automodule:: saws.config 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | saws.keys module 32 | ---------------- 33 | 34 | .. automodule:: saws.keys 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | saws.lexer module 40 | ----------------- 41 | 42 | .. automodule:: saws.lexer 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | saws.logger module 48 | ------------------ 49 | 50 | .. automodule:: saws.logger 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | saws.main module 56 | ---------------- 57 | 58 | .. automodule:: saws.main 59 | :members: 60 | :undoc-members: 61 | :show-inheritance: 62 | 63 | saws.resources module 64 | --------------------- 65 | 66 | .. automodule:: saws.resources 67 | :members: 68 | :undoc-members: 69 | :show-inheritance: 70 | 71 | saws.saws module 72 | ---------------- 73 | 74 | .. automodule:: saws.saws 75 | :members: 76 | :undoc-members: 77 | :show-inheritance: 78 | 79 | saws.style module 80 | ----------------- 81 | 82 | .. automodule:: saws.style 83 | :members: 84 | :undoc-members: 85 | :show-inheritance: 86 | 87 | saws.toolbar module 88 | ------------------- 89 | 90 | .. automodule:: saws.toolbar 91 | :members: 92 | :undoc-members: 93 | :show-inheritance: 94 | 95 | saws.utils module 96 | ----------------- 97 | 98 | .. automodule:: saws.utils 99 | :members: 100 | :undoc-members: 101 | :show-inheritance: 102 | 103 | 104 | Module contents 105 | --------------- 106 | 107 | .. automodule:: saws 108 | :members: 109 | :undoc-members: 110 | :show-inheritance: 111 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | codecov>=1.3.3 2 | flake8>=2.4.1 3 | mock>=1.0.1 4 | pexpect>=3.3 5 | pytest>=2.7.0 6 | tox>=1.9.2 7 | sphinx>=1.3.1 8 | Sphinx-PyPI-upload>=0.2.1 9 | gitchangelog>=2.2.1 10 | -------------------------------------------------------------------------------- /saws/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2015 Donne Martin. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"). You 6 | # may not use this file except in compliance with the License. A copy of 7 | # the License is located at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # or in the "license" file accompanying this file. This file is 12 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 13 | # ANY KIND, either express or implied. See the License for the specific 14 | # language governing permissions and limitations under the License. 15 | 16 | __version__ = '0.4.2' 17 | -------------------------------------------------------------------------------- /saws/commands.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2015 Donne Martin. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"). You 6 | # may not use this file except in compliance with the License. A copy of 7 | # the License is located at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # or in the "license" file accompanying this file. This file is 12 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 13 | # ANY KIND, either express or implied. See the License for the specific 14 | # language governing permissions and limitations under the License. 15 | 16 | from __future__ import unicode_literals 17 | from __future__ import print_function 18 | import os 19 | from enum import Enum 20 | from .data_util import DataUtil 21 | 22 | 23 | class AwsCommands(object): 24 | """Encapsulates AWS commands. 25 | 26 | All commands are listed in the periodically updated data/SOURCES.txt file. 27 | 28 | Attributes: 29 | * AWS_COMMAND: A string representing the 'aws' command. 30 | * AWS_CONFIGURE: A string representing the 'configure' command. 31 | * AWS_HELP: A string representing the 'help' command. 32 | * AWS_DOCS: A string representing the 'docs' command. 33 | * DATA_DIR: A string representing the directory containing 34 | data/SOURCES.txt. 35 | * DATA_PATH: A string representing the full file path of 36 | data/SOURCES.txt. 37 | * data_util: An instance of DataUtil(). 38 | * headers: A list denoting the start of each set of command types. 39 | * header_to_type_map: A dictionary mapping between headers and 40 | CommandType. 41 | * all_commands: A list of all commands, sub_commands, options, etc 42 | from data/SOURCES.txt. 43 | """ 44 | 45 | class CommandType(Enum): 46 | """Enum specifying the command type. 47 | 48 | Attributes: 49 | * COMMANDS: An int representing commands. 50 | * SUB_COMMANDS: An int representing subcommands. 51 | * GLOBAL_OPTIONS: An int representing global options. 52 | * RESOURCE_OPTIONS: An int representing resource options. 53 | * NUM_TYPES: An int representing the number of command types. 54 | """ 55 | 56 | NUM_TYPES = 4 57 | COMMANDS, SUB_COMMANDS, GLOBAL_OPTIONS, RESOURCE_OPTIONS = \ 58 | range(NUM_TYPES) 59 | 60 | AWS_COMMAND = 'aws' 61 | AWS_CONFIGURE = 'configure' 62 | AWS_HELP = 'help' 63 | AWS_DOCS = 'docs' 64 | DATA_DIR = os.path.dirname(os.path.realpath(__file__)) 65 | DATA_PATH = os.path.join(DATA_DIR, 'data/SOURCES.txt') 66 | 67 | def __init__(self): 68 | self.data_util = DataUtil() 69 | self.headers = ['[commands]: ', 70 | '[sub_commands]: ', 71 | '[global_options]: ', 72 | '[resource_options]: '] 73 | self.header_to_type_map = self.data_util.create_header_to_type_map( 74 | headers=self.headers, 75 | data_type=self.CommandType) 76 | self.all_commands = self.data_util.get_data( 77 | data_file_path=self.DATA_PATH, 78 | header_to_type_map=self.header_to_type_map, 79 | data_type=self.CommandType) 80 | -------------------------------------------------------------------------------- /saws/completer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2015 Donne Martin. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"). You 6 | # may not use this file except in compliance with the License. A copy of 7 | # the License is located at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # or in the "license" file accompanying this file. This file is 12 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 13 | # ANY KIND, either express or implied. See the License for the specific 14 | # language governing permissions and limitations under the License. 15 | 16 | from __future__ import unicode_literals 17 | from __future__ import print_function 18 | import re 19 | import sys 20 | import traceback 21 | from six.moves import cStringIO 22 | from prompt_toolkit.completion import Completer 23 | from .utils import TextUtils 24 | from .commands import AwsCommands 25 | from .options import AwsOptions 26 | from .resources import AwsResources 27 | 28 | 29 | class AwsCompleter(Completer): 30 | """Completer for AWS commands, subcommands, options, and parameters. 31 | 32 | Attributes: 33 | * aws_completer: An instance of the official awscli Completer. 34 | * aws_completions: A set of completions to show the user. 35 | * all_commands: A list of all commands, sub_commands, options, etc 36 | from data/SOURCES.txt. 37 | * config: An instance of Config. 38 | * config_obj: An instance of ConfigObj, reads from ~/.sawsrc. 39 | * log_exception: A callable log_exception from SawsLogger. 40 | * text_utils: An instance of TextUtils. 41 | * fuzzy_match: A boolean that determines whether to use fuzzy matching. 42 | * shortcut_match: A boolean that determines whether to match shortcuts. 43 | * BASE_COMMAND: A string representing the 'aws' command. 44 | * shortcuts: An OrderedDict containing shortcuts commands as keys 45 | and their corresponding full commands as values. 46 | * resources: An instance of AwsResources. 47 | * options: An instance of AwsOptions 48 | """ 49 | 50 | def __init__(self, 51 | aws_completer, 52 | all_commands, 53 | config, 54 | config_obj, 55 | log_exception, 56 | fuzzy_match=False, 57 | shortcut_match=False): 58 | """Initializes AwsCompleter. 59 | 60 | Args: 61 | * aws_completer: The official aws cli completer module. 62 | * all_commands: A list of all commands, sub_commands, options, etc 63 | from data/SOURCES.txt. 64 | * config: An instance of Config. 65 | * config_obj: An instance of ConfigObj, reads from ~/.sawsrc. 66 | * log_exception: A callable log_exception from SawsLogger. 67 | * fuzzy_match: A boolean that determines whether to use 68 | fuzzy matching. 69 | * shortcut_match: A boolean that determines whether to 70 | match shortcuts. 71 | 72 | Returns: 73 | None. 74 | """ 75 | self.aws_completer = aws_completer 76 | self.aws_completions = set() 77 | self.all_commands = all_commands 78 | self.config = config 79 | self.config_obj = config_obj 80 | self.log_exception = log_exception 81 | self.text_utils = TextUtils() 82 | self.fuzzy_match = fuzzy_match 83 | self.shortcut_match = shortcut_match 84 | self.BASE_COMMAND = AwsCommands.AWS_COMMAND 85 | self.shortcuts = self.config.get_shortcuts(config_obj) 86 | self.resources = AwsResources(self.log_exception) 87 | self.options = AwsOptions(self.all_commands) 88 | 89 | def get_completions(self, document, _): 90 | """Get completions for the current scope. 91 | 92 | Args: 93 | * document: An instance of prompt_toolkit's Document. 94 | * _: An instance of prompt_toolkit's CompleteEvent (not used). 95 | 96 | Returns: 97 | A generator of prompt_toolkit's Completion objects, containing 98 | matched completions. 99 | """ 100 | # Get completions from the official AWS CLI 101 | aws_completer_results_list = self._get_aws_cli_completions(document) 102 | self.aws_completions = set() 103 | if len(document.text) < len(self.BASE_COMMAND): 104 | # Autocomplete 'aws' at the beginning of the command 105 | self.aws_completions.update([self.BASE_COMMAND]) 106 | else: 107 | self.aws_completions.update(aws_completer_results_list) 108 | word_before_cursor = document.get_word_before_cursor(WORD=True) 109 | words = self.text_utils.get_tokens(document.text) 110 | if len(words) == 0: 111 | return [] 112 | # Determine if we should insert shortcuts 113 | elif len(words) == 2 and \ 114 | words[0] == self.BASE_COMMAND and \ 115 | word_before_cursor != '': 116 | # Insert shortcuts if the user typed 'aws' as the first 117 | # command and is inputting the subcommand 118 | if self.shortcut_match: 119 | self.aws_completions.update(self.shortcuts.keys()) 120 | # Try to get completions for enabled AWS resources 121 | completions = self._get_custom_completions( 122 | words, word_before_cursor, self.resources.resources_options_map) 123 | # Try to get completions for global options, filter options, etc 124 | if completions is None: 125 | completions = self._get_custom_completions( 126 | words, word_before_cursor, self.options.options_map) 127 | # Try to get completions from the official AWS CLI 128 | if completions is None: 129 | fuzzy_aws_completions = self.fuzzy_match 130 | if self.fuzzy_match and word_before_cursor in \ 131 | self.all_commands[AwsCommands.CommandType.COMMANDS.value]: 132 | # Fuzzy completion currently only works with resources, options 133 | # and shortcuts. If we have just completed a top-level 134 | # command (ie. ec2, elb, s3) then disable fuzzy completions, 135 | # otherwise the corresponding subcommands will be fuzzy 136 | # completed and incorrectly shown. 137 | # See: https://github.com/donnemartin/saws/issues/14 138 | fuzzy_aws_completions = False 139 | completions = self.text_utils.find_matches(word_before_cursor, 140 | self.aws_completions, 141 | fuzzy_aws_completions) 142 | return completions 143 | 144 | def refresh_resources_and_options(self, force_refresh=False): 145 | """Convenience function to refresh resources for completion. 146 | 147 | Args: 148 | * force_refresh: A boolean determines whether to force a cache 149 | refresh. This value is set to True when the user presses `F5`. 150 | 151 | Returns: 152 | None. 153 | """ 154 | self.resources.refresh(force_refresh) 155 | 156 | def replace_shortcut(self, text): 157 | """Replaces matched shortcut commands with their full command. 158 | 159 | Currently, only one shortcut is replaced before shortcut replacement 160 | terminates, although this function could potentially be extended 161 | to replace mutliple shortcuts. 162 | 163 | Args: 164 | * text: A string representing the input command text to replace. 165 | 166 | Returns: 167 | A string representing input command text with a shortcut 168 | replaced, if one has been found. 169 | """ 170 | for key, value in self.shortcuts.items(): 171 | if key in text: 172 | text = re.sub(key, value, text) 173 | text = self.replace_substitution(text) 174 | break 175 | return text 176 | 177 | def replace_substitution(self, text): 178 | """Replaces a `%s` with the word immediately following it. 179 | 180 | Currently, only one substitution is done before replacement terminates, 181 | although this function could potentially be extended to do multiple 182 | substitutions. 183 | 184 | Args: 185 | * text: A string representing the input command text to replace. 186 | 187 | Returns: 188 | A string representing input command text with a substitution, 189 | if one has been found. 190 | """ 191 | substitution_marker = '%s' 192 | 193 | if substitution_marker in text: 194 | tokens = text.split() 195 | replacement_index = self.text_utils.get_token_index( 196 | substitution_marker, tokens) + 1 197 | try: 198 | replacement_text = tokens.pop(replacement_index) 199 | text = ' '.join(tokens) 200 | text = re.sub(substitution_marker, replacement_text, text) 201 | except: 202 | return text 203 | return text 204 | 205 | def _get_resource_completions(self, words, word_before_cursor, 206 | option_text, resource): 207 | """Get completions for the specified AWS resource. 208 | 209 | Args: 210 | * words: A list of words that represent the input command text. 211 | * word_before_cursor: A string representing the word before the 212 | cursor. 213 | * option_text: A string to match that represents the resource's 214 | option text, such as '--ec2-state'. For example, if 215 | option_text is '--ec2-state' and it is the word before the 216 | cursor, then display associated resource completions found 217 | in the resource parameter such as pending, running, etc. 218 | * resource: A list that represents the resource completions to 219 | display if option_text is matched. For example, instance ids, 220 | instance tags, etc. 221 | 222 | Returns: 223 | A generator of prompt_toolkit's Completion objects, containing 224 | matched completions. 225 | """ 226 | if len(words) <= 1: 227 | return 228 | # Example: --bucket 229 | option_text_match = (words[-1] == option_text) 230 | # Example: --bucket prod 231 | completing_with_space = \ 232 | (words[-2] == option_text and word_before_cursor != '') 233 | # Example: s3://prod 234 | completing_no_space = \ 235 | (option_text in words[-1] and word_before_cursor != '') 236 | if option_text_match or completing_with_space or completing_no_space: 237 | return self.text_utils.find_matches(word_before_cursor, 238 | resource, 239 | self.fuzzy_match) 240 | 241 | def _get_aws_cli_completions(self, document): 242 | """Get completions from the official AWS CLI for the current scope. 243 | 244 | Args: 245 | * document: An instance of prompt_toolkit's Document. 246 | 247 | Returns: 248 | A list of string completions. 249 | """ 250 | text = self.replace_shortcut(document.text) 251 | # Redirect stdout to a string so we can capture the AWS CLI 252 | # autocompleter results 253 | # See: http://stackoverflow.com/a/1218951 254 | old_stdout = sys.stdout 255 | sys.stdout = mystdout = cStringIO() 256 | try: 257 | self.aws_completer.complete(text, len(text)) 258 | except Exception as e: 259 | self.log_exception(e, traceback) 260 | sys.stdout = old_stdout 261 | aws_completer_results = mystdout.getvalue() 262 | # Tidy up the completions and store it in a list 263 | aws_completer_results = re.sub('\n', '', aws_completer_results) 264 | aws_completer_results_list = aws_completer_results.split() 265 | return aws_completer_results_list 266 | 267 | def _get_custom_completions(self, words, word_before_cursor, mapping): 268 | """Get custom completions resources, options, etc. 269 | 270 | Completions for all enabled AWS resources, global options, 271 | filter options, etc. 272 | 273 | Args: 274 | * words: A list of strings for each word in the command text. 275 | * word_before_cursor: A string representing the word before 276 | the cursor. 277 | * mapping: A dict mapping of keyword triggers to completions 278 | Example: 279 | key: --bucket, value: list of bucket names 280 | key: --ec2-state, value: list of ec2 states 281 | 282 | Returns: 283 | A generator of prompt_toolkit's Completion objects, containing 284 | matched completions. 285 | """ 286 | completions = None 287 | for key, value in mapping.items(): 288 | if completions is None: 289 | completions = self \ 290 | ._get_resource_completions(words, 291 | word_before_cursor, 292 | key, 293 | value) 294 | else: 295 | break 296 | return completions 297 | -------------------------------------------------------------------------------- /saws/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2015 Donne Martin. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"). You 6 | # may not use this file except in compliance with the License. A copy of 7 | # the License is located at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # or in the "license" file accompanying this file. This file is 12 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 13 | # ANY KIND, either express or implied. See the License for the specific 14 | # language governing permissions and limitations under the License. 15 | 16 | from __future__ import unicode_literals 17 | from __future__ import print_function 18 | import shutil 19 | import os 20 | try: 21 | from collections import OrderedDict 22 | except: 23 | from ordereddict import OrderedDict 24 | from configobj import ConfigObj 25 | 26 | 27 | class Config(object): 28 | """Reads and writes the config file `sawsrc`. 29 | 30 | Attributes: 31 | * SHORTCUTS: A string that represents the start of shortcuts in 32 | the config file ~/.sawsrc. 33 | * MAIN: A string that represents the main set of configs in 34 | ~/.sawsrc. 35 | * THEME: A string that represents the config theme. 36 | * LOG_FILE: A string that represents the config log file location. 37 | * LOG_LEVEL: A string that represents the config default log 38 | file level. 39 | * COLOR: A string that represents the config color output mode. 40 | * FUZZY: A string that represents the config fuzzy matching mode. 41 | * SHORTCUT: A string that represents the config shortcut matching 42 | mode. 43 | """ 44 | 45 | SHORTCUTS = 'shortcuts' 46 | MAIN = 'main' 47 | THEME = 'theme' 48 | LOG_FILE = 'log_file' 49 | LOG_LEVEL = 'log_level' 50 | COLOR = 'color_output' 51 | FUZZY = 'fuzzy_match' 52 | SHORTCUT = 'shortcut_match' 53 | 54 | def get_shortcuts(self, config_obj): 55 | """Gets the shortcuts from the specified config. 56 | 57 | Args: 58 | * config_obj: An instance of ConfigObj. 59 | 60 | Returns: 61 | An OrderedDict containing the shortcut commands as the keys and 62 | their corresponding full commands as the values. 63 | """ 64 | shortcut_config_obj = self.read_configuration('saws.shortcuts', 65 | '~/.saws.shortcuts') 66 | return OrderedDict(zip(shortcut_config_obj[self.SHORTCUTS].keys(), 67 | shortcut_config_obj[self.SHORTCUTS].values())) 68 | 69 | def read_configuration(self, config_template=None, config_path=None): 70 | """Reads the config file if it exists, else reads the default config. 71 | 72 | Args: 73 | * config_template: A string representing the template file name. 74 | * config_path: A string representing the template file path. 75 | 76 | Returns: 77 | An instance of a ConfigObj. 78 | """ 79 | if config_template is None: 80 | config_template = 'sawsrc' 81 | if config_path is None: 82 | config_path = '~/.sawsrc' 83 | config_template_path = os.path.join(os.path.dirname(__file__), 84 | config_template) 85 | self._copy_template_config(config_template_path, config_path) 86 | return self._read_configuration(config_path, config_template_path) 87 | 88 | def _read_configuration(self, usr_config, def_config=None): 89 | """Reads the config file if it exists, else reads the default config. 90 | 91 | Internal method, call read_configuration instead. 92 | 93 | Args: 94 | * usr_config: A string that specifies the config file name. 95 | * def_config: A string that specifies the config default file name. 96 | 97 | Returns: 98 | An instance of a ConfigObj. 99 | """ 100 | usr_config_file = os.path.expanduser(usr_config) 101 | cfg = ConfigObj() 102 | cfg.filename = usr_config_file 103 | if def_config: 104 | cfg.merge(ConfigObj(def_config, interpolation=False)) 105 | cfg.merge(ConfigObj(usr_config_file, interpolation=False)) 106 | return cfg 107 | 108 | def _copy_template_config(self, source, destination, overwrite=False): 109 | """Writes the default config from a template. 110 | 111 | Args: 112 | * source: A string that specifies the path to the template. 113 | * destination: A string that specifies the path to write. 114 | * overwite: A boolean that specifies whether to overwite the file. 115 | 116 | Returns: 117 | None. 118 | """ 119 | destination = os.path.expanduser(destination) 120 | if not overwrite and os.path.exists(destination): 121 | return 122 | shutil.copyfile(source, destination) 123 | -------------------------------------------------------------------------------- /saws/data/OPTIONS.txt: -------------------------------------------------------------------------------- 1 | --ec2-state: 6 2 | pending 3 | running 4 | shutting-down 5 | stopped 6 | stopping 7 | terminated 8 | -------------------------------------------------------------------------------- /saws/data/RESOURCES_SAMPLE.txt: -------------------------------------------------------------------------------- 1 | [--instance-ids]: 7 2 | i-b815ecc3 3 | i-a51d05f4 4 | i-a71bd617 5 | i-c86cba18 6 | i-f7df0ea7 7 | i-a1637b56 8 | i-hdcbac0d 9 | [--ec2-tag-key]: 3 10 | Stack 11 | Name 12 | Purpose 13 | [--ec2-tag-value]: 6 14 | metrics 15 | testing 16 | billing 17 | security 18 | development-feature 19 | production 20 | [--bucket]: 16 21 | prod-downloads 22 | prod-downloads-logs 23 | prod-assets 24 | prod-assets-logs 25 | dev-downloads 26 | dev-downloads-logs 27 | dev-assets 28 | dev-assets-logs 29 | dev-feature-downloads 30 | dev-feature-downloads-logs 31 | dev-feature-assets 32 | dev-feature-assets-logs 33 | test-downloads 34 | test-downloads-logs 35 | test-assets 36 | test-assets-logs 37 | [s3:]: 16 38 | prod-downloads 39 | prod-downloads-logs 40 | prod-assets 41 | prod-assets-logs 42 | dev-downloads 43 | dev-downloads-logs 44 | dev-assets 45 | dev-assets-logs 46 | dev-feature-downloads 47 | dev-feature-downloads-logs 48 | dev-feature-assets 49 | dev-feature-assets-logs 50 | test-downloads 51 | test-downloads-logs 52 | test-assets 53 | test-assets-logs 54 | -------------------------------------------------------------------------------- /saws/data/SOURCES.txt: -------------------------------------------------------------------------------- 1 | [commands]: 134 2 | acm 3 | alexaforbusiness 4 | apigateway 5 | application-autoscaling 6 | appstream 7 | appsync 8 | athena 9 | autoscaling 10 | autoscaling-plans 11 | batch 12 | budgets 13 | ce 14 | cloud9 15 | clouddirectory 16 | cloudformation 17 | cloudfront 18 | cloudhsm 19 | cloudhsmv2 20 | cloudsearch 21 | cloudsearchdomain 22 | cloudtrail 23 | cloudwatch 24 | codebuild 25 | codecommit 26 | codepipeline 27 | codestar 28 | cognito-identity 29 | cognito-idp 30 | cognito-sync 31 | comprehend 32 | configservice 33 | configure 34 | cur 35 | datapipeline 36 | dax 37 | deploy 38 | devicefarm 39 | directconnect 40 | discovery 41 | dms 42 | ds 43 | dynamodb 44 | dynamodbstreams 45 | ec2 46 | ecr 47 | ecs 48 | efs 49 | elasticache 50 | elasticbeanstalk 51 | elastictranscoder 52 | elb 53 | elbv2 54 | emr 55 | es 56 | events 57 | firehose 58 | gamelift 59 | glacier 60 | glue 61 | greengrass 62 | guardduty 63 | health 64 | history 65 | iam 66 | importexport 67 | inspector 68 | iot 69 | iot-data 70 | iot-jobs-data 71 | kinesis 72 | kinesis-video-archived-media 73 | kinesis-video-media 74 | kinesisanalytics 75 | kinesisvideo 76 | kms 77 | lambda 78 | lex-models 79 | lex-runtime 80 | lightsail 81 | logs 82 | machinelearning 83 | marketplace-entitlement 84 | marketplacecommerceanalytics 85 | mediaconvert 86 | medialive 87 | mediapackage 88 | mediastore 89 | mediastore-data 90 | meteringmarketplace 91 | mgh 92 | mobile 93 | mq 94 | mturk 95 | opsworks 96 | opsworks-cm 97 | organizations 98 | pinpoint 99 | polly 100 | pricing 101 | rds 102 | redshift 103 | rekognition 104 | resource-groups 105 | resourcegroupstaggingapi 106 | route53 107 | route53domains 108 | s3 109 | s3api 110 | sagemaker 111 | sagemaker-runtime 112 | sdb 113 | serverlessrepo 114 | servicecatalog 115 | servicediscovery 116 | ses 117 | shield 118 | sms 119 | snowball 120 | sns 121 | sqs 122 | ssm 123 | stepfunctions 124 | storagegateway 125 | sts 126 | support 127 | swf 128 | transcribe 129 | translate 130 | waf 131 | waf-regional 132 | workdocs 133 | workmail 134 | workspaces 135 | xray 136 | [sub_commands]: 0 137 | [global_options]: 11 138 | --color 139 | --debug 140 | --endpoint-url 141 | --no-paginate 142 | --no-sign-request 143 | --no-verify-ssl 144 | --output 145 | --profile 146 | --query 147 | --region 148 | --version 149 | [resource_options]: 3 150 | --bucket 151 | --cluster-states 152 | --instance-ids 153 | -------------------------------------------------------------------------------- /saws/data_util.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2015 Donne Martin. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"). You 6 | # may not use this file except in compliance with the License. A copy of 7 | # the License is located at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # or in the "license" file accompanying this file. This file is 12 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 13 | # ANY KIND, either express or implied. See the License for the specific 14 | # language governing permissions and limitations under the License. 15 | 16 | from __future__ import unicode_literals 17 | from __future__ import print_function 18 | import re 19 | try: 20 | from collections import OrderedDict 21 | except: 22 | from ordereddict import OrderedDict 23 | 24 | 25 | class DataUtil(object): 26 | """Utility class to read from the data folder. 27 | 28 | Attributes: 29 | * None. 30 | """ 31 | 32 | def create_header_to_type_map(self, headers, data_type): 33 | """Creates a dict mapping headers to ResourceTypes. 34 | 35 | Headers are the resource headers as they appear in the RESOURCES.txt. 36 | Headers are mapped to their corresponding ResourceType. 37 | 38 | Args: 39 | * headers: A string that represents the header. 40 | * data_type: An Enum specifying the data type. 41 | 42 | Returns: 43 | An OrderedDict mapping headers to ResourceTypes. 44 | """ 45 | command_types = [] 46 | for item in data_type: 47 | if item != data_type.NUM_TYPES: 48 | command_types.append(item) 49 | return OrderedDict(zip(headers, command_types)) 50 | 51 | def get_data(self, data_file_path, header_to_type_map, data_type): 52 | """Gets all data from the specified data file. 53 | 54 | Args: 55 | * data_file_path: A string representing the full file path of 56 | the data file. 57 | * header_to_type_map: A dictionary mapping the data header labels 58 | to the data types. 59 | * data_type: An Enum specifying the data type. 60 | 61 | Returns: 62 | A list, where each element is a list of completions for each 63 | data_type 64 | """ 65 | data_lists = [[] for x in range(data_type.NUM_TYPES.value)] 66 | with open(data_file_path) as f: 67 | for line in f: 68 | line = re.sub('\n', '', line) 69 | parsing_header = False 70 | # Check if we are reading in a data header to determine 71 | # which set of data we are parsing 72 | for key, value in header_to_type_map.items(): 73 | if key in line: 74 | data_type = value 75 | parsing_header = True 76 | break 77 | if not parsing_header: 78 | # Store the data in its associated list 79 | if line.strip() != '': 80 | data_lists[data_type.value].append(line) 81 | for data_list in data_lists: 82 | data_list.sort() 83 | return data_lists 84 | -------------------------------------------------------------------------------- /saws/keys.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2015 Donne Martin. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"). You 6 | # may not use this file except in compliance with the License. A copy of 7 | # the License is located at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # or in the "license" file accompanying this file. This file is 12 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 13 | # ANY KIND, either express or implied. See the License for the specific 14 | # language governing permissions and limitations under the License. 15 | 16 | from __future__ import unicode_literals 17 | from __future__ import print_function 18 | from prompt_toolkit.key_binding.manager import KeyBindingManager 19 | from prompt_toolkit.keys import Keys 20 | 21 | 22 | class KeyManager(object): 23 | """Creates a Key Manager. 24 | 25 | Attributes: 26 | * manager: An instance of a prompt_toolkit's KeyBindingManager. 27 | """ 28 | 29 | def __init__(self, set_color, get_color, 30 | set_fuzzy_match, get_fuzzy_match, 31 | set_shortcut_match, get_shortcut_match, 32 | refresh_resources_and_options, handle_docs): 33 | """Initializes KeyManager. 34 | 35 | Args: 36 | * set_color: A function setting the color output config. 37 | * get_color: A function getting the color output config. 38 | * set_fuzzy_match: A function setting the fuzzy match config. 39 | * get_fuzzy_match: A function getting the fuzzy match config. 40 | * set_shortcut_match: A function setting the shortcut match config. 41 | * get_shortcut_match: A function getting the shortcut match config. 42 | 43 | Returns: 44 | None. 45 | """ 46 | self.manager = None 47 | self._create_key_manager(set_color, get_color, 48 | set_fuzzy_match, get_fuzzy_match, 49 | set_shortcut_match, get_shortcut_match, 50 | refresh_resources_and_options, handle_docs) 51 | 52 | def _create_key_manager(self, set_color, get_color, 53 | set_fuzzy_match, get_fuzzy_match, 54 | set_shortcut_match, get_shortcut_match, 55 | refresh_resources_and_options, handle_docs): 56 | """Creates and initializes the keybinding manager. 57 | 58 | Args: 59 | * set_color: A function setting the color output config. 60 | * get_color: A function getting the color output config. 61 | * set_fuzzy_match: A function setting the fuzzy match config. 62 | * get_fuzzy_match: A function getting the fuzzy match config. 63 | * set_shortcut_match: A function setting the shortcut match config. 64 | * get_shortcut_match: A function getting the shortcut match config. 65 | 66 | Returns: 67 | A KeyBindingManager. 68 | """ 69 | assert callable(set_color) 70 | assert callable(get_color) 71 | assert callable(set_fuzzy_match) 72 | assert callable(get_fuzzy_match) 73 | assert callable(set_shortcut_match) 74 | assert callable(get_shortcut_match) 75 | assert callable(refresh_resources_and_options) 76 | assert callable(handle_docs) 77 | self.manager = KeyBindingManager( 78 | enable_search=True, 79 | enable_abort_and_exit_bindings=True, 80 | enable_system_bindings=True, 81 | enable_auto_suggest_bindings=True) 82 | 83 | @self.manager.registry.add_binding(Keys.F2) 84 | def handle_f2(_): 85 | """Enables/Disables color output. 86 | 87 | Args: 88 | * _: An instance of prompt_toolkit's Event (not used). 89 | 90 | Returns: 91 | None. 92 | """ 93 | set_color(not get_color()) 94 | 95 | @self.manager.registry.add_binding(Keys.F3) 96 | def handle_f3(_): 97 | """Enables/Disables fuzzy matching. 98 | 99 | Args: 100 | * _: An instance of prompt_toolkit's Event (not used). 101 | 102 | Returns: 103 | None. 104 | """ 105 | set_fuzzy_match(not get_fuzzy_match()) 106 | 107 | @self.manager.registry.add_binding(Keys.F4) 108 | def handle_f4(_): 109 | """Enables/Disables shortcut matching. 110 | 111 | Args: 112 | * _: An instance of prompt_toolkit's Event (not used). 113 | 114 | Returns: 115 | None. 116 | """ 117 | set_shortcut_match(not get_shortcut_match()) 118 | 119 | @self.manager.registry.add_binding(Keys.F5) 120 | def handle_f5(event): 121 | """Refreshes AWS resources. 122 | 123 | Args: 124 | * event: An instance of prompt_toolkit's Event. 125 | 126 | Returns: 127 | None. 128 | """ 129 | event.cli.run_in_terminal(refresh_resources_and_options) 130 | 131 | @self.manager.registry.add_binding(Keys.F9) 132 | def handle_f9(_): 133 | """Inputs the "docs" command when the `F9` key is pressed. 134 | 135 | Args: 136 | * _: An instance of prompt_toolkit's Event (not used). 137 | 138 | Returns: 139 | None. 140 | """ 141 | handle_docs(from_fkey=True) 142 | 143 | @self.manager.registry.add_binding(Keys.F10) 144 | def handle_f10(_): 145 | """Quits when the `F10` key is pressed. 146 | 147 | Args: 148 | * _: An instance of prompt_toolkit's Event (not used). 149 | 150 | Returns: 151 | None. 152 | """ 153 | raise EOFError 154 | 155 | @self.manager.registry.add_binding(Keys.ControlSpace) 156 | def handle_ctrl_space(event): 157 | """Initializes autocompletion at the cursor. 158 | 159 | If the autocompletion menu is not showing, display it with the 160 | appropriate completions for the context. 161 | 162 | If the menu is showing, select the next completion. 163 | 164 | Args: 165 | * event: An instance of prompt_toolkit's Event. 166 | 167 | Returns: 168 | None. 169 | """ 170 | b = event.cli.current_buffer 171 | if b.complete_state: 172 | b.complete_next() 173 | else: 174 | event.cli.start_completion(select_first=False) 175 | -------------------------------------------------------------------------------- /saws/lexer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 2 | 3 | # Copyright 2015 Donne Martin. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"). You 6 | # may not use this file except in compliance with the License. A copy of 7 | # the License is located at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # or in the "license" file accompanying this file. This file is 12 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 13 | # ANY KIND, either express or implied. See the License for the specific 14 | # language governing permissions and limitations under the License. 15 | 16 | from pygments.lexer import RegexLexer 17 | from pygments.lexer import words 18 | from pygments.token import Keyword, Name, Operator, Generic, Literal 19 | from .commands import AwsCommands 20 | from .config import Config 21 | 22 | 23 | class CommandLexer(RegexLexer): 24 | """Provides highlighting for commands. 25 | 26 | Attributes: 27 | * config: An instance of Config. 28 | * config_obj: An instance of ConfigObj. 29 | * shortcuts: An OrderedDict containing the shortcut commands as the 30 | keys and their corresponding full commands as the values. 31 | * shortcut_tokens: A list containing words for each shortcut key: 32 | key: 'aws ec2 ls' -> shortcut_tokens: ['aws', 'ec2', 'ls']. 33 | * aws_commands: An instance of AwsCommands. 34 | * commands: A tuple, where each tuple element is a list of: 35 | * commands 36 | * sub_commands 37 | * global_options 38 | * resource_options 39 | * tokens: A dictionary of pygments tokens. 40 | """ 41 | 42 | config = Config() 43 | config_obj = config.read_configuration() 44 | shortcuts = config.get_shortcuts(config_obj) 45 | shortcut_tokens = [] 46 | for shortcut in shortcuts.keys(): 47 | tokens = shortcut.split() 48 | for token in tokens: 49 | shortcut_tokens.append(token) 50 | aws_commands = AwsCommands() 51 | commands = aws_commands.all_commands 52 | tokens = { 53 | 'root': [ 54 | (words( 55 | tuple([AwsCommands.AWS_COMMAND]), 56 | prefix=r'\b', 57 | suffix=r'\b'), 58 | Literal.String), 59 | (words( 60 | tuple([AwsCommands.AWS_DOCS]), 61 | prefix=r'\b', 62 | suffix=r'\b'), 63 | Literal.Number), 64 | (words( 65 | tuple(commands[AwsCommands.CommandType.COMMANDS.value]), 66 | prefix=r'\b', 67 | suffix=r'\b'), 68 | Name.Class), 69 | (words( 70 | tuple(commands[AwsCommands.CommandType.SUB_COMMANDS.value]), 71 | prefix=r'\b', 72 | suffix=r'\b'), 73 | Keyword.Declaration), 74 | (words( 75 | tuple(commands[AwsCommands.CommandType.GLOBAL_OPTIONS.value]), 76 | prefix=r'', 77 | suffix=r'\b'), 78 | Generic.Output), 79 | (words( 80 | tuple(commands[AwsCommands.CommandType.RESOURCE_OPTIONS.value]), 81 | prefix=r'', 82 | suffix=r'\b'), 83 | Operator.Word), 84 | (words( 85 | tuple(shortcut_tokens), 86 | prefix=r'', 87 | suffix=r'\b'), 88 | Name.Exception), 89 | ] 90 | } 91 | -------------------------------------------------------------------------------- /saws/logger.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2015 Donne Martin. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"). You 6 | # may not use this file except in compliance with the License. A copy of 7 | # the License is located at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # or in the "license" file accompanying this file. This file is 12 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 13 | # ANY KIND, either express or implied. See the License for the specific 14 | # language governing permissions and limitations under the License. 15 | 16 | from __future__ import unicode_literals 17 | from __future__ import print_function 18 | import os 19 | import logging 20 | 21 | 22 | class SawsLogger(object): 23 | """Handles Saws logging. 24 | 25 | Attributes: 26 | * logger: An instance of Logger. 27 | """ 28 | 29 | def __init__(self, name, log_file, log_level): 30 | """Initializes a Logger for Saws. 31 | 32 | Args: 33 | * name: A string that represents the logger's name. 34 | * log_file: A string that represents the log file name. 35 | * log_level: A string that represents the logging level. 36 | 37 | Returns: 38 | None. 39 | """ 40 | self.logger = logging.getLogger(name) 41 | level_map = { 42 | 'CRITICAL': logging.CRITICAL, 43 | 'ERROR': logging.ERROR, 44 | 'WARNING': logging.WARNING, 45 | 'INFO': logging.INFO, 46 | 'DEBUG': logging.DEBUG 47 | } 48 | handler = logging.FileHandler(os.path.expanduser(log_file)) 49 | formatter = logging.Formatter( 50 | '%(asctime)s (%(process)d/%(threadName)s) ' 51 | '%(name)s %(levelname)s - %(message)s') 52 | handler.setFormatter(formatter) 53 | root_logger = logging.getLogger('saws') 54 | root_logger.addHandler(handler) 55 | root_logger.setLevel(level_map[log_level.upper()]) 56 | root_logger.debug('Initializing saws logging.') 57 | root_logger.debug('Log file %r.', log_file) 58 | -------------------------------------------------------------------------------- /saws/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright 2015 Donne Martin. All Rights Reserved. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"). You 7 | # may not use this file except in compliance with the License. A copy of 8 | # the License is located at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # or in the "license" file accompanying this file. This file is 13 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 14 | # ANY KIND, either express or implied. See the License for the specific 15 | # language governing permissions and limitations under the License. 16 | 17 | from __future__ import unicode_literals 18 | from __future__ import print_function 19 | import click 20 | from .saws import Saws 21 | 22 | 23 | # Disable Warning: Click detected the use of the unicode_literals 24 | # __future__ import. 25 | click.disable_unicode_literals_warning = True 26 | 27 | 28 | @click.command() 29 | def cli(): 30 | """Creates and calls Saws. 31 | 32 | Args: 33 | * None. 34 | 35 | Returns: 36 | None. 37 | """ 38 | try: 39 | saws = Saws() 40 | saws.run_cli() 41 | except (EOFError, KeyboardInterrupt): 42 | saws.aws_cli.set_return_value(None) 43 | saws.config_obj.write() 44 | 45 | 46 | if __name__ == "__main__": 47 | cli() 48 | -------------------------------------------------------------------------------- /saws/options.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2015 Donne Martin. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"). You 6 | # may not use this file except in compliance with the License. A copy of 7 | # the License is located at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # or in the "license" file accompanying this file. This file is 12 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 13 | # ANY KIND, either express or implied. See the License for the specific 14 | # language governing permissions and limitations under the License. 15 | 16 | from __future__ import unicode_literals 17 | from __future__ import print_function 18 | from enum import Enum 19 | import os 20 | from awscli.customizations.emr.constants import LIST_CLUSTERS_ACTIVE_STATES, \ 21 | LIST_CLUSTERS_TERMINATED_STATES, LIST_CLUSTERS_FAILED_STATES 22 | from .data_util import DataUtil 23 | 24 | 25 | class AwsOptions(object): 26 | """Encapsulates AWS command options such as ec2 running states. 27 | 28 | Some options can be obtained from the awscli, while others cannot. 29 | For example: 30 | Option: --ec2-state completions are not found in the awscli 31 | Completions for --ec2-state should be added to data/OPTIONS.txt 32 | Option: --cluster-states are found in the awscli 33 | See get_cluster_states() 34 | 35 | Attributes: 36 | * OPTIONS_DIR: A string representing the directory containing 37 | data/OPTIONS.txt. 38 | * OPTIONS_PATH: A string representing the full file path of 39 | data/OPTIONS.txt. 40 | * all_commands: A list of all commands, sub_commands, options, etc 41 | from data/SOURCES.txt. 42 | * EC2_STATE_OPT: A string representing the option for ec2 states 43 | * CLUSTER_STATE_OPT: A string representing the option for cluster states 44 | * option_headers: 45 | * data_util: An instance of DataUtil(). 46 | * header_to_type_map: A dict mapping headers as they appear in the 47 | OPTIONS.txt file to their corresponding OptionType. 48 | * ec2_states: A list of the possible EC2 instance states. 49 | * cluster_states: A list of the possible cluster states. 50 | * options_map: A dict mapping of options keywords and 51 | options to complete 52 | """ 53 | 54 | class OptionType(Enum): 55 | """Enum specifying the command type. 56 | 57 | Attributes: 58 | * EC2_STATES: An int representing ec2 running states. 59 | * CLUSTER_STATES: An int representing cluster running states. 60 | * NUM_TYPES: An int representing the number of option types. 61 | """ 62 | 63 | NUM_TYPES = 2 64 | EC2_STATES, CLUSTER_STATES = range(NUM_TYPES) 65 | 66 | OPTIONS_DIR = os.path.dirname(os.path.realpath(__file__)) 67 | OPTIONS_PATH = os.path.join(OPTIONS_DIR, 'data/OPTIONS.txt') 68 | 69 | def __init__(self, 70 | all_commands): 71 | """Initializes AwsResources. 72 | 73 | Args: 74 | * all_commands: A list of all commands, sub_commands, options, etc 75 | from data/SOURCES.txt. 76 | 77 | Returns: 78 | None. 79 | """ 80 | self.all_commands = all_commands 81 | self.EC2_STATE_OPT = '--ec2-state' 82 | self.CLUSTER_STATE_OPT = '--cluster-states' 83 | self.option_headers = [self._make_options_header(self.EC2_STATE_OPT)] 84 | self.data_util = DataUtil() 85 | self.header_to_type_map = self.data_util.create_header_to_type_map( 86 | headers=self.option_headers, 87 | data_type=self.OptionType) 88 | self.ec2_states, _ = DataUtil().get_data(self.OPTIONS_PATH, 89 | self.header_to_type_map, 90 | self.OptionType) 91 | self.cluster_states = self._generate_cluster_states() 92 | self.options_map = dict(zip([self.EC2_STATE_OPT, 93 | self.CLUSTER_STATE_OPT], 94 | [self.ec2_states, 95 | self.cluster_states])) 96 | 97 | def _make_options_header(self, option): 98 | """Creates the header string in OPTIONS.txt from the given option. 99 | 100 | Args: 101 | * option: A string that represents an option. 102 | 103 | Returns: 104 | A string that represents the header in OPTIONS.txt for the 105 | given option. 106 | """ 107 | return option + ': ' 108 | 109 | def _generate_cluster_states(self): 110 | """Generates all the cluster states from the official AWS CLI. 111 | 112 | Args: 113 | * None. 114 | 115 | Returns: 116 | A list containing all cluster states. 117 | """ 118 | cluster_states = [] 119 | cluster_states.extend(LIST_CLUSTERS_ACTIVE_STATES) 120 | cluster_states.extend(LIST_CLUSTERS_TERMINATED_STATES) 121 | cluster_states.extend(LIST_CLUSTERS_FAILED_STATES) 122 | return cluster_states 123 | -------------------------------------------------------------------------------- /saws/resource/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2015 Donne Martin. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"). You 6 | # may not use this file except in compliance with the License. A copy of 7 | # the License is located at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # or in the "license" file accompanying this file. This file is 12 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 13 | # ANY KIND, either express or implied. See the License for the specific 14 | # language governing permissions and limitations under the License. 15 | -------------------------------------------------------------------------------- /saws/resource/bucket.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2015 Donne Martin. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"). You 6 | # may not use this file except in compliance with the License. A copy of 7 | # the License is located at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # or in the "license" file accompanying this file. This file is 12 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 13 | # ANY KIND, either express or implied. See the License for the specific 14 | # language governing permissions and limitations under the License. 15 | 16 | from __future__ import unicode_literals 17 | from __future__ import print_function 18 | from .resource import Resource 19 | from abc import ABCMeta, abstractmethod 20 | 21 | 22 | class Bucket(Resource): 23 | """Encapsulates the S3 bucket resources. 24 | 25 | Base class for BucketNames and BucketUris 26 | 27 | Attributes: 28 | * OPTION: A string representing the option for bucket uri. 29 | * QUERY: A string representing the AWS query to list all bucket uri. 30 | * resources: A list of bucket uri. 31 | """ 32 | 33 | __metaclass__ = ABCMeta 34 | 35 | OPTION = '' 36 | QUERY = '' 37 | 38 | def __init__(self): 39 | """Initializes BucketNames. 40 | 41 | Args: 42 | * None. 43 | 44 | Returns: 45 | None. 46 | """ 47 | super(Bucket, self).__init__() 48 | 49 | def query_resource(self): 50 | """Queries and stores bucket names from AWS. 51 | 52 | Special case for S3: 53 | We have two ways to invoke S3 completions: 54 | Option: --bucket Completion: foo 55 | Option: s3: Completion: s3://foo 56 | 57 | Args: 58 | * None. 59 | 60 | Returns: 61 | None. 62 | 63 | Raises: 64 | A subprocess.CalledProcessError if check_output returns a non-zero 65 | exit status, which is called by self._query_aws. 66 | """ 67 | output = self._query_aws(self.QUERY) 68 | if output is not None: 69 | self.clear_resources() 70 | result_list = output.split('\n') 71 | for result in result_list: 72 | try: 73 | result = result.split()[-1] 74 | self.add_bucket_name(result) 75 | except: 76 | # Ignore blank lines 77 | pass 78 | 79 | @abstractmethod 80 | def add_bucket_name(self, bucket_name): 81 | """Adds the bucket name to our bucket resources. 82 | 83 | Abstract method. 84 | 85 | Args: 86 | * bucket_name: A string representing the bucket name. 87 | 88 | Returns: 89 | None. 90 | """ 91 | pass 92 | -------------------------------------------------------------------------------- /saws/resource/bucket_names.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2015 Donne Martin. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"). You 6 | # may not use this file except in compliance with the License. A copy of 7 | # the License is located at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # or in the "license" file accompanying this file. This file is 12 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 13 | # ANY KIND, either express or implied. See the License for the specific 14 | # language governing permissions and limitations under the License. 15 | 16 | from __future__ import unicode_literals 17 | from __future__ import print_function 18 | from .bucket import Bucket 19 | 20 | 21 | class BucketNames(Bucket): 22 | """Encapsulates the S3 bucket names resources. 23 | 24 | Attributes: 25 | * OPTION: A string representing the option for bucket names. 26 | * QUERY: A string representing the AWS query to list all bucket names. 27 | * resources: A list of bucket names. 28 | """ 29 | 30 | OPTION = '--bucket' 31 | QUERY = 'aws s3 ls' 32 | 33 | def __init__(self): 34 | """Initializes BucketNames. 35 | 36 | Args: 37 | * None. 38 | 39 | Returns: 40 | None. 41 | """ 42 | super(BucketNames, self).__init__() 43 | 44 | def query_resource(self): 45 | """Queries and stores bucket names from AWS. 46 | 47 | Args: 48 | * None. 49 | 50 | Returns: 51 | None. 52 | 53 | Raises: 54 | A subprocess.CalledProcessError if check_output returns a non-zero 55 | exit status, which is called by self._query_aws. 56 | """ 57 | print(' Refreshing bucket names...') 58 | super(BucketNames, self).query_resource() 59 | 60 | def add_bucket_name(self, bucket_name): 61 | """Adds the bucket name to our bucket resources. 62 | 63 | Args: 64 | * bucket_name: A string representing the bucket name. 65 | 66 | Returns: 67 | None. 68 | """ 69 | self.resources.extend([bucket_name]) 70 | -------------------------------------------------------------------------------- /saws/resource/bucket_uris.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2015 Donne Martin. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"). You 6 | # may not use this file except in compliance with the License. A copy of 7 | # the License is located at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # or in the "license" file accompanying this file. This file is 12 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 13 | # ANY KIND, either express or implied. See the License for the specific 14 | # language governing permissions and limitations under the License. 15 | 16 | from __future__ import unicode_literals 17 | from __future__ import print_function 18 | from .bucket import Bucket 19 | 20 | 21 | class BucketUris(Bucket): 22 | """Encapsulates the S3 bucket uri resources. 23 | 24 | Attributes: 25 | * OPTION: A string representing the option for bucket uri. 26 | * QUERY: A string representing the AWS query to list all bucket uri. 27 | * resources: A list of bucket uri. 28 | """ 29 | 30 | OPTION = 's3:' 31 | QUERY = 'aws s3 ls' 32 | PREFIX = OPTION + '//' 33 | 34 | def __init__(self): 35 | """Initializes BucketNames. 36 | 37 | Args: 38 | * None. 39 | 40 | Returns: 41 | None. 42 | """ 43 | super(BucketUris, self).__init__() 44 | 45 | def query_resource(self): 46 | """Queries and stores bucket uris from AWS. 47 | 48 | Args: 49 | * None. 50 | 51 | Returns: 52 | None. 53 | 54 | Raises: 55 | A subprocess.CalledProcessError if check_output returns a non-zero 56 | exit status, which is called by self._query_aws. 57 | """ 58 | print(' Refreshing bucket uris...') 59 | super(BucketUris, self).query_resource() 60 | 61 | def add_bucket_name(self, bucket_name): 62 | """Adds the bucket name to our bucket resources. 63 | 64 | Args: 65 | * bucket_name: A string representing the bucket name. 66 | 67 | Returns: 68 | None. 69 | """ 70 | self.resources.extend([self.PREFIX + bucket_name]) 71 | -------------------------------------------------------------------------------- /saws/resource/instance_ids.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2015 Donne Martin. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"). You 6 | # may not use this file except in compliance with the License. A copy of 7 | # the License is located at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # or in the "license" file accompanying this file. This file is 12 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 13 | # ANY KIND, either express or implied. See the License for the specific 14 | # language governing permissions and limitations under the License. 15 | 16 | from __future__ import unicode_literals 17 | from __future__ import print_function 18 | import re 19 | from .resource import Resource 20 | 21 | 22 | class InstanceIds(Resource): 23 | """Encapsulates the EC2 instance ids resources. 24 | 25 | Attributes: 26 | * OPTION: A string representing the option for instance ids. 27 | * QUERY: A string representing the AWS query to list all instance ids. 28 | * resources: A list of instance ids. 29 | """ 30 | 31 | OPTION = '--instance-ids' 32 | QUERY = 'aws ec2 describe-instances --query "Reservations[].Instances[].[InstanceId]" --output text' # NOQA 33 | 34 | def __init__(self): 35 | """Initializes InstanceIds. 36 | 37 | Args: 38 | * None. 39 | 40 | Returns: 41 | None. 42 | """ 43 | super(InstanceIds, self).__init__() 44 | 45 | def query_resource(self): 46 | """Queries and stores instance ids from AWS. 47 | 48 | Args: 49 | * None. 50 | 51 | Returns: 52 | The list of resources. 53 | 54 | Raises: 55 | A subprocess.CalledProcessError if check_output returns a non-zero 56 | exit status, which is called by self._query_aws. 57 | """ 58 | print(' Refreshing instance ids...') 59 | output = self._query_aws(self.QUERY) 60 | if output is not None: 61 | output = re.sub('\n', ' ', output) 62 | self.resources = output.split() 63 | -------------------------------------------------------------------------------- /saws/resource/instance_tag_keys.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2015 Donne Martin. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"). You 6 | # may not use this file except in compliance with the License. A copy of 7 | # the License is located at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # or in the "license" file accompanying this file. This file is 12 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 13 | # ANY KIND, either express or implied. See the License for the specific 14 | # language governing permissions and limitations under the License. 15 | 16 | from __future__ import unicode_literals 17 | from __future__ import print_function 18 | from .resource import Resource 19 | 20 | 21 | class InstanceTagKeys(Resource): 22 | """Encapsulates the EC2 instance tag keys resources. 23 | 24 | Attributes: 25 | * OPTION: A string representing the option for instance tag keys. 26 | * QUERY: A string representing the AWS query to list all instance 27 | tag keys. 28 | * resources: A list of instance tag keys. 29 | """ 30 | 31 | OPTION = '--ec2-tag-key' 32 | QUERY = 'aws ec2 describe-instances --filters "Name=tag-key,Values=*" --query Reservations[].Instances[].Tags[].Key --output text' # NOQA 33 | 34 | def __init__(self): 35 | """Initializes InstanceTagKeys. 36 | 37 | Args: 38 | * None. 39 | 40 | Returns: 41 | None. 42 | """ 43 | super(InstanceTagKeys, self).__init__() 44 | 45 | def query_resource(self): 46 | """Queries and stores instance ids from AWS. 47 | 48 | Args: 49 | * None. 50 | 51 | Returns: 52 | The list of resources. 53 | 54 | Raises: 55 | A subprocess.CalledProcessError if check_output returns a non-zero 56 | exit status, which is called by self._query_aws. 57 | """ 58 | # TODO: Refactor query_resource in InstanceTagKeys and InstanceTagValues 59 | print(' Refreshing instance tag keys...') 60 | output = self._query_aws(self.QUERY) 61 | if output is not None: 62 | self.resources = list(set(output.split('\t'))) 63 | -------------------------------------------------------------------------------- /saws/resource/instance_tag_values.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2015 Donne Martin. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"). You 6 | # may not use this file except in compliance with the License. A copy of 7 | # the License is located at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # or in the "license" file accompanying this file. This file is 12 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 13 | # ANY KIND, either express or implied. See the License for the specific 14 | # language governing permissions and limitations under the License. 15 | 16 | from __future__ import unicode_literals 17 | from __future__ import print_function 18 | from .resource import Resource 19 | 20 | 21 | class InstanceTagValues(Resource): 22 | """Encapsulates the EC2 instance tag values resources. 23 | 24 | Attributes: 25 | * OPTION: A string representing the option for instance tag values. 26 | * QUERY: A string representing the AWS query to list all instance 27 | tag values. 28 | * resources: A list of instance tag values. 29 | """ 30 | 31 | OPTION = '--ec2-tag-value' 32 | QUERY = 'aws ec2 describe-instances --filters "Name=tag-value,Values=*" --query Reservations[].Instances[].Tags[].Value --output text' # NOQA 33 | 34 | def __init__(self): 35 | """Initializes InstanceTagValues. 36 | 37 | Args: 38 | * None. 39 | 40 | Returns: 41 | None. 42 | """ 43 | super(InstanceTagValues, self).__init__() 44 | 45 | def query_resource(self): 46 | """Queries and stores instance ids from AWS. 47 | 48 | Args: 49 | * None. 50 | 51 | Returns: 52 | The list of resources. 53 | 54 | Raises: 55 | A subprocess.CalledProcessError if check_output returns a non-zero 56 | exit status, which is called by self._query_aws. 57 | """ 58 | # TODO: Refactor query_resource in InstanceTagKeys and InstanceTagValues 59 | print(' Refreshing instance tag values...') 60 | output = self._query_aws(self.QUERY) 61 | if output is not None: 62 | self.resources = list(set(output.split('\t'))) 63 | -------------------------------------------------------------------------------- /saws/resource/resource.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2015 Donne Martin. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"). You 6 | # may not use this file except in compliance with the License. A copy of 7 | # the License is located at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # or in the "license" file accompanying this file. This file is 12 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 13 | # ANY KIND, either express or implied. See the License for the specific 14 | # language governing permissions and limitations under the License. 15 | 16 | from __future__ import unicode_literals 17 | from __future__ import print_function 18 | import subprocess 19 | from abc import ABCMeta, abstractmethod 20 | 21 | 22 | class Resource(): 23 | """Encapsulates an AWS resource. 24 | 25 | Abstract base class for resources. 26 | 27 | Attributes: 28 | * OPTION: A string representing the option that will cause the resource 29 | completions to be displayed when typed. 30 | * HEADER: A string representing the header in the RESOURCES.txt file 31 | that denote the start of the given resources. 32 | * QUERY: A string representing the AWS query to list all resources 33 | * resources: A list of resources. 34 | """ 35 | 36 | __metaclass__ = ABCMeta 37 | 38 | OPTION = '' 39 | HEADER = '' 40 | QUERY = '' 41 | 42 | def __init__(self): 43 | """Initializes Resource. 44 | 45 | Args: 46 | * None. 47 | 48 | Returns: 49 | None. 50 | """ 51 | self.resources = [] 52 | self.HEADER = '[' + self.OPTION + ']' 53 | 54 | def clear_resources(self): 55 | """Clears the resource. 56 | 57 | Args: 58 | * None. 59 | 60 | Returns: 61 | None. 62 | """ 63 | self.resources[:] = [] 64 | 65 | @abstractmethod 66 | def query_resource(self): 67 | """Queries and stores resources from AWS. 68 | 69 | Abstract method. 70 | 71 | Args: 72 | * None. 73 | 74 | Raises: 75 | A subprocess.CalledProcessError if check_output returns a non-zero 76 | exit status, which is called by self._query_aws. 77 | """ 78 | pass 79 | 80 | def _query_aws(self, query): 81 | """Sends the given query to the shell for processing. 82 | 83 | The awscli will process the command and output its results. The 84 | results are captured and returned. 85 | 86 | Args: 87 | * command: A string representing the given query. 88 | 89 | Returns: 90 | A string representing the awscli output. 91 | 92 | Raises: 93 | A subprocess.CalledProcessError if check_output returns a non-zero 94 | exit status. 95 | """ 96 | return subprocess.check_output(query, 97 | universal_newlines=True, 98 | shell=True) 99 | -------------------------------------------------------------------------------- /saws/resources.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2015 Donne Martin. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"). You 6 | # may not use this file except in compliance with the License. A copy of 7 | # the License is located at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # or in the "license" file accompanying this file. This file is 12 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 13 | # ANY KIND, either express or implied. See the License for the specific 14 | # language governing permissions and limitations under the License. 15 | 16 | from __future__ import unicode_literals 17 | from __future__ import print_function 18 | import os 19 | try: 20 | from collections import OrderedDict 21 | except: 22 | from ordereddict import OrderedDict 23 | import traceback 24 | from enum import Enum 25 | from .data_util import DataUtil 26 | from .resource.instance_ids import InstanceIds 27 | from .resource.instance_tag_keys import InstanceTagKeys 28 | from .resource.instance_tag_values import InstanceTagValues 29 | from .resource.bucket_names import BucketNames 30 | from .resource.bucket_uris import BucketUris 31 | 32 | 33 | class AwsResources(object): 34 | """Encapsulates AWS resources such as ec2 tags and buckets. 35 | 36 | Attributes: 37 | * resources_path: A string representing the full file path of 38 | data/RESOURCES.txt. 39 | * log_exception: A callable log_exception from SawsLogger. 40 | * resource_lists: A list where each element is a list of completions 41 | for each resource. 42 | * resources_headers_map: A dict mapping resource headers to 43 | resources to complete. Headers denote the start of each 44 | set of resources in the RESOURCES.txt file. 45 | * resources_options_map: A dict mapping resource options to 46 | resources to complete. 47 | * resource_headers: A list of headers that denote the start of each 48 | set of resources in the RESOURCES.txt file. 49 | * data_util: An instance of DataUtil(). 50 | * header_to_type_map: A dict mapping headers as they appear in the 51 | RESOURCES.txt file to their corresponding ResourceType. 52 | """ 53 | 54 | class ResourceType(Enum): 55 | """Enum specifying the resource type. 56 | 57 | Append new resource class instances here and increment NUM_TYPES. 58 | Note: Order is important, new resources should be added to the end. 59 | 60 | Attributes: 61 | * INSTANCE_IDS: An int representing instance ids. 62 | * INSTANCE_TAG_KEYS: An int representing instance tag keys. 63 | * INSTANCE_TAG_VALUES: An int representing instance tag values. 64 | * BUCKET_NAMES: An int representing bucket names. 65 | * BUCKET_URIS: An int representing bucket uris. 66 | * NUM_TYPES: An int representing the number of resource types. 67 | """ 68 | NUM_TYPES = 5 69 | INSTANCE_IDS, INSTANCE_TAG_KEYS, INSTANCE_TAG_VALUES, \ 70 | BUCKET_NAMES, BUCKET_URIS = range(NUM_TYPES) 71 | 72 | def __init__(self, 73 | log_exception): 74 | """Initializes AwsResources. 75 | 76 | Args: 77 | * log_exception: A callable log_exception from SawsLogger. 78 | 79 | Returns: 80 | None. 81 | """ 82 | # TODO: Use a file version instead of a new file 83 | self._set_resources_path('data/RESOURCES_v2.txt') 84 | self.log_exception = log_exception 85 | self.resource_lists = self._create_resource_lists() 86 | self.resources_headers_map = None 87 | self.resources_options_map = None 88 | self.resource_headers = self._get_resource_headers() 89 | self.resource_options = self._get_resource_options() 90 | self.data_util = DataUtil() 91 | self.header_to_type_map = self.data_util.create_header_to_type_map( 92 | headers=self.resource_headers, 93 | data_type=self.ResourceType) 94 | 95 | def refresh(self, force_refresh=False): 96 | """Refreshes the AWS resources and caches them to a file. 97 | 98 | This function is called on startup. 99 | If no cache exists, it queries AWS to build the resource lists. 100 | Pressing the `F5` key will set force_refresh to True, which proceeds 101 | to refresh the list regardless of whether a cache exists. 102 | Before returning, it saves the resource lists to cache. 103 | 104 | Args: 105 | * force_refresh: A boolean determines whether to force a cache 106 | refresh. This value is set to True when the user presses `F5`. 107 | 108 | Returns: 109 | None. 110 | """ 111 | self.clear_resources() 112 | if not force_refresh: 113 | try: 114 | self._refresh_resources_from_file() 115 | print('Loaded resources from cache') 116 | except IOError: 117 | print('No resource cache found') 118 | force_refresh = True 119 | if force_refresh: 120 | self._query_resources() 121 | try: 122 | self.resources_headers_map = self._create_resources_map( 123 | self.resource_headers) 124 | self.resources_options_map = self._create_resources_map( 125 | self.resource_options) 126 | self._save_resources_to_file() 127 | except IOError as e: 128 | self.log_exception(e, traceback) 129 | 130 | def clear_resources(self): 131 | """Clears all resources. 132 | 133 | Args: 134 | * None. 135 | 136 | Returns: 137 | None. 138 | """ 139 | for resource_list in self.resource_lists: 140 | resource_list.clear_resources() 141 | 142 | def _create_resource_lists(self): 143 | """Create the resource lists. 144 | 145 | Append new resource class instances here. 146 | Note: Order is important, new resources should be added to the end. 147 | 148 | Args: 149 | * None. 150 | 151 | Returns: 152 | None. 153 | """ 154 | return [InstanceIds(), 155 | InstanceTagKeys(), 156 | InstanceTagValues(), 157 | BucketNames(), 158 | BucketUris()] 159 | 160 | def _get_resource_headers(self): 161 | """Builds a list of resource headers found in the resource file. 162 | 163 | Each header denotes the start of each set of resources in the 164 | RESOURCES.txt file 165 | 166 | Args: 167 | * None. 168 | 169 | Returns: 170 | A list of headers that denote the start of each set of resources 171 | in the RESOURCES.txt file. 172 | """ 173 | resource_headers = [] 174 | for resource_list in self.resource_lists: 175 | resource_headers.append(resource_list.HEADER) 176 | return resource_headers 177 | 178 | def _get_resource_options(self): 179 | """Builds a list of resource options that kick off option completions. 180 | 181 | Args: 182 | * None. 183 | 184 | Returns: 185 | A list of options that kick off option completions. 186 | """ 187 | resource_options = [] 188 | for resource_list in self.resource_lists: 189 | resource_options.append(resource_list.OPTION) 190 | return resource_options 191 | 192 | def _create_resources_map(self, resource_key): 193 | """Creates a mapping of resource keywords and resources to complete. 194 | 195 | Requires self.resource_headers to already contain all headers. 196 | 197 | Example with resource_key == resource_headers: 198 | Key: '[--instance-ids]'. 199 | Value: List of instance ids. 200 | 201 | Example with resource_key == resource_options: 202 | Key: '--instance-ids'. 203 | Value: List of instance ids. 204 | 205 | Args: 206 | * resource_key: One of the two attributes: 207 | resource_headers or resource_options. 208 | 209 | Returns: 210 | An OrderedDict resource keywords and resources to complete. 211 | """ 212 | resources = [] 213 | for resource_list in self.resource_lists: 214 | resources.append(resource_list.resources) 215 | resources_map = OrderedDict(zip(resource_key, resources)) 216 | return resources_map 217 | 218 | def _query_resources(self): 219 | """Runs queries for all resources. 220 | 221 | Args: 222 | * None. 223 | 224 | Returns: 225 | None. 226 | """ 227 | print('Refreshing resources...') 228 | for resource_list in self.resource_lists: 229 | try: 230 | resource_list.query_resource() 231 | except Exception as e: 232 | self.log_exception(e, traceback) 233 | print('Done refreshing') 234 | 235 | def _get_all_resources(self): 236 | """Gets all resources from the data/RESOURCES.txt file. 237 | 238 | Args: 239 | * None. 240 | 241 | Returns: 242 | A list, where each element is a list of completions for each 243 | ResourceType 244 | """ 245 | return DataUtil().get_data(self.resources_path, 246 | self.header_to_type_map, 247 | self.ResourceType) 248 | 249 | def _set_resources_path(self, resources_file): 250 | """Sets the path of where to load the resources. 251 | 252 | Args: 253 | * resources_file: A string representing the resource file 254 | path relative to the saws package. 255 | 256 | Returns: 257 | None. 258 | """ 259 | RESOURCES_DIR = os.path.dirname(os.path.realpath(__file__)) 260 | self.resources_path = os.path.join(RESOURCES_DIR, resources_file) 261 | 262 | def _refresh_resources_from_file(self): 263 | """Refreshes the AWS resources from data/RESOURCES.txt. 264 | 265 | Args: 266 | * file_path: A string representing the resource file path. 267 | 268 | Returns: 269 | None. 270 | """ 271 | all_resources = self._get_all_resources() 272 | for index, resources in enumerate(all_resources): 273 | self.resource_lists[index].resources = resources 274 | 275 | def _save_resources_to_file(self): 276 | """Saves the AWS resources to data/RESOURCES.txt. 277 | 278 | Args: 279 | * None. 280 | 281 | Returns: 282 | None. 283 | """ 284 | with open(self.resources_path, 'wt') as fp: 285 | for key, resources in self.resources_headers_map.items(): 286 | fp.write(key + ': ' + str(len(resources)) + '\n') 287 | for resource in resources: 288 | fp.write(resource + '\n') 289 | -------------------------------------------------------------------------------- /saws/saws.shortcuts: -------------------------------------------------------------------------------- 1 | [shortcuts] 2 | 3 | # See the following link for the latest list of shortcuts provided by the 4 | # community: https://github.com/donnemartin/saws/blob/master/saws/saws.shortcuts 5 | 6 | # Shortcut entries in this file follow the form: 7 | # 'shortcut command' = 'full command' 8 | # Once a shortcut command is found, the corresponding full command is 9 | # substituted and matching stops. Shortcuts are evaluated top to bottom. 10 | # Currently, shortcuts are only shown after the user types 'aws' as the first 11 | # command and the user is currently inputting the subcommand: 12 | # aws [show shortcut matches] 13 | # Shortcuts optionally support fuzzy completion. 14 | 15 | # Simple shortcut demonstration to replace describe-instances with ls 16 | # Running the following command will kick off fuzzy auto-completion: 17 | # aws ec2ls 18 | 'ec2 ls --instance-ids' = 'ec2 describe-instances --instance-ids' 19 | # aws emrls 20 | 'emr ls --cluster-states' = 'emr list-clusters --cluster-states' 21 | 22 | # The following entries are placed here solely for easy fuzzy autocompletion 23 | # For example: the following commands will kick off fuzzy auto-completion: 24 | # aws ec2start 25 | # aws ec2stop 26 | 'ec2 start-instances --instance-ids' = 'ec2 start-instances --instance-ids' 27 | 'ec2 stop-instances --instance-ids' = 'ec2 stop-instances --instance-ids' 28 | 29 | # The following commands will kick off fuzzy auto-completion: 30 | # aws ec2tagkey 31 | # aws ec2tagval 32 | # These shortcuts support a string substitution: 33 | # aws ls --ec2-tag-key [tag-key] 34 | # aws ls --ec2-tag-value [tag-value] 35 | # Example input: 36 | # aws ls --ec2-tag-key Stack 37 | # Example result: 38 | # 'ec2 ls --ec2-tag-key' = 'ec2 describe-instances --filters "Name=tag-key,Values=Stack"' 39 | 'ec2 ls --ec2-tag-key' = 'ec2 describe-instances --filters "Name=tag-key,Values=%s"' 40 | 'ec2 ls --ec2-tag-value' = 'ec2 describe-instances --filters "Name=tag-value,Values=%s"' 41 | 42 | # Saws will auto-complete the following --ec2-state auto-completions: 43 | # [pending | running | shutting-down | terminated | stopping | stopped] 44 | # Usage: 45 | # ec2 ls --ec2-state 46 | 'ec2 ls --ec2-state' = 'ec2 describe-instances --filters "Name=instance-state-name,Values=%s"' 47 | 48 | # Simple 'ls' shortcuts 49 | 'ec2 ls' = 'ec2 describe-instances' 50 | 'emr ls' = 'emr list-clusters' 51 | 'elb ls' = 'elb describe-load-balancers' 52 | 'dynamodb ls' = 'dynamodb list-tables' 53 | -------------------------------------------------------------------------------- /saws/sawsrc: -------------------------------------------------------------------------------- 1 | [main] 2 | 3 | # Visual theme. Possible values: manni, igor, xcode, vim, autumn, vs, rrt, 4 | # native, perldoc, borland, tango, emacs, friendly, monokai, paraiso-dark, 5 | # colorful, murphy, bw, pastie, paraiso-light, trac, default, fruity 6 | theme = vim 7 | 8 | # Use color output mode. 9 | color_output = True 10 | 11 | # Use fuzzy matching mode for resources (default is to use simple substring match). 12 | fuzzy_match = True 13 | 14 | # Use shortcut matching mode 15 | shortcut_match = True 16 | 17 | # log_file location. 18 | log_file = ~/.saws.log 19 | 20 | # Default log level. Possible values: "CRITICAL", "ERROR", "WARNING", "INFO" 21 | # and "DEBUG". 22 | log_level = INFO 23 | -------------------------------------------------------------------------------- /saws/style.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 2 | 3 | # Copyright 2015 Donne Martin. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"). You 6 | # may not use this file except in compliance with the License. A copy of 7 | # the License is located at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # or in the "license" file accompanying this file. This file is 12 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 13 | # ANY KIND, either express or implied. See the License for the specific 14 | # language governing permissions and limitations under the License. 15 | 16 | from pygments.token import Token 17 | from pygments.util import ClassNotFound 18 | from prompt_toolkit.styles import default_style_extensions, style_from_dict 19 | import pygments.styles 20 | 21 | 22 | class StyleFactory(object): 23 | """Creates a custom saws style. 24 | 25 | Provides styles for the completions menu and toolbar. 26 | 27 | Attributes: 28 | * style: An instance of a Pygments Style. 29 | """ 30 | 31 | def __init__(self, name): 32 | """Initializes StyleFactory. 33 | 34 | Args: 35 | * name: A string representing the pygments style. 36 | 37 | Returns: 38 | An instance of CliStyle. 39 | """ 40 | self.style = self.style_factory(name) 41 | 42 | def style_factory(self, name): 43 | """Retrieves the specified pygments style. 44 | 45 | If the specified style is not found, the native style is returned. 46 | 47 | Args: 48 | * name: A string representing the pygments style. 49 | 50 | Returns: 51 | An instance of CliStyle. 52 | """ 53 | try: 54 | style = pygments.styles.get_style_by_name(name) 55 | except ClassNotFound: 56 | style = pygments.styles.get_style_by_name('native') 57 | 58 | # Create styles dictionary. 59 | styles = {} 60 | styles.update(style.styles) 61 | styles.update(default_style_extensions) 62 | styles.update({ 63 | Token.Menu.Completions.Completion.Current: 'bg:#00aaaa #000000', 64 | Token.Menu.Completions.Completion: 'bg:#008888 #ffffff', 65 | Token.Scrollbar: 'bg:#00aaaa', 66 | Token.Scrollbar.Button: 'bg:#003333', 67 | Token.Toolbar: 'bg:#222222 #cccccc', 68 | Token.Toolbar.Off: 'bg:#222222 #696969', 69 | Token.Toolbar.On: 'bg:#222222 #ffffff', 70 | Token.Toolbar.Search: 'noinherit bold', 71 | Token.Toolbar.Search.Text: 'nobold', 72 | Token.Toolbar.System: 'noinherit bold', 73 | Token.Toolbar.Arg: 'noinherit bold', 74 | Token.Toolbar.Arg.Text: 'nobold' 75 | }) 76 | 77 | return style_from_dict(styles) 78 | -------------------------------------------------------------------------------- /saws/toolbar.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2015 Donne Martin. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"). You 6 | # may not use this file except in compliance with the License. A copy of 7 | # the License is located at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # or in the "license" file accompanying this file. This file is 12 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 13 | # ANY KIND, either express or implied. See the License for the specific 14 | # language governing permissions and limitations under the License. 15 | 16 | from __future__ import unicode_literals 17 | from __future__ import print_function 18 | from pygments.token import Token 19 | 20 | 21 | class Toolbar(object): 22 | """Encapsulates the bottom toolbar. 23 | 24 | Attributes: 25 | * handler: A callable get_toolbar_items. 26 | """ 27 | 28 | def __init__(self, color_cfg, fuzzy_cfg, shortcuts_cfg): 29 | """Initializes ToolBar. 30 | 31 | Args: 32 | * color_cfg: A boolean that spedifies whether to color the output. 33 | * fuzzy_cfg: A boolean that spedifies whether to do fuzzy matching. 34 | * shortcuts_cfg: A boolean that spedifies whether to match 35 | shortcuts. 36 | 37 | Returns: 38 | None 39 | """ 40 | self.handler = self._create_toolbar_handler(color_cfg, 41 | fuzzy_cfg, 42 | shortcuts_cfg) 43 | 44 | def _create_toolbar_handler(self, color_cfg, fuzzy_cfg, shortcuts_cfg): 45 | """Creates the toolbar handler. 46 | 47 | Args: 48 | * color_cfg: A boolean that spedifies whether to color the output. 49 | * fuzzy_cfg: A boolean that spedifies whether to do fuzzy matching. 50 | * shortcuts_cfg: A boolean that spedifies whether to match 51 | shortcuts. 52 | 53 | Returns: 54 | A callable get_toolbar_items. 55 | """ 56 | assert callable(color_cfg) 57 | assert callable(fuzzy_cfg) 58 | assert callable(shortcuts_cfg) 59 | 60 | def get_toolbar_items(_): 61 | """Returns bottom menu items. 62 | 63 | Args: 64 | * _: An instance of prompt_toolkit's Cli (not used). 65 | 66 | Returns: 67 | A list of Token.Toolbar. 68 | """ 69 | if color_cfg(): 70 | color_token = Token.Toolbar.On 71 | color = 'ON' 72 | else: 73 | color_token = Token.Toolbar.Off 74 | color = 'OFF' 75 | if fuzzy_cfg(): 76 | fuzzy_token = Token.Toolbar.On 77 | fuzzy = 'ON' 78 | else: 79 | fuzzy_token = Token.Toolbar.Off 80 | fuzzy = 'OFF' 81 | if shortcuts_cfg(): 82 | shortcuts_token = Token.Toolbar.On 83 | shortcuts = 'ON' 84 | else: 85 | shortcuts_token = Token.Toolbar.Off 86 | shortcuts = 'OFF' 87 | return [ 88 | (color_token, ' [F2] Color: {0} '.format(color)), 89 | (fuzzy_token, ' [F3] Fuzzy: {0} '.format(fuzzy)), 90 | (shortcuts_token, ' [F4] Shortcuts: {0} '.format(shortcuts)), 91 | (Token.Toolbar, ' [F5] Refresh '), 92 | (Token.Toolbar, ' [F9] Docs '), 93 | (Token.Toolbar, ' [F10] Exit ') 94 | ] 95 | 96 | return get_toolbar_items 97 | -------------------------------------------------------------------------------- /saws/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2015 Donne Martin. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"). You 6 | # may not use this file except in compliance with the License. A copy of 7 | # the License is located at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # or in the "license" file accompanying this file. This file is 12 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 13 | # ANY KIND, either express or implied. See the License for the specific 14 | # language governing permissions and limitations under the License. 15 | 16 | from __future__ import unicode_literals 17 | from __future__ import print_function 18 | import re 19 | import six 20 | import shlex 21 | from prompt_toolkit.completion import Completion 22 | 23 | 24 | class TextUtils(object): 25 | """Utilities for parsing and matching text. 26 | 27 | Attributes: 28 | * None. 29 | """ 30 | 31 | def find_matches(self, word, collection, fuzzy): 32 | """Finds all matches in collection for word. 33 | 34 | Args: 35 | * word: A string representing the word before 36 | the cursor. 37 | * collection: A collection of words to match. 38 | * fuzzy: A boolean that specifies whether to use fuzzy matching. 39 | 40 | Yields: 41 | A generator of prompt_toolkit's Completions. 42 | """ 43 | word = self._last_token(word).lower() 44 | for suggestion in self._find_collection_matches( 45 | word, collection, fuzzy): 46 | yield suggestion 47 | 48 | def get_tokens(self, text): 49 | """Parses out all tokens. 50 | 51 | Args: 52 | * text: A string to split into tokens. 53 | 54 | Returns: 55 | A list of strings for each word in the text. 56 | """ 57 | if text is not None: 58 | text = text.strip() 59 | words = self._safe_split(text) 60 | return words 61 | return [] 62 | 63 | def get_token_index(self, text, collection): 64 | """Given a text return the index in the collection. 65 | 66 | Args: 67 | * text: A string to find and obtain the index. 68 | * collection: A collection of words to match. 69 | 70 | Returns: 71 | An integer representing the index in the collection 72 | where the text was found. 73 | """ 74 | for token in collection: 75 | if text in token: 76 | return collection.index(token) 77 | return None 78 | 79 | def _last_token(self, text): 80 | """Finds the last word in text. 81 | 82 | Args: 83 | * text: A string to parse and obtain the last word. 84 | 85 | Returns: 86 | A string representing the last word in the text. 87 | """ 88 | if text is not None: 89 | text = text.strip() 90 | if len(text) > 0: 91 | word = self._safe_split(text)[-1] 92 | word = word.strip() 93 | return word 94 | return '' 95 | 96 | def _fuzzy_finder(self, text, collection, case_sensitive=True): 97 | """Customized fuzzy finder with optional case-insensitive matching. 98 | 99 | Adapted from: https://github.com/amjith/fuzzyfinder. 100 | 101 | Args: 102 | text: A string which is typically entered by a user. 103 | collection: An iterable that represents a collection of strings 104 | which will be filtered based on the input `text`. 105 | case_sensitive: A boolean that indicates whether the find 106 | will be case sensitive. 107 | 108 | Returns: 109 | A generator object that produces a list of suggestions 110 | narrowed down from `collections` using the `text` input. 111 | """ 112 | suggestions = [] 113 | if case_sensitive: 114 | pat = '.*?'.join(map(re.escape, text)) 115 | else: 116 | pat = '.*?'.join(map(re.escape, text.lower())) 117 | regex = re.compile(pat) 118 | for item in collection: 119 | if case_sensitive: 120 | r = regex.search(item) 121 | else: 122 | r = regex.search(item.lower()) 123 | if r: 124 | suggestions.append((len(r.group()), r.start(), item)) 125 | 126 | return (z for _, _, z in sorted(suggestions)) 127 | 128 | def _find_collection_matches(self, word, collection, fuzzy): 129 | """Yields all matching names in list. 130 | 131 | Args: 132 | * word: A string representing the word before 133 | the cursor. 134 | * collection: A collection of words to match. 135 | * fuzzy: A boolean that specifies whether to use fuzzy matching. 136 | 137 | Yields: 138 | A generator of prompt_toolkit's Completions. 139 | """ 140 | word = word.lower() 141 | if fuzzy: 142 | for suggestion in self._fuzzy_finder(word, 143 | collection, 144 | case_sensitive=False): 145 | yield Completion(suggestion, -len(word)) 146 | else: 147 | for name in sorted(collection): 148 | if name.lower().startswith(word) or not word: 149 | yield Completion(name, -len(word)) 150 | 151 | def _shlex_split(self, text): 152 | """Wrapper for shlex, because it does not seem to handle unicode in 2.6. 153 | 154 | Args: 155 | * text: A string to split. 156 | 157 | Returns: 158 | A list that contains words for each split element of text. 159 | """ 160 | if six.PY2: 161 | text = text.encode('utf-8') 162 | return shlex.split(text) 163 | 164 | def _safe_split(self, text): 165 | """Safely splits the input text. 166 | 167 | Shlex can't always split. For example, "\" crashes the completer. 168 | 169 | Args: 170 | * text: A string to split. 171 | 172 | Returns: 173 | A list that contains words for each split element of text. 174 | 175 | """ 176 | try: 177 | words = self._shlex_split(text) 178 | return words 179 | except: 180 | return text 181 | -------------------------------------------------------------------------------- /scripts/create_changelog.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | gitchangelog > CHANGELOG_DRAFT 4 | pandoc --from=markdown --to=rst --output=CHANGELOG.rst CHANGELOG.md 5 | -------------------------------------------------------------------------------- /scripts/create_readme_rst.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | pandoc --from=markdown --to=rst --output=docs/source/README.rst README.md -------------------------------------------------------------------------------- /scripts/run_code_checks.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | flake8 --max-line-length=80 --exclude=build,scratch,docs,compat.py . 4 | -------------------------------------------------------------------------------- /scripts/set_changelog_as_readme.sh: -------------------------------------------------------------------------------- 1 | mv README.md README_temp.md 2 | cp CHANGELOG.rst README.md -------------------------------------------------------------------------------- /scripts/set_changelog_as_readme_undo.sh: -------------------------------------------------------------------------------- 1 | mv README_temp.md README.md -------------------------------------------------------------------------------- /scripts/update_docs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | scripts/create_readme_rst.sh 4 | python setup.py build_sphinx -------------------------------------------------------------------------------- /scripts/upload_pypi.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # N. Set CHANGELOG as `README.md` 4 | scripts/set_changelog_as_readme.sh 5 | # O. Register package with PyPi 6 | python setup.py register -r pypi 7 | # P. Upload to PyPi 8 | python setup.py sdist upload -r pypi 9 | # Q. Upload Sphinx docs to PyPi 10 | python setup.py upload_sphinx 11 | # R. Restore `README.md` 12 | scripts/set_changelog_as_readme_undo.sh 13 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | 4 | [build_sphinx] 5 | source-dir = docs/source 6 | build-dir = docs/build 7 | all_files = 1 8 | 9 | [upload_sphinx] 10 | upload-dir = docs/build/html -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from saws.__init__ import __version__ 2 | import sys 3 | try: 4 | from setuptools import setup, find_packages 5 | except ImportError: 6 | from distutils.core import setup 7 | 8 | install_requires = [ 9 | 'awscli>=1.7.46,<2.0.0', 10 | 'click>=4.0,<7.0', 11 | 'configobj>=5.0.6,<6.0.0', 12 | 'prompt-toolkit>=1.0.0,<1.1.0', 13 | 'six>=1.9.0,<2.0.0', 14 | 'pygments>=2.0.2,<3.0.0' 15 | ] 16 | 17 | if sys.version_info < (2, 7): 18 | # Introduced in Python 2.7 19 | install_requires.append('ordereddict>=1.1') 20 | if sys.version_info < (3, 4): 21 | # Backport of Python 3.4 enums to earlier versions 22 | install_requires.append('enum34>=1.0.4') 23 | 24 | setup( 25 | description='SAWS: A Supercharged AWS Command Line Interface (CLI)', 26 | author='Donne Martin', 27 | url='https://github.com/donnemartin/saws', 28 | download_url='https://pypi.python.org/pypi/saws', 29 | author_email='donne.martin@gmail.com', 30 | version=__version__, 31 | license='Apache License 2.0', 32 | install_requires=install_requires, 33 | extras_require={ 34 | 'testing': [ 35 | 'mock>=1.0.1', 36 | 'tox>=1.9.2' 37 | ], 38 | }, 39 | entry_points={ 40 | 'console_scripts': 'saws = saws.main:cli' 41 | }, 42 | packages=find_packages(), 43 | package_data={'saws': ['sawsrc', 44 | 'saws.shortcuts', 45 | 'data/SOURCES.txt', 46 | 'data/RESOURCES_SAMPLE.txt', 47 | 'data/OPTIONS.txt']}, 48 | scripts=[], 49 | name='saws', 50 | classifiers=[ 51 | 'Intended Audience :: Developers', 52 | 'Intended Audience :: System Administrators', 53 | 'License :: OSI Approved :: Apache Software License', 54 | 'Natural Language :: English', 55 | 'Programming Language :: Python', 56 | 'Programming Language :: Python :: 2.6', 57 | 'Programming Language :: Python :: 2.7', 58 | 'Programming Language :: Python :: 3', 59 | 'Programming Language :: Python :: 3.3', 60 | 'Programming Language :: Python :: 3.4', 61 | 'Topic :: Software Development', 62 | 'Topic :: Software Development :: Libraries :: Python Modules', 63 | ], 64 | ) 65 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2015 Donne Martin. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"). You 6 | # may not use this file except in compliance with the License. A copy of 7 | # the License is located at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # or in the "license" file accompanying this file. This file is 12 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 13 | # ANY KIND, either express or implied. See the License for the specific 14 | # language governing permissions and limitations under the License. 15 | -------------------------------------------------------------------------------- /tests/compat.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2015 Donne Martin. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"). You 6 | # may not use this file except in compliance with the License. A copy of 7 | # the License is located at 8 | # 9 | # http://aws.amazon.com/apache2.0/ 10 | # 11 | # or in the "license" file accompanying this file. This file is 12 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 13 | # ANY KIND, either express or implied. See the License for the specific 14 | # language governing permissions and limitations under the License. 15 | 16 | import sys 17 | 18 | if sys.version_info < (2, 7): 19 | import unittest2 as unittest 20 | else: 21 | import unittest 22 | -------------------------------------------------------------------------------- /tests/run_tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright 2015 Donne Martin. All Rights Reserved. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"). You 7 | # may not use this file except in compliance with the License. A copy of 8 | # the License is located at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # or in the "license" file accompanying this file. This file is 13 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 14 | # ANY KIND, either express or implied. See the License for the specific 15 | # language governing permissions and limitations under the License. 16 | 17 | from __future__ import unicode_literals 18 | from __future__ import print_function 19 | import unittest 20 | from test_completer import CompleterTest # NOQA 21 | from test_commands import CommandsTest # NOQA 22 | from test_resources import ResourcesTest # NOQA 23 | from test_options import OptionsTest # NOQA 24 | from test_saws import SawsTest # NOQA 25 | from test_toolbar import ToolbarTest # NOQA 26 | from test_keys import KeysTest # NOQA 27 | try: 28 | from test_cli import CliTest # NOQA 29 | except: 30 | # pexpect import fails on Windows 31 | pass 32 | 33 | 34 | if __name__ == '__main__': 35 | unittest.main() 36 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2015 Donne Martin. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"). You 6 | # may not use this file except in compliance with the License. A copy of 7 | # the License is located at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # or in the "license" file accompanying this file. This file is 12 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 13 | # ANY KIND, either express or implied. See the License for the specific 14 | # language governing permissions and limitations under the License. 15 | 16 | from __future__ import unicode_literals 17 | import unittest 18 | import pip 19 | import pexpect 20 | 21 | 22 | class CliTest(unittest.TestCase): 23 | 24 | def test_run_cli(self): 25 | self.cli = None 26 | self.step_cli_installed() 27 | self.step_run_cli() 28 | self.step_see_prompt() 29 | self.step_send_ctrld() 30 | 31 | def step_cli_installed(self): 32 | """ 33 | Make sure saws is in installed packages. 34 | """ 35 | dists = set([di.key for di in pip.get_installed_distributions()]) 36 | assert 'saws' in dists 37 | 38 | def step_run_cli(self): 39 | """ 40 | Run the process using pexpect. 41 | """ 42 | self.cli = pexpect.spawnu('saws') 43 | 44 | def step_see_prompt(self): 45 | """ 46 | Expect to see prompt. 47 | """ 48 | self.cli.expect('saws> ') 49 | 50 | def step_send_ctrld(self): 51 | """ 52 | Send Ctrl + D to exit. 53 | """ 54 | self.cli.sendcontrol('d') 55 | self.cli.expect(pexpect.EOF) 56 | -------------------------------------------------------------------------------- /tests/test_commands.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2015 Donne Martin. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"). You 6 | # may not use this file except in compliance with the License. A copy of 7 | # the License is located at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # or in the "license" file accompanying this file. This file is 12 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 13 | # ANY KIND, either express or implied. See the License for the specific 14 | # language governing permissions and limitations under the License. 15 | 16 | from __future__ import unicode_literals 17 | from __future__ import print_function 18 | import unittest 19 | import re 20 | from saws.commands import AwsCommands 21 | 22 | 23 | class CommandsTest(unittest.TestCase): 24 | 25 | def test_all_commands(self): 26 | aws_commands = AwsCommands() 27 | command_lists = aws_commands.all_commands 28 | num_results_list = [None] * \ 29 | aws_commands.CommandType.NUM_TYPES.value 30 | with open(AwsCommands.DATA_PATH) as f: 31 | for line in f: 32 | line = re.sub('\n', '', line) 33 | for command_header in aws_commands.headers: 34 | if command_header in line: 35 | command_type_value = aws_commands \ 36 | .header_to_type_map[command_header].value 37 | num_results_list[command_type_value] = \ 38 | int(line.strip(command_header)) 39 | continue 40 | for index, num_results in enumerate(num_results_list): 41 | assert(len(command_lists[index]) == num_results) 42 | -------------------------------------------------------------------------------- /tests/test_completer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2015 Donne Martin. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"). You 6 | # may not use this file except in compliance with the License. A copy of 7 | # the License is located at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # or in the "license" file accompanying this file. This file is 12 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 13 | # ANY KIND, either express or implied. See the License for the specific 14 | # language governing permissions and limitations under the License. 15 | 16 | from __future__ import unicode_literals 17 | from __future__ import print_function 18 | import unittest 19 | import mock 20 | import re 21 | from prompt_toolkit.document import Document 22 | from awscli import completer as awscli_completer 23 | from saws.completer import AwsCompleter 24 | from saws.commands import AwsCommands 25 | from saws.saws import Saws 26 | 27 | 28 | class CompleterTest(unittest.TestCase): 29 | 30 | @mock.patch('saws.resources.print') 31 | def setUp(self, mock_print): 32 | self.saws = Saws(refresh_resources=False) 33 | self.completer = self.create_completer() 34 | self.completer.resources._set_resources_path( 35 | 'data/RESOURCES_SAMPLE.txt') 36 | self.completer.refresh_resources_and_options() 37 | self.completer_event = self.create_completer_event() 38 | mock_print.assert_called_with('Loaded resources from cache') 39 | 40 | def create_completer(self): 41 | # TODO: Fix duplicate creation of AwsCompleter, which is already 42 | # created by Saws init 43 | self.aws_commands = AwsCommands() 44 | self.all_commands = self.aws_commands.all_commands 45 | self.commands, self.sub_commands, self.global_options, \ 46 | self.resource_options = self.all_commands 47 | return AwsCompleter(awscli_completer, 48 | self.all_commands, 49 | self.saws.config, 50 | self.saws.config_obj, 51 | self.saws.logger) 52 | 53 | def create_completer_event(self): 54 | return mock.Mock() 55 | 56 | def _get_completions(self, command): 57 | position = len(command) 58 | result = set(self.completer.get_completions( 59 | Document(text=command, cursor_position=position), 60 | self.completer_event)) 61 | return result 62 | 63 | def verify_completions(self, commands, expected): 64 | result = set() 65 | for command in commands: 66 | # Call the AWS CLI autocompleter 67 | result.update(self._get_completions(command)) 68 | result_texts = [] 69 | for item in result: 70 | # Each result item is a Completion object, 71 | # we are only interested in the text portion 72 | result_texts.append(item.text) 73 | assert result_texts 74 | if len(expected) == 1: 75 | assert expected[0] in result_texts 76 | else: 77 | for item in expected: 78 | assert item in result_texts 79 | 80 | def test_no_completions(self): 81 | command = 'aws ec2' 82 | expected = set([]) 83 | assert expected == self._get_completions(command) 84 | command = 'aws elb' 85 | assert expected == self._get_completions(command) 86 | command = 'aws elasticache' 87 | assert expected == self._get_completions(command) 88 | 89 | def test_ec2_commands(self): 90 | commands = ['aws e'] 91 | expected = ['ec2', 92 | 'ecs', 93 | 'efs', 94 | 'elasticache', 95 | 'elasticbeanstalk', 96 | 'elastictranscoder', 97 | 'elb', 98 | 'emr'] 99 | self.verify_completions(commands, expected) 100 | 101 | def test_ec2_subcommands(self): 102 | commands = ['aws ec2 c'] 103 | expected = ['cancel-bundle-task', 104 | 'cancel-conversion-task', 105 | 'cancel-export-task', 106 | 'cancel-import-task', 107 | 'cancel-reserved-instances-listing', 108 | 'cancel-spot-fleet-requests', 109 | 'cancel-spot-instance-requests'] 110 | self.verify_completions(commands, expected) 111 | 112 | def test_aws_command(self): 113 | commands = ['a', 'aw'] 114 | expected = [AwsCommands.AWS_COMMAND] 115 | self.verify_completions(commands, expected) 116 | 117 | def test_global_options(self): 118 | commands = ['aws -', 'aws --'] 119 | expected = self.saws \ 120 | .all_commands[AwsCommands.CommandType.GLOBAL_OPTIONS.value] 121 | self.verify_completions(commands, expected) 122 | 123 | def test_resource_options(self): 124 | commands = ['aws ec2 describe-instances --', 125 | 'aws s3api get-bucket-acl --', 126 | 'aws emr list-clusters --'] 127 | expected = self.saws \ 128 | .all_commands[AwsCommands.CommandType.RESOURCE_OPTIONS.value] 129 | self.verify_completions(commands, expected) 130 | 131 | def test_shortcuts(self): 132 | commands = ['aws ec2 ls', 133 | 'aws emr ls', 134 | 'aws elb ls', 135 | 'aws dynamodb ls'] 136 | expected = ['aws ec2 describe-instances', 137 | 'aws emr list-clusters', 138 | 'aws elb describe-load-balancers', 139 | 'aws dynamodb list-tables'] 140 | shortcuts = dict(zip(commands, expected)) 141 | for command, expect in shortcuts.items(): 142 | result = self.completer.replace_shortcut(command) 143 | assert result == expect 144 | 145 | def test_shortcuts_fuzzy(self): 146 | self.completer.fuzzy_match = True 147 | self.completer.shortcut_match = True 148 | commands = ['aws ec2ls'] 149 | expected = ['ec2 ls --instance-ids'] 150 | self.verify_completions(commands, expected) 151 | commands = ['aws ec2start'] 152 | expected = ['ec2 start-instances --instance-ids'] 153 | self.verify_completions(commands, expected) 154 | commands = ['aws ec2stop'] 155 | expected = ['ec2 stop-instances --instance-ids'] 156 | self.verify_completions(commands, expected) 157 | commands = ['aws ec2tagk'] 158 | expected = ['ec2 ls --ec2-tag-key'] 159 | self.verify_completions(commands, expected) 160 | commands = ['aws ec2tagv'] 161 | expected = ['ec2 ls --ec2-tag-value'] 162 | self.verify_completions(commands, expected) 163 | 164 | def test_substitutions(self): 165 | command = 'aws ec2 ls --filters "Name=tag-key,Values=%s prod"' 166 | expected = 'aws ec2 ls --filters "Name=tag-key,Values=prod"' 167 | result = self.completer.replace_substitution(command) 168 | assert result == expected 169 | command = 'aws ec2 ls --ec2-tag-key Stack' 170 | expected = \ 171 | 'aws ec2 describe-instances --filters "Name=tag-key,Values=Stack"' 172 | result = self.completer.replace_shortcut(command) 173 | assert result == expected 174 | command = 'aws ec2 ls --ec2-tag-value prod' 175 | expected = \ 176 | 'aws ec2 describe-instances --filters "Name=tag-value,Values=prod"' 177 | result = self.completer.replace_shortcut(command) 178 | assert result == expected 179 | 180 | def test_substitutions_with_more_tokens(self): 181 | command = 'aws ec2 ls --filters "Name=tag-key,Values=%s prod" ' \ 182 | '| grep IpAddress' 183 | expected = 'aws ec2 ls --filters "Name=tag-key,Values=prod" ' \ 184 | '| grep IpAddress' 185 | result = self.completer.replace_substitution(command) 186 | assert result == expected 187 | command = 'aws ec2 ls --ec2-tag-key Stack | grep IpAddress' 188 | expected = 'aws ec2 describe-instances --filters ' \ 189 | '"Name=tag-key,Values=Stack" | grep IpAddress' 190 | result = self.completer.replace_shortcut(command) 191 | assert result == expected 192 | command = 'aws ec2 ls --ec2-tag-value prod | grep IpAddress' 193 | expected = 'aws ec2 describe-instances --filters ' \ 194 | '"Name=tag-value,Values=prod" | grep IpAddress' 195 | result = self.completer.replace_shortcut(command) 196 | assert result == expected 197 | 198 | @mock.patch('saws.resources.print') 199 | def test_refresh_resources_and_options(self, mock_print): 200 | self.completer.refresh_resources_and_options(force_refresh=False) 201 | mock_print.assert_called_with('Loaded resources from cache') 202 | 203 | def test_instance_ids(self): 204 | commands = ['aws ec2 ls --instance-ids i-b'] 205 | expected = ['i-b875ecc3', 'i-b51d05f4', 'i-b3628153'] 206 | instance_ids = self.completer.resources.resource_lists[ 207 | self.completer.resources.ResourceType.INSTANCE_IDS.value] 208 | instance_ids.resources.extend(expected) 209 | self.verify_completions(commands, expected) 210 | commands = ['aws ec2 ls --instance-ids i-a'] 211 | expected = ['i-a51d05f4', 'i-a71bd617', 'i-a1637b56'] 212 | instance_ids.resources.extend(expected) 213 | self.verify_completions(commands, expected) 214 | 215 | def test_instance_ids_fuzzy(self): 216 | self.completer.fuzzy_match = True 217 | commands = ['aws ec2 ls --instance-ids a5'] 218 | expected = ['i-a875ecc3', 'i-a41d55f4', 'i-a3628153'] 219 | instance_ids = self.completer.resources.resource_lists[ 220 | self.completer.resources.ResourceType.INSTANCE_IDS.value] 221 | instance_ids.resources.extend(expected) 222 | self.verify_completions(commands, expected) 223 | 224 | def test_instance_keys(self): 225 | commands = ['aws ec2 ls --ec2-tag-key na'] 226 | expected = ['name', 'namE'] 227 | instance_tag_keys = self.completer.resources.resource_lists[ 228 | self.completer.resources.ResourceType.INSTANCE_TAG_KEYS.value] 229 | instance_tag_keys.resources.extend(expected) 230 | self.verify_completions(commands, expected) 231 | commands = ['aws ec2 ls --ec2-tag-key Sta'] 232 | expected = ['Stack'] 233 | instance_tag_keys.resources.extend(expected) 234 | self.verify_completions(commands, expected) 235 | 236 | def test_instance_tag_values(self): 237 | commands = ['aws ec2 ls --ec2-tag-value prod'] 238 | expected = ['production', 'production-blue', 'production-green'] 239 | instance_tag_values = self.completer.resources.resource_lists[ 240 | self.completer.resources.ResourceType.INSTANCE_TAG_VALUES.value] 241 | instance_tag_values.resources.extend(expected) 242 | self.verify_completions(commands, expected) 243 | commands = ['aws ec2 ls --ec2-tag-value test'] 244 | expected = ['testing'] 245 | instance_tag_values.resources.extend(expected) 246 | self.verify_completions(commands, expected) 247 | 248 | def test_bucket_names(self): 249 | commands = ['aws s3pi get-bucket-acl --bucket web-'] 250 | expected = ['web-server-logs', 'web-server-images'] 251 | bucket_names = self.completer.resources.resource_lists[ 252 | self.completer.resources.ResourceType.BUCKET_NAMES.value] 253 | for bucket_name in expected: 254 | bucket_names.add_bucket_name(bucket_name) 255 | self.verify_completions(commands, expected) 256 | 257 | def test_s3_uri(self): 258 | commands = ['aws s3 ls s3:'] 259 | expected = ['s3://web-server-logs', 's3://web-server-images'] 260 | bucket_uris = self.completer.resources.resource_lists[ 261 | self.completer.resources.ResourceType.BUCKET_URIS.value] 262 | for s3_uri in expected: 263 | bucket_name = re.sub('s3://', '', s3_uri) 264 | bucket_uris.add_bucket_name(bucket_name) 265 | self.verify_completions(commands, expected) 266 | commands = ['aws s3 ls s3://web'] 267 | self.verify_completions(commands, expected) 268 | 269 | def test_ec2_states(self): 270 | commands = ['aws ec2 ls --ec2-state pend'] 271 | expected = ['pending'] 272 | self.verify_completions(commands, expected) 273 | commands = ['aws ec2 ls --ec2-state run'] 274 | expected = ['running'] 275 | self.verify_completions(commands, expected) 276 | commands = ['aws ec2 ls --ec2-state shut'] 277 | expected = ['shutting-down'] 278 | self.verify_completions(commands, expected) 279 | commands = ['aws ec2 ls --ec2-state term'] 280 | expected = ['terminated'] 281 | self.verify_completions(commands, expected) 282 | commands = ['aws ec2 ls --ec2-state stop'] 283 | expected = ['stopping', 284 | 'stopped'] 285 | self.verify_completions(commands, expected) 286 | 287 | def test_cluster_states(self): 288 | self.verify_cluster_states() 289 | 290 | def test_cluster_states_fuzzy(self): 291 | self.completer.fuzzy_match = True 292 | self.verify_cluster_states() 293 | 294 | def verify_cluster_states(self): 295 | commands = ['aws emr ls --cluster-states star'] 296 | expected = ['STARTING'] 297 | self.verify_completions(commands, expected) 298 | commands = ['emr ls --cluster-states BOOT'] 299 | expected = ['BOOTSTRAPPING'] 300 | self.verify_completions(commands, expected) 301 | commands = ['emr ls --cluster-states run'] 302 | expected = ['RUNNING'] 303 | self.verify_completions(commands, expected) 304 | commands = ['emr ls --cluster-states WAIT'] 305 | expected = ['WAITING'] 306 | self.verify_completions(commands, expected) 307 | commands = ['emr ls --cluster-states term'] 308 | expected = ['TERMINATING', 309 | 'TERMINATED', 310 | 'TERMINATED_WITH_ERRORS'] 311 | self.verify_completions(commands, expected) 312 | -------------------------------------------------------------------------------- /tests/test_keys.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2015 Donne Martin. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"). You 6 | # may not use this file except in compliance with the License. A copy of 7 | # the License is located at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # or in the "license" file accompanying this file. This file is 12 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 13 | # ANY KIND, either express or implied. See the License for the specific 14 | # language governing permissions and limitations under the License. 15 | 16 | from __future__ import unicode_literals 17 | from __future__ import print_function 18 | import mock 19 | from tests.compat import unittest 20 | from prompt_toolkit.key_binding.input_processor import KeyPress 21 | from prompt_toolkit.keys import Keys 22 | from saws.saws import Saws 23 | 24 | 25 | class KeysTest(unittest.TestCase): 26 | 27 | def setUp(self): 28 | self.saws = Saws(refresh_resources=False) 29 | self.registry = self.saws.key_manager.manager.registry 30 | self.processor = self.saws.aws_cli.input_processor 31 | self.DOCS_HOME_URL = \ 32 | 'http://docs.aws.amazon.com/cli/latest/reference/index.html' 33 | 34 | def feed_key(self, key): 35 | self.processor.feed(KeyPress(key, u'')) 36 | self.processor.process_keys() 37 | 38 | def test_F2(self): 39 | orig_color = self.saws.get_color() 40 | self.feed_key(Keys.F2) 41 | assert orig_color != self.saws.get_color() 42 | 43 | def test_F3(self): 44 | orig_fuzzy = self.saws.get_fuzzy_match() 45 | self.feed_key(Keys.F3) 46 | assert orig_fuzzy != self.saws.get_fuzzy_match() 47 | 48 | def test_F4(self): 49 | orig_shortcut = self.saws.get_shortcut_match() 50 | self.feed_key(Keys.F4) 51 | assert orig_shortcut != self.saws.get_shortcut_match() 52 | 53 | @mock.patch('saws.saws.webbrowser') 54 | def test_F9(self, mock_webbrowser): 55 | self.feed_key(Keys.F9) 56 | mock_webbrowser.open.assert_called_with(self.DOCS_HOME_URL) 57 | 58 | def test_F10(self): 59 | with self.assertRaises(EOFError): 60 | self.feed_key(Keys.F10) 61 | 62 | @mock.patch('saws.resources.print') 63 | def test_f5(self, mock_print): 64 | self.feed_key(Keys.F5) 65 | mock_print.assert_called_with('Done refreshing') 66 | -------------------------------------------------------------------------------- /tests/test_options.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2015 Donne Martin. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"). You 6 | # may not use this file except in compliance with the License. A copy of 7 | # the License is located at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # or in the "license" file accompanying this file. This file is 12 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 13 | # ANY KIND, either express or implied. See the License for the specific 14 | # language governing permissions and limitations under the License. 15 | 16 | from __future__ import unicode_literals 17 | from __future__ import print_function 18 | import unittest 19 | from saws.saws import Saws 20 | 21 | 22 | class OptionsTest(unittest.TestCase): 23 | 24 | def setUp(self): 25 | self.create_options() 26 | 27 | def create_options(self): 28 | self.saws = Saws(refresh_resources=False) 29 | self.options = self.saws.completer.options 30 | 31 | def test_make_header(self): 32 | option = '--ec2-state' 33 | header = '--ec2-state: ' 34 | assert header == self.options._make_options_header(option) 35 | 36 | def test_generate_cluster_states(self): 37 | self.options.cluster_states = [] 38 | self.options.cluster_states = self.options._generate_cluster_states() 39 | states = ['STARTING', 'BOOTSTRAPPING', 'RUNNING', 'WAITING', 40 | 'TERMINATING', 'TERMINATED', 'TERMINATED_WITH_ERRORS'] 41 | for state in states: 42 | assert state in self.options.cluster_states 43 | -------------------------------------------------------------------------------- /tests/test_resources.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2015 Donne Martin. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"). You 6 | # may not use this file except in compliance with the License. A copy of 7 | # the License is located at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # or in the "license" file accompanying this file. This file is 12 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 13 | # ANY KIND, either express or implied. See the License for the specific 14 | # language governing permissions and limitations under the License. 15 | 16 | from __future__ import unicode_literals 17 | from __future__ import print_function 18 | import mock 19 | from tests.compat import unittest 20 | from saws.saws import Saws 21 | 22 | 23 | class ResourcesTest(unittest.TestCase): 24 | 25 | NUM_SAMPLE_INSTANCE_IDS = 7 26 | NUM_SAMPLE_INSTANCE_TAG_KEYS = 3 27 | NUM_SAMPLE_INSTANCE_TAG_VALUES = 6 28 | NUM_SAMPLE_BUCKET_NAMES = 16 29 | NUM_SAMPLE_BUCKET_URIS = 16 30 | 31 | def setUp(self): 32 | self.create_resources() 33 | self.sample_resource_counts = [ 34 | self.NUM_SAMPLE_INSTANCE_IDS, 35 | self.NUM_SAMPLE_INSTANCE_TAG_KEYS, 36 | self.NUM_SAMPLE_INSTANCE_TAG_VALUES, 37 | self.NUM_SAMPLE_BUCKET_NAMES, 38 | self.NUM_SAMPLE_BUCKET_URIS 39 | ] 40 | 41 | def create_resources(self): 42 | self.saws = Saws(refresh_resources=False) 43 | self.resources = self.saws.completer.resources 44 | self.resources._set_resources_path('data/RESOURCES_SAMPLE.txt') 45 | 46 | def verify_resources(self): 47 | for resource_list, sample_resource_count in zip( 48 | self.resources.resource_lists, 49 | self.sample_resource_counts): 50 | assert len(resource_list.resources) == sample_resource_count 51 | 52 | # TODO: Silence output 53 | @mock.patch('saws.resources.print') 54 | def test_refresh_forced(self, mock_print): 55 | self.resources._set_resources_path('data/RESOURCES_FORCED.txt') 56 | self.resources.clear_resources() 57 | self.resources.refresh(force_refresh=True) 58 | mock_print.assert_called_with('Done refreshing') 59 | 60 | # TODO: Silence output 61 | @mock.patch('saws.resources.print') 62 | def test_refresh(self, mock_print): 63 | self.resources.refresh(force_refresh=False) 64 | self.verify_resources() 65 | mock_print.assert_called_with('Loaded resources from cache') 66 | 67 | # TODO: Fix mocks 68 | @unittest.skip('') 69 | @mock.patch('saws.resources.subprocess') 70 | def test_query_aws_instance_ids(self, mock_subprocess): 71 | instance_ids = self.resources.resource_lists[ 72 | self.resources.ResourceType.INSTANCE_IDS.value] 73 | instance_ids._query_aws(instance_ids.QUERY) 74 | mock_subprocess.check_output.assert_called_with( 75 | instance_ids.QUERY, 76 | universal_newlines=True, 77 | shell=True) 78 | 79 | # TODO: Fix mocks 80 | @unittest.skip('') 81 | @mock.patch('saws.resources.subprocess') 82 | def test_query_aws_instance_tag_keys(self, mock_subprocess): 83 | instance_tag_keys = self.resources.resource_lists[ 84 | self.resources.ResourceType.INSTANCE_TAG_KEYS.value] 85 | instance_tag_keys._query_aws(instance_tag_keys.QUERY) 86 | mock_subprocess.check_output.assert_called_with( 87 | instance_tag_keys.QUERY, 88 | universal_newlines=True, 89 | shell=True) 90 | 91 | # TODO: Fix mocks 92 | # @unittest.skip('') 93 | @mock.patch('saws.resources.subprocess') 94 | def query_aws_instance_tag_values(self, mock_subprocess): 95 | instance_tag_values = self.resources.resource_lists[ 96 | self.resources.ResourceType.INSTANCE_TAG_VALUES.value] 97 | instance_tag_values._query_aws(instance_tag_values.QUERY) 98 | mock_subprocess.check_output.assert_called_with( 99 | instance_tag_values.QUERY, 100 | universal_newlines=True, 101 | shell=True) 102 | 103 | # TODO: Fix mocks 104 | @unittest.skip('') 105 | @mock.patch('saws.resources.subprocess') 106 | def test_query_aws_bucket_names(self, mock_subprocess): 107 | bucket_names = self.resources.resource_lists[ 108 | self.resources.ResourceType.BUCKET_NAMES.value] 109 | bucket_names._query_aws(bucket_names.QUERY) 110 | mock_subprocess.check_output.assert_called_with( 111 | bucket_names.QUERY, 112 | universal_newlines=True, 113 | shell=True) 114 | 115 | def test_add_and_clear_bucket_name(self): 116 | BUCKET_NAME = 'test_bucket_name' 117 | bucket_names = self.resources.resource_lists[ 118 | self.resources.ResourceType.BUCKET_NAMES.value] 119 | bucket_uris = self.resources.resource_lists[ 120 | self.resources.ResourceType.BUCKET_URIS.value] 121 | bucket_names.clear_resources() 122 | bucket_names.add_bucket_name(BUCKET_NAME) 123 | assert BUCKET_NAME in bucket_names.resources 124 | bucket_uris.add_bucket_name(BUCKET_NAME) 125 | BUCKET_URI = bucket_uris.PREFIX + BUCKET_NAME 126 | assert BUCKET_URI in bucket_uris.resources 127 | bucket_names.clear_resources() 128 | bucket_uris.clear_resources() 129 | assert len(bucket_names.resources) == 0 130 | assert len(bucket_uris.resources) == 0 131 | -------------------------------------------------------------------------------- /tests/test_saws.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2015 Donne Martin. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"). You 6 | # may not use this file except in compliance with the License. A copy of 7 | # the License is located at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # or in the "license" file accompanying this file. This file is 12 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 13 | # ANY KIND, either express or implied. See the License for the specific 14 | # language governing permissions and limitations under the License. 15 | 16 | from __future__ import unicode_literals 17 | from __future__ import print_function 18 | import mock 19 | import os 20 | import traceback 21 | from tests.compat import unittest 22 | from saws.saws import Saws 23 | from saws.commands import AwsCommands 24 | 25 | 26 | class SawsTest(unittest.TestCase): 27 | 28 | def setUp(self): 29 | self.file_name = os.path.expanduser('~') + '/' + '.saws.log' 30 | self.saws = Saws(refresh_resources=False) 31 | self.DOCS_HOME_URL = \ 32 | 'http://docs.aws.amazon.com/cli/latest/reference/index.html' 33 | 34 | @mock.patch('saws.saws.click') 35 | def test_log_exception(self, mock_click): 36 | exception_message = 'test_log_exception' 37 | e = Exception(exception_message) 38 | try: 39 | raise e 40 | except Exception: 41 | # Traceback needs to have an active exception as described in: 42 | # http://bugs.python.org/issue23003 43 | self.saws.log_exception(e, traceback, echo=True) 44 | mock_click.secho.assert_called_with(str(e), fg='red') 45 | assert os.path.isfile(self.file_name) 46 | with open(self.file_name, 'r') as fp: 47 | for line in fp: 48 | pass 49 | assert exception_message in line 50 | 51 | def test_set_get_color(self): 52 | self.saws.set_color(True) 53 | assert self.saws.get_color() 54 | self.saws.set_color(False) 55 | assert not self.saws.get_color() 56 | 57 | def test_get_set_fuzzy_match(self): 58 | self.saws.set_fuzzy_match(True) 59 | assert self.saws.get_fuzzy_match() 60 | self.saws.set_fuzzy_match(False) 61 | assert not self.saws.get_fuzzy_match() 62 | 63 | def test_get_set_shortcut_match(self): 64 | self.saws.set_shortcut_match(True) 65 | assert self.saws.get_shortcut_match() 66 | self.saws.set_shortcut_match(False) 67 | assert not self.saws.get_shortcut_match() 68 | 69 | @mock.patch('saws.saws.webbrowser') 70 | def test_handle_docs(self, mock_webbrowser): 71 | EC2_URL = \ 72 | 'http://docs.aws.amazon.com/cli/latest/reference/ec2/index.html' 73 | EC2_DESC_INSTANCES_URL = \ 74 | 'http://docs.aws.amazon.com/cli/latest/reference/ec2/describe-instances.html' # NOQA 75 | assert not self.saws.handle_docs('') 76 | assert not self.saws.handle_docs('foo bar') 77 | assert self.saws.handle_docs('', 78 | from_fkey=True) 79 | mock_webbrowser.open.assert_called_with(self.DOCS_HOME_URL) 80 | assert self.saws.handle_docs('baz', 81 | from_fkey=True) 82 | mock_webbrowser.open.assert_called_with(self.DOCS_HOME_URL) 83 | assert self.saws.handle_docs('aws ec2', 84 | from_fkey=True) 85 | mock_webbrowser.open.assert_called_with(EC2_URL) 86 | assert self.saws.handle_docs('aws ec2 docs', 87 | from_fkey=False) 88 | mock_webbrowser.open.assert_called_with(EC2_URL) 89 | assert self.saws.handle_docs('aws ec2 describe-instances', 90 | from_fkey=True) 91 | mock_webbrowser.open.assert_called_with(EC2_DESC_INSTANCES_URL) 92 | assert self.saws.handle_docs('aws ec2 describe-instances docs', 93 | from_fkey=False) 94 | mock_webbrowser.open.assert_called_with(EC2_DESC_INSTANCES_URL) 95 | 96 | @mock.patch('saws.saws.os') 97 | def test_handle_cd(self, mock_os): 98 | assert not self.saws._handle_cd('aws') 99 | assert self.saws._handle_cd('cd ') 100 | assert self.saws._handle_cd('cd foo') 101 | mock_os.chdir.assert_called_with('foo') 102 | 103 | def test_colorize_output(self): 104 | self.saws.set_color(False) 105 | assert self.saws._colorize_output(AwsCommands.AWS_COMMAND) == \ 106 | AwsCommands.AWS_COMMAND 107 | self.saws.set_color(True) 108 | assert self.saws._colorize_output(AwsCommands.AWS_CONFIGURE) == \ 109 | AwsCommands.AWS_CONFIGURE 110 | assert self.saws._colorize_output(AwsCommands.AWS_HELP) == \ 111 | AwsCommands.AWS_HELP 112 | EC2_LS_CMD = 'aws ec2 ls' 113 | assert self.saws._colorize_output(EC2_LS_CMD) == \ 114 | EC2_LS_CMD + self.saws.PYGMENTS_CMD 115 | 116 | @mock.patch('saws.saws.subprocess') 117 | @mock.patch('saws.saws.webbrowser') 118 | def test_process_command_docs(self, mock_webbrowser, mock_subprocess): 119 | self.saws._process_command('aws docs') 120 | mock_webbrowser.open.assert_called_with(self.DOCS_HOME_URL) 121 | mock_subprocess.call.assert_not_called() 122 | 123 | @mock.patch('saws.saws.subprocess') 124 | def test_process_command_cd(self, mock_subprocess): 125 | self.saws._process_command('cd .') 126 | mock_subprocess.call.assert_not_called() 127 | 128 | @mock.patch('saws.saws.subprocess') 129 | def test_process_command(self, mock_subprocess): 130 | self.saws.set_color(False) 131 | INVAL_CMD = 'foo' 132 | self.saws._process_command(INVAL_CMD) 133 | mock_subprocess.call.assert_called_with(INVAL_CMD, 134 | shell=True) 135 | self.saws._process_command(AwsCommands.AWS_COMMAND) 136 | mock_subprocess.call.assert_called_with(AwsCommands.AWS_COMMAND, 137 | shell=True) 138 | self.saws.set_color(True) 139 | colorized_command = AwsCommands.AWS_COMMAND + self.saws.PYGMENTS_CMD 140 | self.saws._process_command(AwsCommands.AWS_COMMAND) 141 | mock_subprocess.call.assert_called_with(colorized_command, 142 | shell=True) 143 | 144 | def test_handle_keyboard_interrupt(self): 145 | e = KeyboardInterrupt('') 146 | # TODO: Mock calls to renderer.clear and input_processor.feed 147 | self.saws._handle_keyboard_interrupt(e, platform='Darwin') 148 | with self.assertRaises(KeyboardInterrupt): 149 | self.saws._handle_keyboard_interrupt(e, platform='Windows') 150 | -------------------------------------------------------------------------------- /tests/test_toolbar.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2015 Donne Martin. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"). You 6 | # may not use this file except in compliance with the License. A copy of 7 | # the License is located at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # or in the "license" file accompanying this file. This file is 12 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 13 | # ANY KIND, either express or implied. See the License for the specific 14 | # language governing permissions and limitations under the License. 15 | 16 | from __future__ import unicode_literals 17 | from __future__ import print_function 18 | import unittest 19 | from pygments.token import Token 20 | from saws.saws import Saws 21 | from saws.toolbar import Toolbar 22 | 23 | 24 | class ToolbarTest(unittest.TestCase): 25 | 26 | def setUp(self): 27 | self.saws = Saws(refresh_resources=False) 28 | self.toolbar = Toolbar(self.saws.get_color, 29 | self.saws.get_fuzzy_match, 30 | self.saws.get_shortcut_match) 31 | 32 | def test_toolbar_on(self): 33 | self.saws.set_color(True) 34 | self.saws.set_fuzzy_match(True) 35 | self.saws.set_shortcut_match(True) 36 | expected = [ 37 | (Token.Toolbar.On, ' [F2] Color: ON '), 38 | (Token.Toolbar.On, ' [F3] Fuzzy: ON '), 39 | (Token.Toolbar.On, ' [F4] Shortcuts: ON '), 40 | (Token.Toolbar, ' [F5] Refresh '), 41 | (Token.Toolbar, ' [F9] Docs '), 42 | (Token.Toolbar, ' [F10] Exit ')] 43 | assert expected == self.toolbar.handler(None) 44 | 45 | def test_toolbar_off(self): 46 | self.saws.set_color(False) 47 | self.saws.set_fuzzy_match(False) 48 | self.saws.set_shortcut_match(False) 49 | expected = [ 50 | (Token.Toolbar.Off, ' [F2] Color: OFF '), 51 | (Token.Toolbar.Off, ' [F3] Fuzzy: OFF '), 52 | (Token.Toolbar.Off, ' [F4] Shortcuts: OFF '), 53 | (Token.Toolbar, ' [F5] Refresh '), 54 | (Token.Toolbar, ' [F9] Docs '), 55 | (Token.Toolbar, ' [F10] Exit ')] 56 | assert expected == self.toolbar.handler(None) 57 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (http://tox.testrun.org/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | envlist = py27, py36, pypy 8 | 9 | [testenv] 10 | passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH 11 | deps = 12 | coverage 13 | mock 14 | prompt_toolkit 15 | unittest2 16 | commands = 17 | coverage erase 18 | coverage run {toxinidir}/tests/run_tests.py 19 | coverage report --include={toxinidir}/*saws/* 20 | --------------------------------------------------------------------------------