├── .gitignore ├── LICENSE.md ├── README.md ├── django_example ├── README.md ├── consul_example │ ├── set_env.sh │ └── web00.django.test.json ├── django_example │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── env │ ├── __init__.py │ ├── admin.py │ ├── migrations │ │ └── __init__.py │ ├── models.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── manage.py └── requirements.txt ├── envconsul ├── __init__.py ├── consts.py ├── envconsul.py ├── errors.py └── utils.py ├── pytest.ini ├── requirements-dev.txt ├── requirements.txt ├── setup.cfg ├── setup.py └── tests └── test_utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | # C extensions 5 | *.so 6 | # Distribution / packaging 7 | .Python 8 | env/ 9 | build/ 10 | develop-eggs/ 11 | dist/ 12 | downloads/ 13 | eggs/ 14 | .eggs/ 15 | lib/ 16 | lib64/ 17 | parts/ 18 | sdist/ 19 | var/ 20 | *.egg-info/ 21 | .installed.cfg 22 | *.egg 23 | # PyInstaller 24 | # Usually these files are written by a python script from a template 25 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 26 | *.manifest 27 | *.spec 28 | # Installer logs 29 | pip-log.txt 30 | pip-delete-this-directory.txt 31 | # Unit test / coverage reports 32 | htmlcov/ 33 | .tox/ 34 | .coverage 35 | .coverage.* 36 | .cache 37 | nosetests.xml 38 | coverage.xml 39 | # Translations 40 | *.mo 41 | *.pot 42 | # Django stuff: 43 | *.log 44 | # Sphinx documentation 45 | docs/_build/ 46 | # PyBuilder 47 | target/ 48 | 49 | 50 | *.sqlite3 51 | *.pyc 52 | *.egg* 53 | 54 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | [OSI Approved License] 3 | 4 | The MIT License (MIT) 5 | 6 | Copyright (c) 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in 16 | all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EnvConsul 2 | 3 | Python environment variable wrapper around Consul key/value storage. When instantiated, `EnvConsol` fetches the key value data for a defined service from Consul. These environment variables are retrievable via the path they were stored in Consul. 4 | 5 | For example if we insert a database name for specific services in Consul 6 | 7 | ```shell 8 | curl -X PUT -d 'db.sqlite3' \ 9 | http://localhost:8500/v1/kv/web00.django.test/databases/default/name 10 | ``` 11 | EnvConsul can fetch and be queried like so 12 | 13 | ```python 14 | import envconsul 15 | ENV_CONSUL = envconsul.EnvConsul('web00.django.test') 16 | DATABASE_NAME = ENV_CONSUL.get_str('/databases/default/name') 17 | ``` 18 | 19 | ## Features 20 | 21 | - Retrieve Consul key/value environment variables service 22 | - Typed retrieval of environment variables 23 | - EnvConsol object implements a simple ReadOnly/Immutable dict 24 | - Implemented using supported Python's Consul `consulate` 25 | - https://github.com/gmr/consulate 26 | 27 | 28 | ## Install 29 | 30 | ```shell 31 | pip install envconsul 32 | ``` 33 | 34 | ## Example Usage 35 | 36 | ### Initialization 37 | 38 | ```python 39 | import envconsul 40 | # where service name is 'web00.django.test' 41 | ENV_CONSUL = envconsul.EnvConsul('web00.django.test') 42 | 43 | # Or if using remote Consul instance 44 | ENV_CONSUL = envconsul.EnvConsul( 45 | service_name='web00.django.test', 46 | host="localhost", 47 | port=8500, 48 | ) 49 | ``` 50 | 51 | ### Retrieval of Environment Variables 52 | 53 | #### Example Django settings file 54 | [Full example here Django settings file here](https://github.com/cevaris/python-envconsul/blob/master/django_example/django_example/settings.py) 55 | 56 | 57 | ##### Bool type 58 | 59 | ```shell 60 | curl -X PUT -d 'True' http://localhost:8500/v1/kv/web00.django.test/debug 61 | ``` 62 | 63 | ```python 64 | DEBUG = ENV_CONSUL.get_bool('/debug', True) 65 | ``` 66 | 67 | 68 | #### Return list type 69 | ```shell 70 | curl -X PUT -d "example.com, example.com." http://localhost:8500/v1/kv/web00.django.test/allowed_hosts 71 | ``` 72 | 73 | ```python 74 | ALLOWED_HOSTS = [] 75 | ALLOWED_HOSTS += ENV_CONSUL.get_list('/allowed_hosts', []) 76 | ``` 77 | 78 | 79 | #### Return tuple type 80 | 81 | ```shell 82 | curl -X PUT -d 'django.contrib.admin, env' http://localhost:8500/v1/kv/web00.django.test/installed_apps 83 | ``` 84 | 85 | ```python 86 | INSTALLED_APPS = ( 87 | 'django.contrib.auth', 88 | 'django.contrib.contenttypes', 89 | 'django.contrib.sessions', 90 | 'django.contrib.messages', 91 | 'django.contrib.staticfiles', 92 | ) 93 | INSTALLED_APPS += ENV_CONSUL.get_tuple('/installed_apps', ['django.contrib.admin',]) 94 | ``` 95 | 96 | #### Nested path example 97 | 98 | ```shell 99 | curl -X PUT -d 'django.db.backends.sqlite3' http://localhost:8500/v1/kv/web00.django.test/databases/default/engine 100 | curl -X PUT -d 'db.sqlite3' http://localhost:8500/v1/kv/web00.django.test/databases/default/name 101 | ``` 102 | 103 | ```python 104 | DATABASES = { 105 | 'default': { 106 | 'ENGINE': ENV_CONSUL.get_str('/databases/default/engine'), 107 | 'NAME': os.path.join(BASE_DIR, ENV_CONSUL.get_str('/databases/default/name', 'db.sqlite3')), 108 | } 109 | } 110 | ``` 111 | 112 | #### Simple no-default, no type retrieval; not recommend 113 | 114 | ```python 115 | REDIS_HOST = ENV_CONSUL.get('/redis_host') 116 | ``` 117 | 118 | [Full example here Django settings file here](https://github.com/cevaris/python-envconsul/blob/master/django_example/django_example/settings.py) 119 | 120 | 121 | 122 | ## For Development 123 | 124 | Git clone project 125 | 126 | `git clone https://github.com/cevaris/python-envconsul.git` 127 | 128 | Build and install project 129 | 130 | `pip install -r requirements-dev.txt` 131 | 132 | Run tests 133 | 134 | `py.test tests/` 135 | -------------------------------------------------------------------------------- /django_example/README.md: -------------------------------------------------------------------------------- 1 | # Django Example using python-envconsul 2 | 3 | Django projects can now reference Consul's key/value store in setting setting.py variables. 4 | 5 | ## Getting started 6 | 7 | 8 | Need to install Consul. Installation guide found here [http://www.consul.io/intro/getting-started/install.html](http://www.consul.io/intro/getting-started/install.html) 9 | 10 | ``` 11 | # Mainly, install consul (Mac OS X) via brew 12 | brew install caskroom/cask/brew-cask 13 | brew cask install consul 14 | ``` 15 | 16 | Create a `/etc/consul.d` directory 17 | 18 | ``` 19 | sudo mkdir /etc/consul.d 20 | ``` 21 | 22 | Copy `django_example/consul_example/web00.django.test.json` to `/etc/consul.d/web00.django.test.json` 23 | 24 | ``` 25 | cat /etc/consul.d/web00.django.test.json 26 | { 27 | "service":{ 28 | "name": "web00.django.test", 29 | "tags": ["django"], 30 | "port": 8000 31 | } 32 | } 33 | ``` 34 | 35 | Start up consul server. Here is the guide for starting consul [http://www.consul.io/intro/getting-started/agent.html](http://www.consul.io/intro/getting-started/agent.html) 36 | 37 | ``` 38 | consul agent -server -bootstrap-expect 1 -data-dir /tmp/consul -config-dir /etc/consul.d 39 | ``` 40 | 41 | Execute `django_example/consul_example/setup_env.sh` to load example settings.py variables into Consul 42 | 43 | ``` 44 | ./django_example/consul_example/setup_env.sh 45 | ``` 46 | 47 | Setup django environment and run server 48 | 49 | ``` 50 | cd django_example 51 | pip install -r requirements.txt 52 | ./manage.py runserver 53 | 54 | ``` 55 | 56 | Now navigate to [http://127.0.0.1:8000/env/](http://127.0.0.1:8000/env/) in your web browser or use this `curl` command to view Consul backed environment variables 57 | 58 | ``` 59 | echo $(curl http://127.0.0.1:8000/env/ | sed 's//\\n/g') 60 | ``` 61 | -------------------------------------------------------------------------------- /django_example/consul_example/set_env.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Set environment variables for django_example app 4 | # Demostation only, should use other tool for managing Consul key/value store 5 | 6 | CONSUL_HOST=http://localhost:8500 7 | curl -X PUT -d 'True' $CONSUL_HOST/v1/kv/web00.django.test/debug 8 | curl -X PUT -d 'django.db.backends.sqlite3' $CONSUL_HOST/v1/kv/web00.django.test/databases/default/engine 9 | curl -X PUT -d 'db.envconsul.sqlite3' $CONSUL_HOST/v1/kv/web00.django.test/databases/default/name 10 | curl -X PUT -d 'django.contrib.admin, env' $CONSUL_HOST/v1/kv/web00.django.test/installed_apps 11 | curl -X PUT -d '.example.com, .example.com.' $CONSUL_HOST/v1/kv/web00.django.test/allowed_hosts 12 | echo -e "\nDone setting variables" 13 | -------------------------------------------------------------------------------- /django_example/consul_example/web00.django.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "service":{ 3 | "name": "web00.django.test", 4 | "tags": ["django"], 5 | "port": 8000 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /django_example/django_example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cevaris/python-envconsul/aeb490a5f47e2c2fd8a467a9d13eb9917cc4cadb/django_example/django_example/__init__.py -------------------------------------------------------------------------------- /django_example/django_example/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for django_example project. 3 | 4 | For more information on this file, see 5 | https://docs.djangoproject.com/en/1.7/topics/settings/ 6 | 7 | For the full list of settings and their values, see 8 | https://docs.djangoproject.com/en/1.7/ref/settings/ 9 | """ 10 | import envconsul 11 | ENV_CONSUL = envconsul.EnvConsul( 12 | service_name='web00.django.test', 13 | host="localhost", 14 | port=8500, 15 | ) 16 | 17 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 18 | import os 19 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 20 | 21 | # Quick-start development settings - unsuitable for production 22 | # See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ 23 | 24 | # SECURITY WARNING: keep the secret key used in production secret! 25 | SECRET_KEY = 'n5eah^tt=+tq9^0nym*6zm&*xml)u9(p8r^!5ohl^pnbh$m$!s' 26 | 27 | # SECURITY WARNING: don't run with debug turned on in production! 28 | DEBUG = ENV_CONSUL.get_bool('/debug', True) 29 | 30 | TEMPLATE_DEBUG = True 31 | 32 | ALLOWED_HOSTS = [] 33 | ALLOWED_HOSTS += ENV_CONSUL.get_list('/allowed_hosts', []) 34 | 35 | 36 | # Application definition 37 | 38 | INSTALLED_APPS = ( 39 | 'django.contrib.auth', 40 | 'django.contrib.contenttypes', 41 | 'django.contrib.sessions', 42 | 'django.contrib.messages', 43 | 'django.contrib.staticfiles', 44 | ) 45 | INSTALLED_APPS += ENV_CONSUL.get_tuple('/installed_apps', ['django.contrib.admin',]) 46 | 47 | 48 | MIDDLEWARE_CLASSES = ( 49 | 'django.contrib.sessions.middleware.SessionMiddleware', 50 | 'django.middleware.common.CommonMiddleware', 51 | 'django.middleware.csrf.CsrfViewMiddleware', 52 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 53 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 54 | 'django.contrib.messages.middleware.MessageMiddleware', 55 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 56 | ) 57 | 58 | ROOT_URLCONF = 'django_example.urls' 59 | 60 | WSGI_APPLICATION = 'django_example.wsgi.application' 61 | 62 | 63 | # Database 64 | # https://docs.djangoproject.com/en/1.7/ref/settings/#databases 65 | 66 | DATABASES = { 67 | 'default': { 68 | 'ENGINE': ENV_CONSUL.get_str('/databases/default/engine', 'django.db.backends.sqlite3'), 69 | 'NAME': os.path.join(BASE_DIR, ENV_CONSUL.get_str('/databases/default/name', 'db.sqlite3')), 70 | } 71 | } 72 | 73 | # Internationalization 74 | # https://docs.djangoproject.com/en/1.7/topics/i18n/ 75 | 76 | LANGUAGE_CODE = 'en-us' 77 | 78 | TIME_ZONE = 'UTC' 79 | 80 | USE_I18N = True 81 | 82 | USE_L10N = True 83 | 84 | USE_TZ = True 85 | 86 | 87 | # Static files (CSS, JavaScript, Images) 88 | # https://docs.djangoproject.com/en/1.7/howto/static-files/ 89 | 90 | STATIC_URL = '/static/' 91 | 92 | 93 | REDIS_HOST = ENV_CONSUL.get('/redis_host') 94 | -------------------------------------------------------------------------------- /django_example/django_example/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, include, url 2 | from django.contrib import admin 3 | 4 | from env import views 5 | 6 | urlpatterns = patterns('', 7 | # Examples: 8 | # url(r'^$', 'django_example.views.home', name='home'), 9 | # url(r'^blog/', include('blog.urls')), 10 | 11 | url(r'^admin/', include(admin.site.urls)), 12 | url(r'^env/', include('env.urls')), 13 | ) 14 | -------------------------------------------------------------------------------- /django_example/django_example/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for django_example project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_example.settings") 12 | 13 | from django.core.wsgi import get_wsgi_application 14 | application = get_wsgi_application() 15 | -------------------------------------------------------------------------------- /django_example/env/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cevaris/python-envconsul/aeb490a5f47e2c2fd8a467a9d13eb9917cc4cadb/django_example/env/__init__.py -------------------------------------------------------------------------------- /django_example/env/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /django_example/env/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cevaris/python-envconsul/aeb490a5f47e2c2fd8a467a9d13eb9917cc4cadb/django_example/env/migrations/__init__.py -------------------------------------------------------------------------------- /django_example/env/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /django_example/env/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /django_example/env/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, url 2 | 3 | from env import views 4 | 5 | urlpatterns = patterns('', 6 | url(r'^$', views.index, name='index'), 7 | ) 8 | -------------------------------------------------------------------------------- /django_example/env/views.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.http import HttpResponse 3 | from django.shortcuts import render 4 | 5 | 6 | def index(request): 7 | resposne = [] 8 | for name in dir(settings): 9 | resposne.append("{}: {}
".format(name, getattr(settings, name))) 10 | return HttpResponse(resposne) 11 | -------------------------------------------------------------------------------- /django_example/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_example.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /django_example/requirements.txt: -------------------------------------------------------------------------------- 1 | Django==1.7.2 2 | -e ../ 3 | -------------------------------------------------------------------------------- /envconsul/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '1.0.0' 2 | 3 | from .envconsul import EnvConsul 4 | 5 | __all__ = [ 6 | '__version__', 7 | 'EnvConsul' 8 | ] 9 | -------------------------------------------------------------------------------- /envconsul/consts.py: -------------------------------------------------------------------------------- 1 | 2 | LIST_DELIM = ',' 3 | DEFAULT_HOST = 'localhost' 4 | DEFAULT_PORT = 8500 5 | -------------------------------------------------------------------------------- /envconsul/envconsul.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | from consulate import Consulate 4 | import requests 5 | 6 | from . import errors 7 | from . import utils 8 | from .consts import ( 9 | DEFAULT_HOST, 10 | DEFAULT_PORT, 11 | LIST_DELIM, 12 | ) 13 | 14 | 15 | class EnvConsul(collections.Mapping): 16 | """Collect Consul key/value data into a dict""" 17 | 18 | def __init__(self, service_name=None, host=DEFAULT_HOST, port=DEFAULT_PORT, *args, **kwargs): 19 | self._d = dict(*args, **kwargs) 20 | 21 | self.consul = False 22 | try: 23 | self.consul = Consulate(host, port) 24 | self.consul.kv.items() # Force connection, to test host/port 25 | except Exception as e: 26 | raise errors.ConsulConnectionFailed(e) 27 | 28 | if service_name and self.consul: 29 | self._d.update(utils.get_settings_for(self.consul, service_name)) 30 | 31 | def __iter__(self): 32 | return iter(self._d) 33 | 34 | def __len__(self): 35 | return len(self._d) 36 | 37 | def __getitem__(self, key): 38 | return self._d[key] 39 | 40 | def get_bool(self, key, default=None): 41 | return bool(self._d.get(key, default)) 42 | 43 | def get_str(self, key, default=None): 44 | return str(self._d.get(key, default)) 45 | 46 | def get_tuple(self, key, default=None): 47 | default = default or tuple() 48 | return tuple(self.get_list(key, default)) 49 | 50 | def get_list(self, key, default=None): 51 | default = default or [] 52 | data = self._d.get(key, default) 53 | if data and LIST_DELIM in data: 54 | data = data.split(LIST_DELIM) 55 | data = [str(d.strip()) for d in data] 56 | return data 57 | -------------------------------------------------------------------------------- /envconsul/errors.py: -------------------------------------------------------------------------------- 1 | 2 | class ConsulConnectionFailed(Exception): 3 | pass 4 | -------------------------------------------------------------------------------- /envconsul/utils.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | def strip_key_header(text_to_strip, dictionary): 4 | dict_copy = copy.copy(dictionary) 5 | for k, v in dictionary.items(): 6 | if k.startswith(text_to_strip): 7 | new_key = k.replace(text_to_strip, '', 1) 8 | dict_copy[new_key] = v 9 | dict_copy.pop(k, None) 10 | return dict_copy 11 | 12 | 13 | def get_settings_for(consul, service_name): 14 | return strip_key_header( 15 | service_name, 16 | consul.kv.find(service_name), 17 | ) 18 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | python_paths= . 3 | looponfailroots= . 4 | addopts= --tb native 5 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | ipython==3.0.0 2 | pytest==2.6.4 3 | wheel==0.24.0 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | consulate==0.2.0 2 | tornado==4.1 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import envconsul 2 | version = envconsul.__version__ 3 | readme = open('README.md').read() 4 | 5 | try: 6 | from setuptools import setup 7 | except ImportError: 8 | from distutils.core import setup 9 | 10 | setup( 11 | name='envconsul', 12 | version=version, 13 | description="""Environment Variable and Settings Management via Consul""", 14 | long_description="""TODO""", 15 | author='Adam Cardenas', 16 | author_email='cevaris@gmail.com', 17 | url='https://github.com/cevaris/python-envconsul', 18 | packages=['envconsul',], 19 | include_package_data=True, 20 | install_requires=[ 21 | 'consulate==0.2.0', 22 | 'tornado==4.1', 23 | ], 24 | license="MIT", 25 | zip_safe=False, 26 | keywords='envconsul', 27 | classifiers=[ 28 | 'Development Status :: 2 - Pre-Alpha', 29 | 'Intended Audience :: Developers', 30 | 'License :: OSI Approved :: BSD License', 31 | 'Natural Language :: English', 32 | 'Programming Language :: Python :: 2.7', 33 | ], 34 | 35 | ) 36 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from envconsul import utils 4 | 5 | 6 | def test_strip_key_header(): 7 | sample = {'abcda': 1, 'abcd': 2, 'acde': 3} 8 | expected = {'cda': 1, 'cd': 2, 'acde': 3} 9 | actual = utils.strip_key_header('ab', sample) 10 | assert actual == expected 11 | --------------------------------------------------------------------------------