├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── s3env.json ├── s3env.py ├── setup.cfg ├── setup.py └── tty.gif /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Version 0.0.4 4 | 5 | 2017-02-15 6 | 7 | Added full README to pypi. 8 | 9 | ## Version 0.0.1 10 | 11 | 2017-02-15 12 | 13 | Initial release! 14 | 15 | - Added the ability to `get` `set` and `rm` keys from an json file on an s3 bucket. 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Cameron Maske 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | Contact GitHub API Training Shop Blog About 9 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # S3env 2 | 3 | Manipulate a key/value JSON object file in an S3 bucket through the CLI. Built to ease setting [remote enviroment variables with Zappa](https://github.com/Miserlou/Zappa#remote-environment-variables). 4 | 5 | ## Usage 6 | 7 | ![](tty.gif) 8 | 9 | ### Quick start. 10 | ``` 11 | $ s3env prod get 12 | API_KEY=secret 13 | FOO=bar 14 | 15 | $ s3env prod set:BONJOUR=hello 16 | Key successfully set. 17 | Current key/values are... 18 | API_KEY=secret 19 | FOO=bar 20 | BONJOUR=hello 21 | 22 | $ s3env prod rm:BONJOIR 23 | Key removed. 24 | Current key/values are... 25 | API_KEY=secret 26 | FOO=bar 27 | ``` 28 | 29 | ### Getting started. 30 | 31 | Install via... 32 | ``` 33 | pip install s3env 34 | ``` 35 | 36 | Create an `s3env.json`. 37 | ``` 38 | { 39 | "prod": "s3://your-bucket-here/file-in-bucket.json" 40 | } 41 | ```` 42 | 43 | Run commands. 44 | ``` 45 | $ s3env --help 46 | ``` 47 | 48 | ## Development 49 | 50 | After pulling down the repo locally, create a virtualenv, then install by running... 51 | ``` 52 | pip install -e . 53 | ``` 54 | 55 | 56 | ## Deployment 57 | Publish to pypi with... 58 | ``` 59 | python setup.py sdist upload -r pypi 60 | ``` 61 | -------------------------------------------------------------------------------- /s3env.json: -------------------------------------------------------------------------------- 1 | { 2 | "prod": "s3://s3-env-a91he1s1/example.json" 3 | } -------------------------------------------------------------------------------- /s3env.py: -------------------------------------------------------------------------------- 1 | import click 2 | import json 3 | import bucketstore 4 | import botocore 5 | 6 | 7 | def s3env_json(): 8 | try: 9 | with open("s3env.json") as f: 10 | return json.load(f) 11 | except IOError: 12 | raise click.ClickException("Cannot find an s3env.json") 13 | 14 | 15 | def get_bucket_name_and_key_path(name): 16 | try: 17 | s3_path = s3env_json()[name] 18 | except KeyError: 19 | raise click.ClickException("Cannot find {} in s3env.json".format(name)) 20 | 21 | if s3_path.startswith("s3://"): 22 | s3_path = s3_path.replace("s3://", "") 23 | return s3_path.split("/", 1) 24 | 25 | 26 | def get_key(env): 27 | bucket_name, key_path = get_bucket_name_and_key_path(env) 28 | try: 29 | bucket = bucketstore.get(bucket_name) 30 | except ValueError: 31 | if click.confirm("The bucket {} does not exist, would you like to create it?".format(bucket_name)): 32 | try: 33 | bucket = bucketstore.get(bucket_name, create=True) 34 | except botocore.exceptions.ClientError as e: 35 | if (e.response["Error"]["Code"] == "BucketAlreadyExists"): 36 | raise click.ClickException("The bucket {} already exists.\nIf you created this bucket, you currently credentials do not have access, else try renaming to a different bucket".format(bucket_name)) 37 | else: 38 | raise e 39 | else: 40 | return 41 | try: 42 | key = bucket.key(key_path) 43 | # Call the meta, will raise the error if does not exist 44 | key.meta 45 | except botocore.exceptions.ClientError as e: 46 | if (e.response["Error"]["Code"] == "NoSuchKey"): 47 | bucket.set(key_path, "{}") 48 | key = bucket.key(key_path) 49 | else: 50 | raise e 51 | return key 52 | 53 | 54 | @click.group() 55 | @click.argument("env") 56 | @click.pass_context 57 | def cli(ctx, env): 58 | """ 59 | Edit an s3 json file. 60 | """ 61 | ctx.obj = { 62 | "env": env 63 | } 64 | 65 | 66 | @cli.command() 67 | @click.pass_context 68 | def get(ctx): 69 | """ 70 | Get key/values from an s3 json file. 71 | 72 | \b 73 | 74 | s3env prod get 75 | s3env staging get 76 | """ 77 | s3_key = get_key(ctx.obj["env"]) 78 | key_as_json = json.loads(s3_key.get()) 79 | if not key_as_json: 80 | click.echo("No key/values are currently set.") 81 | else: 82 | click.echo("Current key/values are...") 83 | for k, v in key_as_json.items(): 84 | click.echo("{}={}".format(k, v)) 85 | 86 | 87 | @cli.command() 88 | @click.argument('key') 89 | @click.argument('value') 90 | @click.pass_context 91 | def set(ctx, key, value): 92 | """ 93 | Set a new key/value in an s3 json file. 94 | 95 | \b 96 | 97 | s3env prod set API_KEY "secret" 98 | s3env prod set DATABASE_URL sqlite3://mydatabase:password:user:9200 99 | """ 100 | s3_key = get_key(ctx.obj["env"]) 101 | key_as_json = json.loads(s3_key.get()) 102 | key_as_json[key] = value 103 | s3_key.set(json.dumps(key_as_json, indent=4)) 104 | click.echo("Key successfully set.") 105 | click.echo("Current key/values are...") 106 | for k, v in key_as_json.items(): 107 | click.echo("{}={}".format(k, v)) 108 | 109 | 110 | @cli.command() 111 | @click.argument('key') 112 | @click.pass_context 113 | def rm(ctx, key): 114 | """ 115 | Remove a key from an s3 json file. 116 | 117 | \b 118 | 119 | s3env prod rm API_KEY 120 | s3env staging rm DEBUG 121 | """ 122 | s3_key = get_key(ctx.obj["env"]) 123 | key_as_json = json.loads(s3_key.get()) 124 | try: 125 | del key_as_json[key] 126 | click.echo("Key removed.") 127 | except KeyError: 128 | raise click.ClickException("No key set for {}".format(key)) 129 | s3_key.set(json.dumps(key_as_json, indent=4)) 130 | if not key_as_json: 131 | click.echo("No key/values are currently set.") 132 | else: 133 | click.echo("Current key/values are...") 134 | for k, v in key_as_json.items(): 135 | click.echo("{}={}".format(k, v)) 136 | 137 | 138 | if __name__ == "__main__": 139 | cli(obj={}) 140 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | requires = [ 4 | 'click==6.7', 5 | 'bucketstore==0.1.1' 6 | ] 7 | 8 | 9 | setup( 10 | name="s3env", 11 | version="0.0.4", 12 | author="Cameron Maske", 13 | description="Manipulate a key/value JSON object file in an S3 bucket through the CLI", 14 | author_email="cameronmaske@gmail.com", 15 | url='https://github.com/cameronmaske/s3env', 16 | py_modules=['s3env'], 17 | license='MIT', 18 | install_requires=requires, 19 | entry_points=''' 20 | [console_scripts] 21 | s3env=s3env:cli 22 | ''', 23 | ) 24 | -------------------------------------------------------------------------------- /tty.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cameronmaske/s3env/83cf2dcfc8334caa59d79d36d8a3c0acda80c8ee/tty.gif --------------------------------------------------------------------------------