├── .gitignore ├── .travis.yml ├── CHANGELOG.rst ├── LICENSE ├── README.rst ├── cfenv └── __init__.py ├── dev-requirements.txt ├── setup.cfg ├── setup.py ├── tasks.py └── tests └── test_cfenv.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 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 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | 37 | # Complexity 38 | output/*.html 39 | output/*/index.html 40 | 41 | # Sphinx 42 | docs/_build 43 | README.html 44 | 45 | _sandbox 46 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | sudo: false 4 | 5 | python: 6 | - "2.7" 7 | - "3.3" 8 | - "3.4" 9 | - "3.5" 10 | - "pypy" 11 | 12 | install: 13 | - travis_retry pip install -U . 14 | - travis_retry pip install -U -r dev-requirements.txt 15 | 16 | before_script: 17 | - flake8 . 18 | 19 | script: py.test 20 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | --------- 3 | 4 | 0.5.3 (2017-06-09) 5 | ++++++++++++++++++ 6 | * Fix license in setup.py. Thanks @mmessmore! 7 | 8 | 0.5.2 (2016-03-10) 9 | ++++++++++++++++++ 10 | * Parse instance index to int. 11 | 12 | 0.5.1 (2016-02-03) 13 | ++++++++++++++++++ 14 | * Allow regular expressions in get_service. 15 | * Always return list from uris. 16 | 17 | 0.4.0 (2015-11-24) 18 | ++++++++++++++++++ 19 | * Add default value for get_credential. 20 | 21 | 0.3.0 (2015-11-24) 22 | ++++++++++++++++++ 23 | * Add space and index properties. 24 | * Add flexible key matching on get_service. 25 | 26 | 0.2.0 (2015-11-24) 27 | ++++++++++++++++++ 28 | * Add `get_credential` helper. 29 | 30 | 0.1.0 (2015-11-21) 31 | ++++++++++++++++++ 32 | * First release. 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 Joshua Carp 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | py-cfenv 3 | ======== 4 | 5 | .. image:: https://img.shields.io/pypi/v/cfenv.svg 6 | :target: http://badge.fury.io/py/cfenv 7 | :alt: Latest version 8 | 9 | .. image:: https://img.shields.io/travis/jmcarp/py-cfenv/master.svg 10 | :target: https://travis-ci.org/jmcarp/py-cfenv 11 | :alt: Travis-CI 12 | 13 | **py-cfenv** is a tiny utility that simplifies interactions with Cloud Foundry environment variables, modeled after node-cfenv_. 14 | 15 | Quickstart 16 | ---------- 17 | 18 | .. code-block:: python 19 | 20 | from cfenv import AppEnv 21 | 22 | env = AppEnv() 23 | env.name # 'test-app' 24 | env.port # 5000 25 | 26 | redis = env.get_service(label='redis') 27 | redis.credentials # {'uri': '...', 'password': '...'} 28 | redis.get_url(host='hostname', password='password', port='port') # redis://pass:host 29 | 30 | Keys may change based on the service. To see what keys are available for the app's services: 31 | 32 | .. code-block:: bash 33 | 34 | $ cf env my-app 35 | 36 | Getting env variables for app my-app in org my-org / space my-space as me@example.com... 37 | OK 38 | 39 | System-Provided: 40 | { 41 | "VCAP_SERVICES": { 42 | "redis": [ 43 | { 44 | "credentials": { 45 | "hostname": "example.redis.host", 46 | "password": "verysecurepassword", 47 | "port": "30773", 48 | "ports": { 49 | "6379/tcp": "30773" 50 | }, 51 | "uri": "redis://:verysecurepassword@example.redis.host:30773" 52 | }, 53 | "label": "redis", 54 | "name": "example-redis", 55 | "plan": "standard", 56 | "provider": null, 57 | "syslog_drain_url": null, 58 | "tags": [ 59 | "redis28", 60 | "redis" 61 | ], 62 | "volume_mounts": [] 63 | } 64 | ] 65 | } 66 | } 67 | 68 | .. _node-cfenv: https://github.com/cloudfoundry-community/node-cfenv/ 69 | -------------------------------------------------------------------------------- /cfenv/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import re 5 | import json 6 | 7 | import furl 8 | 9 | __version__ = '0.5.3' 10 | 11 | RegexType = type(re.compile('')) 12 | 13 | class AppEnv(object): 14 | 15 | def __init__(self): 16 | self.app = json.loads(os.getenv('VCAP_APPLICATION', '{}')) 17 | self.services = [ 18 | Service(each) 19 | for services in json.loads(os.getenv('VCAP_SERVICES', '{}')).values() 20 | for each in services 21 | ] 22 | 23 | @property 24 | def name(self): 25 | return self.app.get('name') 26 | 27 | @property 28 | def space(self): 29 | return self.app.get('space_name') 30 | 31 | @property 32 | def index(self): 33 | index = os.getenv('CF_INSTANCE_INDEX') 34 | return int(index) if index else None 35 | 36 | @property 37 | def port(self): 38 | port = ( 39 | os.getenv('PORT') or 40 | os.getenv('CF_INSTANCE_PORT') or 41 | os.getenv('VCAP_APP_PORT') 42 | ) 43 | return int(port) if port else None 44 | 45 | @property 46 | def bind(self): 47 | return self.app.get('host', 'localhost') 48 | 49 | @property 50 | def uris(self): 51 | return self.app.get('uris', []) 52 | 53 | def get_service(self, **kwargs): 54 | return next( 55 | ( 56 | service for service in self.services 57 | if match_all(service.env, kwargs) 58 | ), 59 | None, 60 | ) 61 | 62 | def get_credential(self, key, default=None): 63 | return next( 64 | ( 65 | value for service in self.services 66 | for name, value in service.credentials.items() 67 | if key == name 68 | ), 69 | os.getenv(key, default), 70 | ) 71 | 72 | def __repr__(self): 73 | return ''.format(name=self.name) 74 | 75 | class Service(object): 76 | 77 | def __init__(self, env): 78 | self.env = env 79 | self.credentials = self.env.get('credentials', {}) 80 | 81 | @property 82 | def name(self): 83 | return self.env.get('name') 84 | 85 | def get_url(self, url='url', **keys): 86 | parsed = furl.furl(self.credentials.get(url, '')) 87 | for key, value in keys.items(): 88 | setattr(parsed, key, self.credentials.get(value)) 89 | return parsed.url 90 | 91 | def __repr__(self): 92 | return ''.format(name=self.name) 93 | 94 | def match_all(target, patterns): 95 | return all( 96 | match(target.get(key), pattern) 97 | for key, pattern in patterns.items() 98 | ) 99 | 100 | def match(target, pattern): 101 | if target is None: 102 | return False 103 | if isinstance(pattern, RegexType): 104 | return pattern.search(target) 105 | return pattern == target 106 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | invoke 2 | pytest 3 | flake8 4 | 5 | twine 6 | wheel 7 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | # This flag says that the code is written to work on both Python 2 and Python 3 | # 3. If at all possible, it is good practice to do this. If you cannot, you 4 | # will need to generate wheels for each Python version that you support. 5 | universal=1 6 | 7 | # E127: continuation line over-indented for visual indent 8 | # E128: continuation line under-indented for visual indent 9 | # E265: block comment should start with # 10 | # E301: expected 1 blank line, found 1 11 | # E302: expected 2 blank lines, found 0 12 | [flake8] 13 | ignore = E127,E128,E265,E301,E302,E305 14 | max-line-length = 90 15 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import re 4 | from setuptools import setup 5 | from setuptools import find_packages 6 | 7 | REQUIRES = [ 8 | 'furl>=0.4.8', 9 | ] 10 | 11 | def find_version(fname): 12 | """Attempts to find the version number in the file names fname. 13 | Raises RuntimeError if not found. 14 | """ 15 | version = '' 16 | with open(fname, 'r') as fp: 17 | reg = re.compile(r'__version__ = [\'"]([^\'"]*)[\'"]') 18 | for line in fp: 19 | m = reg.match(line) 20 | if m: 21 | version = m.group(1) 22 | break 23 | if not version: 24 | raise RuntimeError('Cannot find version information') 25 | return version 26 | 27 | def read(fname): 28 | with open(fname) as fp: 29 | content = fp.read() 30 | return content 31 | 32 | setup( 33 | name='cfenv', 34 | version=find_version('cfenv/__init__.py'), 35 | description='Python wrapper for Cloud Foundry environments', 36 | long_description=read('README.rst'), 37 | author='Joshua Carp', 38 | author_email='jm.carp@gmail.com', 39 | url='https://github.com/jmcarp/py-cfenv', 40 | packages=find_packages(exclude=('test*', )), 41 | package_dir={'cfenv': 'cfenv'}, 42 | include_package_data=True, 43 | install_requires=REQUIRES, 44 | license='MIT', 45 | zip_safe=False, 46 | keywords='cloud foundry', 47 | classifiers=[ 48 | 'Development Status :: 2 - Pre-Alpha', 49 | 'Intended Audience :: Developers', 50 | 'License :: OSI Approved :: MIT License', 51 | 'Natural Language :: English', 52 | 'Programming Language :: Python :: 2', 53 | 'Programming Language :: Python :: 2.7', 54 | 'Programming Language :: Python :: 3', 55 | 'Programming Language :: Python :: 3.3', 56 | 'Programming Language :: Python :: 3.4', 57 | 'Programming Language :: Python :: 3.5', 58 | ], 59 | test_suite='tests' 60 | ) 61 | -------------------------------------------------------------------------------- /tasks.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from invoke import task, run 4 | 5 | @task 6 | def clean(ctx): 7 | run('rm -rf dist') 8 | run('rm -rf build') 9 | run('rm -rf cfenv.egg-info') 10 | 11 | @task 12 | def publish(ctx, test=False): 13 | """Publish to the cheeseshop.""" 14 | clean(ctx) 15 | if test: 16 | run('python setup.py register -r test sdist bdist_wheel', echo=True) 17 | run('twine upload dist/* -r test', echo=True) 18 | else: 19 | run('python setup.py register sdist bdist_wheel', echo=True) 20 | run('twine upload dist/*', echo=True) 21 | -------------------------------------------------------------------------------- /tests/test_cfenv.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import re 4 | import json 5 | 6 | import pytest 7 | 8 | from cfenv import AppEnv 9 | 10 | @pytest.fixture 11 | def application(monkeypatch): 12 | app = { 13 | 'application_uris': [], 14 | 'name': 'test-app', 15 | 'space_name': 'dev', 16 | 'uris': [], 17 | } 18 | monkeypatch.setenv('VCAP_APPLICATION', json.dumps(app)) 19 | return app 20 | 21 | @pytest.fixture 22 | def services(monkeypatch): 23 | services = { 24 | 'test-credentials': [ 25 | { 26 | 'name': 'test-credentials', 27 | 'label': 'user-provided', 28 | 'plan': 'free', 29 | 'credentials': { 30 | 'url': 'https://test-service.com/', 31 | 'username': 'user', 32 | 'password': 'pass', 33 | }, 34 | } 35 | ], 36 | 'test-database': [ 37 | { 38 | 'name': 'test-database', 39 | 'label': 'webscaledb', 40 | 'plan': 'free', 41 | 'credentials': { 42 | 'url': 'https://test-service.com/', 43 | 'username': 'user', 44 | 'password': 'pass', 45 | }, 46 | } 47 | ], 48 | } 49 | monkeypatch.setenv('VCAP_SERVICES', json.dumps(services)) 50 | return services 51 | 52 | @pytest.fixture 53 | def env(application, services): 54 | return AppEnv() 55 | 56 | class TestEnv: 57 | 58 | def test_name(self, env): 59 | assert env.name == 'test-app' 60 | 61 | @pytest.mark.parametrize(['raw', 'parsed'], [ 62 | ('0', 0), 63 | ('1', 1), 64 | ('', None), 65 | ]) 66 | def test_index(self, raw, parsed, env, monkeypatch): 67 | monkeypatch.setenv('CF_INSTANCE_INDEX', raw) 68 | assert env.index == parsed 69 | 70 | @pytest.mark.parametrize(['key', 'raw', 'parsed'], [ 71 | ('PORT', '3000', 3000), 72 | ('CF_INSTANCE_PORT', '4000', 4000), 73 | ('VCAP_APP_PORT', '5000', 5000), 74 | ('VCAP_APP_PORT', '', None), 75 | ]) 76 | def test_port(self, key, raw, parsed, env, monkeypatch): 77 | monkeypatch.setenv(key, raw) 78 | assert env.port == parsed 79 | 80 | def test_get_credential(self, env): 81 | assert env.get_credential('password') == 'pass' 82 | 83 | def test_get_credential_default(self, env): 84 | assert env.get_credential('missing') is None 85 | assert env.get_credential('missing', 'default') == 'default' 86 | 87 | class TestService: 88 | 89 | def test_name(self, env): 90 | service = env.get_service(name='test-credentials') 91 | assert service.name == 'test-credentials' 92 | 93 | def test_regex(self, env): 94 | service = env.get_service(label=re.compile(r'^webscale')) 95 | assert service.name == 'test-database' 96 | 97 | def test_get_url(self, env): 98 | service = env.get_service(label='user-provided') 99 | url = service.get_url(url='url', username='username', password='password') 100 | assert url == 'https://user:pass@test-service.com/' 101 | --------------------------------------------------------------------------------