{ "info": { "author": "Ben Windsor", "author_email": "", "bugtrack_url": null, "classifiers": [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Topic :: Software Development", "Typing :: Typed" ], "description": "[![Build Status](https://travis-ci.org/bwindsor/typed-config.svg?branch=master)](https://travis-ci.org/bwindsor/typed-config)\n[![codecov](https://codecov.io/gh/bwindsor/typed-config/branch/master/graph/badge.svg)](https://codecov.io/gh/bwindsor/typed-config)\n\n# typed-config\nTyped, extensible, dependency free configuration reader for Python projects for multiple config sources and working well in IDEs for great autocomplete performance.\n\n`pip install typed-config`\n\nRequires python 3.6 or above.\n\n## Basic usage\n```python\n# my_app/config.py\nfrom typedconfig import Config, key, section\nfrom typedconfig.source import EnvironmentConfigSource\n\n@section('database')\nclass AppConfig(Config):\n host = key(cast=str)\n port = key(cast=int)\n timeout = key(cast=float)\n\nconfig = AppConfig()\nconfig.add_source(EnvironmentConfigSource())\nconfig.read()\n```\n\n```python\n# my_app/main.py\nfrom my_app.config import config\nprint(config.host)\n```\nIn PyCharm, and hopefully other IDEs, it will recognise the datatypes of your configuration and allow you to autocomplete. No more remembering strings to get the right thing out!\n\n## How it works\nConfiguration is always supplied in a two level structure, so your source configuration can have multiple sections, and each section contains multiple key/value configuration pairs. For example:\n```ini\n[database]\nhost = 127.0.0.1\nport = 2000\n\n[algorithm]\nmax_value = 10\nmin_value = 20\n```\n\nYou then create your configuration hierarchy in code (this can be flat or many levels deep) and supply the matching between strings in your config sources and properties of your configuration classes.\n\nYou provide one or more `ConfigSource`s, from which the config for your application can be read. For example, you might supply an `EnvironmentConfigSource`, and two `IniFileConfigSource`s. This would make your application first look for a configuration value in environment variables, if not found there it would then look at the first INI file (perhaps a user-specific file), before falling back to the second INI file (perhaps a default configuration shared between all users). If a parameter is still not found and is a required parameter, an error would be thrown.\n\nThere is emphasis on type information being available for everything so that an IDE will autocomplete when trying to use your config across your application.\n\n### Multiple data sources\n```python\nfrom typedconfig import Config, key, section, group_key\nfrom typedconfig.source import EnvironmentConfigSource, IniFileConfigSource\n\n@section('database')\nclass DatabaseConfig(Config):\n host = key(cast=str)\n port = key(cast=int)\n username = key(cast=str)\n password = key(cast=str)\n\nconfig = DatabaseConfig()\nconfig.add_source(EnvironmentConfigSource(prefix=\"EXAMPLE\"))\nconfig.add_source(IniFileConfigSource(\"config.cfg\"))\n\n# OR provide sources directly to the constructor\nconfig = DatabaseConfig(sources=[\n EnvironmentConfigSource(prefix=\"EXAMPLE\"),\n IniFileConfigSource(\"config.cfg\")\n])\n```\n\nSince you don't want to hard code your secret credentials, you might supply them through the environment.\nSo for the above configuration, the environment might look like this:\n```bash\nexport EXAMPLE_DATABASE_USERNAME=my_username\nexport EXAMPLE_DATABASE_PASSWORD=my_very_secret_password\nexport EXAMPLE_DATABASE_PORT=2001\n```\n\nThose values which couldn't be found in the environment would then be read from the INI file, which might look like this:\n```ini\n[database]\nHOST = db1.mydomain.com\nPORT = 2000\n```\n\nNote after this, `config.port` will be equal to `2001` as the value in the environment took priority over the value in the INI file.\n\n### Caching\nWhen config values are first used, they are read. This is lazy evaluation by default so that not everything is read if not necessary.\n\nAfter first use, they are cached in memory so that there should be no further I/O if the config value is used again.\n\nFor fail fast behaviour, and also to stop unexpected latency when a config value is read partway through your application (e.g. your config could be coming across a network), the option is available to read all config values at the start. Just call\n\n`config.read()`\n\nThis will throw an exception if any required config value cannot be found, and will also keep all read config values in memory for next time they are used. If you do not use `read` you will only get the exception when you first try to use the offending config key.\n\n### Hierarchical configuration\nUse `group_key` to represent a \"sub-config\" of a configuration. Set up \"sub-configs\" exactly as demonstrated above, and then create a parent config to compose them in one place.\n```python\nfrom typedconfig import Config, key, section, group_key\nfrom typedconfig.source import EnvironmentConfigSource, IniFileConfigSource\n\n@section('database')\nclass DatabaseConfig(Config):\n host = key(cast=str)\n port = key(cast=int)\n\n@section('algorithm')\nclass AlgorithmConfig(Config):\n max_value = key(cast=float)\n min_value = key(cast=float)\n\nclass ParentConfig(Config):\n database = group_key(DatabaseConfig)\n algorithm = group_key(AlgorithmConfig)\n description = key(cast=str, section_name=\"general\")\n\nconfig = ParentConfig()\nconfig.add_source(EnvironmentConfigSource(prefix=\"EXAMPLE\"))\nconfig.add_source(IniFileConfigSource(\"config.cfg\"))\nconfig.read()\n```\n\nThe first time the `config.database` or `config.algorithm` is accessed (which in the case above is when `read()` is called), then an instance will be instantiated. Notice that it is the class definition, not an instance of the class, which is passed to the `group_key` function.\n\n### Custom section/key names, optional parameters, default values\nLet's take a look at this:\n```python\nfrom typedconfig import Config, key, section\n\n@section('database')\nclass AppConfig(Config):\n host1 = key()\n host2 = key(section_name='database', key_name='HOST2',\n required=True, cast=str, default=None)\n```\nBoth `host1` and `host2` are legitimate configuration key definitions.\n\n* `section_name` - this name of the section in the configuration source from which this parameter should be read. This can be provided on a key-by-key basis, but if it is left out then the section name supplied by the `@section` decorator is used. If all keys supply a `section_name`, the class decorator is not needed. If both `section_name` and a decorator are provided, the `section_name` argument takes priority.\n* `key_name` - the name of this key in the configuration source from which this parameter is read. If not supplied, some magic uses the object property name as the key name.\n* `required` - default True. If False, and the configuration value can't be found, no error will be thrown and the default value will be used, if provided. If a default not provided, `None` will be used.\n* `cast` - probably the most important option for typing. **If you want autocomplete typing support you must specify this**. It's just a function which takes a string as an input and returns a parsed value. See the casting section for more. If not supplied, the value remains as a string.\n* `default` - only applicable if `required` is false. When `required` is false this value is used if a value cannot be found.\n\n### Types\n```python\nfrom typedconfig import Config, key, section\nfrom typing import List\n\ndef split_str(s: str) -> List[str]:\n return [x.strip() for x in s.split(\",\")]\n\n@section('database')\nclass AppConfig(Config):\n host = key()\n port = key(cast=int)\n users = key(cast=split_str)\n zero_based_index = key(cast=lambda x: int(x)-1)\nconfig = AppConfig(sources=[...])\n```\nIn this example we have three ways of casting:\n1. Not casting at all. This default to returning a `str`, but your IDE won't know that so if you want type hints use `cast=str`\n2. Casting to an built in type which can take a string input and parse it, for example `int`\n3. Defining a custom function. Your function should take one string input and return one output of any type. To get type hint, just make sure your function has type annotations.\n4. Using a lambda expression. The type inference may or may not work depending on your expression, so if it doesn't just write it as a function with type annotations.\n\n## Configuration Sources\nConfiguration sources are how your main `Config` class knows where to get its data from. These are totally extensible so that you can read in your configuration from wherever you like - from a database, from S3, anywhere that you can write code for.\n\nYou supply your configuration source to your config after you've instantiated it, but **before** you try to read any data from it:\n```python\nconfig = AppConfig()\nconfig.add_source(my_first_source)\nconfig.add_source(my_second_source)\nconfig.read()\n```\nOr you can supply the sources directly in the constructor like this:\n```python\nconfig = AppConfig(sources=[my_first_source, my_second_source])\nconfig.read()\n```\n\nThe below is bad practice, but if for some reason you do add further config sources after it's been read, or need to refresh the config for some reason, you'll need to clear any cached values in order to force re-reading of the config. You can do this by\n```python\nconfig.clear_cache()\nconfig.read() # Read all configuration values again\n```\n\n### Supplied Config Sources\n#### `EnvironmentConfigSource`\nThis just reads configuration from environment variables.\n```python\nfrom typedconfig.source import EnvironmentConfigSource\nsource = EnvironmentConfigSource(prefix=\"XYZ\")\n# OR just\nsource = EnvironmentConfigSource()\n```\nIt just takes one optional input argument, a prefix. This can be useful to avoid name clashes in environment variables.\n\n* If prefix is provided, environment variables are expected to look like `{PREFIX}_{SECTION}_{KEY}`, for example `export XYZ_DATABASE_PORT=2000`. \n* If no prefix is provided, environment variables should look like `{SECTION}_{KEY}`, for example `export DATABASE_PORT=2000`.\n\n#### `IniFileConfigSource`\nThis reads from an INI file using Python's built in [configparser](https://docs.python.org/3/library/configparser.html). Read the docs for `configparser` for more about the structure of the file.\n```python\nfrom typedconfig.source import IniFileConfigSource\nsource = IniFileConfigSource(\"config.cfg\", encoding='utf-8', must_exist=True)\n```\n\n* The first argument is the filename (absolute or relative to the current working directory).\n* `encoding` is the text encoding of the file. `configparser`'s default is used if not supplied.\n* `must_exist` - default `True`. If the file can't be found, an error will be thrown by default. Setting `must_exist` to be `False` allows the file not to be present, in which case this source will just report that it can't find any configuration values and your `Config` class will move onto looking in the next `ConfigSource`.\n\n#### `IniStringConfigSource`\nThis reads from a string instead of a file\n```python\nfrom typedconfig.source import IniStringConfigSource\nsource = IniStringConfigSource(\"\"\"\n[section_name]\nkey_name=key_value\n\"\"\")\n```\n\n#### `DictConfigSource`\nThe most basic source, entirely in memory, and also useful when writing tests. It is case insensitive.\n```python\nfrom typedconfig.source import DictConfigSource\nsource = DictConfigSource({\n 'database': dict(HOST='db1', PORT='2000'),\n 'algorithm': dict(MAX_VALUE='20', MIN_VALUE='10')\n})\n```\n\nIt expects data type `Dict[str, Dict[str, str]]`, i.e. such that `string_value = d['section_name']['key_name']`. Everything should be provided as string data so that it can be parsed in the same way as if data was coming from a file or elsewhere.\n\nThis is an alternative way of supplying default values instead of using the `default` option when defining your `key`s. Just provide a `DictConfigSource` as the lowest priority source, containing your defaults.\n\n### Writing your own `ConfigSource`s\nAn abstract base class `ConfigSource` is supplied. You should extend it and implement the method `get_config_value` as demonstrated below, which takes a section name and key name, and returns either a `str` config value, or `None` if the value could not be found. It should not error if the value cannot be found, `Config` will throw an error later if it still can't find the value in any of its other available sources. To make it easier for the user try to make your source case insensitive.\n\nHere's an outline of how you might implement a source to read your config from a JSON file, for example. Use the `__init__` method to provide any information your source needs to fetch the data, such as filename, api details, etc. You can do sanity checks in the `__init__` method and throw an error if something is wrong.\n```python\nimport json\nfrom typing import Optional\nfrom typedconfig.source import ConfigSource\n\nclass JsonConfigSource(ConfigSource):\n def __init__(self, filename: str):\n # Read data - will raise an exception if problem with file\n with open(filename, 'r') as f:\n self.data = json.load(f)\n # Quick checks on data format\n assert type(self.data) is dict\n for k, v in self.data.items():\n assert type(k) is str\n assert type(v) is dict\n for v_k, v_v in v.items():\n assert type(v_k) is str\n assert type(v_v) is str\n # Convert all keys to lowercase\n self.data = {\n k.lower(): {\n v_k.lower(): v_v\n for v_k, v_v in v.items()\n }\n for k, v in self.data.items()\n } \n\n def get_config_value(self, section_name: str, key_name: str) -> Optional[str]:\n # Extract info from data which we read in during __init__\n section = self.data.get(section_name.lower(), None)\n if section is None:\n return None\n return section.get(key_name.lower(), None)\n```\n\n### Additional config sources\nIn order to keep `typed-config` dependency free, `ConfigSources` requiring additional dependencies are in separate packages, which also have `typed-config` as a dependency.\n\nThese are listed here:\n\n| pip install name | import name | Description |\n| --- | --- | --- |\n| [typed-config-aws-sources](https://pypi.org/project/typed-config-aws-sources) | `typedconfig_awssource` | Config sources using `boto3` to get config e.g. from S3 or DynamoDB\n\n## Contributing\nIdeas for new features and pull requests are welcome. PRs must come with tests included. This was developed using Python 3.7 but Travis tests run with v3.6 too.\n\n### Development setup\n1. Clone the git repository\n2. Create a virtual environment `virtualenv venv`\n3. Activate the environment `venv/scripts/activate`\n4. Install development dependencies `pip install -r requirements.txt`\n\n### Running tests\n`pytest`\n\nTo run with coverage:\n\n`pytest --cov`\n\n### Deploying to PyPI\nYou'll need to `pip install twine` if you don't have it.\n\n1. Bump version number in `typedconfig/__version__.py`\n2. Clear the dist directory `rm -r dist`\n3. `python setup.py sdist bdist_wheel`\n4. `twine check dist/*`\n5. Upload to the test PyPI `twine upload --repository-url https://test.pypi.org/legacy/ dist/*`\n6. Check all looks ok at [https://test.pypi.org/project/typed-config](https://test.pypi.org/project/typed-config)\n7. Upload to live PyPI `twine upload dist/*`\n\nHere is [a good tutorial](https://realpython.com/pypi-publish-python-package) on publishing packages to PyPI.\n\n\n", "description_content_type": "text/markdown", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/bwindsor/typed-config", "keywords": "", "license": "MIT", "maintainer": "", "maintainer_email": "", "name": "typed-config", "package_url": "https://pypi.org/project/typed-config/", "platform": "", "project_url": "https://pypi.org/project/typed-config/", "project_urls": { "Homepage": "https://github.com/bwindsor/typed-config" }, "release_url": "https://pypi.org/project/typed-config/0.1.1/", "requires_dist": null, "requires_python": ">=3.6.0", "summary": "Typed, extensible, dependency free configuration reader for Python projects for multiple config sources and working well in IDEs for great autocomplete performance.", "version": "0.1.1" }, "last_serial": 5246957, "releases": { "0.1.0": [ { "comment_text": "", "digests": { "md5": "b021510b121541dc18ade41159a764eb", "sha256": "ad0f1800da5d6968ea0fb5e5caecf64d91860e80aa74aa045099a6228f847dbd" }, "downloads": -1, "filename": "typed_config-0.1.0-py3-none-any.whl", "has_sig": false, "md5_digest": "b021510b121541dc18ade41159a764eb", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6.0", "size": 11856, "upload_time": "2019-03-20T17:56:45", "url": "https://files.pythonhosted.org/packages/e3/9e/b05414ce55aa948b425850cb343abacc5c669515757d435aa720d5b19e8c/typed_config-0.1.0-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "5ce536175e2df8121fdcb42c6575944a", "sha256": "c7460ed1c5a83e7504817c65453624bb5ed366971fdd42beca441e58643baab9" }, "downloads": -1, "filename": "typed-config-0.1.0.tar.gz", "has_sig": false, "md5_digest": "5ce536175e2df8121fdcb42c6575944a", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6.0", "size": 17519, "upload_time": "2019-03-20T17:56:47", "url": "https://files.pythonhosted.org/packages/e3/7b/c2054746a06d41c186361a76d801ed5421ca5c79e775aa9996e458977145/typed-config-0.1.0.tar.gz" } ], "0.1.1": [ { "comment_text": "", "digests": { "md5": "210cff15abc245acb66454a4fbc3cd6d", "sha256": "a7beb7243e2e3f41e732583c94a37b82c650f21209384448347e18b2529988aa" }, "downloads": -1, "filename": "typed_config-0.1.1-py3-none-any.whl", "has_sig": false, "md5_digest": "210cff15abc245acb66454a4fbc3cd6d", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6.0", "size": 11864, "upload_time": "2019-05-09T11:23:35", "url": "https://files.pythonhosted.org/packages/5b/b3/b4d2af26c4fed3c99c1ad26c2b0d933080392b7eb209217a33fd7588f1e1/typed_config-0.1.1-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "343343be9fe4a36bf1ff9ec3d3d14abe", "sha256": "71ae2592408776036a6a8ae6d015132ccc3cd58d369c0902953ed35877e9acd0" }, "downloads": -1, "filename": "typed-config-0.1.1.tar.gz", "has_sig": false, "md5_digest": "343343be9fe4a36bf1ff9ec3d3d14abe", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6.0", "size": 18253, "upload_time": "2019-05-09T11:23:36", "url": "https://files.pythonhosted.org/packages/6b/74/90bf73ce5b2cb1e29412b3c8e984bd514ed386cb7633dbfcdd24740d65f1/typed-config-0.1.1.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "210cff15abc245acb66454a4fbc3cd6d", "sha256": "a7beb7243e2e3f41e732583c94a37b82c650f21209384448347e18b2529988aa" }, "downloads": -1, "filename": "typed_config-0.1.1-py3-none-any.whl", "has_sig": false, "md5_digest": "210cff15abc245acb66454a4fbc3cd6d", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6.0", "size": 11864, "upload_time": "2019-05-09T11:23:35", "url": "https://files.pythonhosted.org/packages/5b/b3/b4d2af26c4fed3c99c1ad26c2b0d933080392b7eb209217a33fd7588f1e1/typed_config-0.1.1-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "343343be9fe4a36bf1ff9ec3d3d14abe", "sha256": "71ae2592408776036a6a8ae6d015132ccc3cd58d369c0902953ed35877e9acd0" }, "downloads": -1, "filename": "typed-config-0.1.1.tar.gz", "has_sig": false, "md5_digest": "343343be9fe4a36bf1ff9ec3d3d14abe", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6.0", "size": 18253, "upload_time": "2019-05-09T11:23:36", "url": "https://files.pythonhosted.org/packages/6b/74/90bf73ce5b2cb1e29412b3c8e984bd514ed386cb7633dbfcdd24740d65f1/typed-config-0.1.1.tar.gz" } ] }