{"id":16913294,"url":"https://github.com/pmbrull/levy","last_synced_at":"2026-03-16T14:01:27.678Z","repository":{"id":47063528,"uuid":"380532252","full_name":"pmbrull/levy","owner":"pmbrull","description":"Yet Another Configuration Parser","archived":false,"fork":false,"pushed_at":"2024-03-20T16:11:28.000Z","size":379,"stargazers_count":3,"open_issues_count":3,"forks_count":0,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-03-21T08:02:30.297Z","etag":null,"topics":["config","jinja2","python","yaml"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pmbrull.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"docs/contributing.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-06-26T15:16:40.000Z","updated_at":"2023-11-13T09:38:12.000Z","dependencies_parsed_at":"2025-02-19T17:33:44.198Z","dependency_job_id":"5ebc8d5b-f1e3-4456-a193-18e051a75322","html_url":"https://github.com/pmbrull/levy","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pmbrull%2Flevy","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pmbrull%2Flevy/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pmbrull%2Flevy/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pmbrull%2Flevy/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pmbrull","download_url":"https://codeload.github.com/pmbrull/levy/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248438515,"owners_count":21103410,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["config","jinja2","python","yaml"],"created_at":"2024-10-13T19:12:49.841Z","updated_at":"2026-03-16T14:01:22.613Z","avatar_url":"https://github.com/pmbrull.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# levy\n\n[![Latest version](https://img.shields.io/pypi/v/levy.svg)](https://pypi.org/project/levy/)\n[![Python versions](https://img.shields.io/pypi/pyversions/levy.svg)](https://pypi.org/project/levy/)\n[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)\n[![actions](https://github.com/pmbrull/levy/actions/workflows/CI.yaml/badge.svg)](https://github.com/pmbrull/levy)\n\n\u003e Yet Another Configuration Parser\n\nThis project is a lightweight take on configuration parsing with a twist.\n\n- Docs: https://pmbrull.github.io/levy/\n\n## Installation\n\nGet up and running with\n\n```bash\npip install levy\n```\n\nIt supports reading both JSON and YAML files, as well as getting configurations\ndirectly from a `dict`.\n\nThe interesting approach here is regarding handling multiple environments. Usually we\nneed to pass different parameters depending on where we are (DEV, PROD, and any \narbitrary environment name we might use). It is also common to have these specific parameters\navailable as env variables, be it our infra or in a CI/CD process.\n\n`levy` adds a `jinja2` layer on top of our config files, so that not only we can load\nenv variables on the fly, but helps us leverage templating syntax to keep\nour configurations centralized and DRY.\n\n## How to\n\nLet's suppose we have the following configuration:\n\n```yaml\ntitle: \"Lévy the cat\"\ncolors:\n  - \"black\"\n  - \"white\"\nhobby:\n  eating:\n    what: \"anything\"\nfriends:\n  {% set friends = [ \"cartman\", \"lima\" ] %}\n  {% for friend in friends %}\n  - name: ${ friend }\n    type: \"cat\"\n  {% endfor %}\n```\n\nWe have a bit of everything:\n- Root configurations\n- Simple lists\n- Nested configurations\n- Dynamic `jinja2` lists as nested configurations\n\nWe can create our `Config` object as\n\n```python\nfrom levy.config import Config\n\ncfg = Config.read_file(\"test.yaml\")\n```\n\nAs there is the `jinja2` layer we might want to check what is the shape of the\nparsed values. We can do so with `cfg._vars`. In our case we'll get back something\nlike:\n\n```\n{\n'title': 'Lévy the cat',\n'colors': ['black', 'white'],\n'hobby': {\n  'eating': {\n    'what': 'anything'\n    }\n  },\n'friends': [\n  {'name': 'cartman', 'type': 'cat'},\n  {'name': 'lima', 'type': 'cat'}\n  ]\n}\n```\n\n\u003e OBS: When reading from files and for debugging purposes, we can access the `cfg._file`\nvar to check what file was parsed.\n\n### Accessing values\n\nAll the information has been set as attributes to the `Config` instance. We can\nretrieve the values as `cfg.\u003cname\u003e`, e.g.\n\n```python\ncfg.title  # 'Lévy the cat'\ncfg.colors  # ['black', 'white']\n```\n\nNote that so far those are just `root` values, as they come directly from the root\nconfiguration. Whenever we have a nested item, we are creating a `Config` attribute\nwith the key as name:\n\n```python\nprint(cfg)  # Config(root)\nprint(cfg.hobby)  # Config(hobby)\n```\n\nIf we need to retrieve nested values, as we are just nesting `Config` instances, we can\nkeep chaining attribute calls:\n\n```python\ncfg.hobby.eating.what  # 'anything'\n```\n\n### Nested Config lists\n\nThe `colors` list has nothing fancy in it, as we have simple types. However, we want\nto parse nested configurations as `Config`, while being able to access them by name\nas attributes.\n\nTo fit this spot we have `namedtuple`s. The list attribute becomes a `namedtuple` where\nthe properties are the `name`s of the nested items. `name` is set as the default\nidentifier, but we can pass others as parameter,\n\n```python\nprint(cfg.friends.lima)  # Config(lima)\ncfg.friends.lima.type  # 'cat'\n```\n\nAnd if we check the type...\n```python\nisinstance(cfg.friends, tuple)  # True\n```\n\nIf we encounter an error while defining the `namedtuple`s structure, we will get a \n`ListParseException`. We should then check how are we defining the lists and our `list_id`.\n\n\u003e OBS: Note that the `list_id` field should be a valid `namedtuple` key. This means that\n    it cannot contain spaces or other not supported special characters.\n\n## Using defaults\n\nIt is common to fall back to default values when some parameter is not informed in our configuration.\n\nWe can `__call__` our `Config` in order to be able to apply them.\n\n```python\ncfg(\"not in there\", default=\"default\")  # 'default'\ncfg(\"not in there\", default=None)  # None\n```\n\nIf no default is specified, the call will run the usual attribute retrieval. This is\ninteresting for cases where we need to dynamically get some configuration that *should*\nbe there:\n\n```python\ncfg(\"not in there\")  # AttributeError\n```\n\n## Render custom functions\n\n### Environment Variables\n\nWith this templating approach on top of our files, we can not only use default behaviors, but also\ndefine our own custom functionalities.\n\nThe one we have provided by default is reading environment variables at render time:\n\n```yaml\nvariable: ${ env('VARIABLE') }\ndefault: ${ env('foo', default='bar') }\n```\n\nWhere the function `env` is the key name given to a function defined to `get` env vars\nwith an optional default. If the env variable is not found and no default is provided,\nwe'll get a `MissingEnvException`.\n\n### Registering new functions\n\nIf we need to apply different functions when rendering the files, we can register them\nby name before instantiating the `Config` class.\n\nLet's imagine the following YAML file:\n\n```yaml\nvariable: ${ my_func(1) }\nfoo: ${ bar('x') }\n```\n\nWe then need to define the behavior of the functions `my_func` and `bar`.\n\n```python\nfrom levy.config import Config\nfrom levy.renderer import render_reg\n\n@render_reg.add()  # By default, it registers the function name\ndef my_func(num: int):\n    return num + 1\n\n@render_reg.add('bar')  # Name can be overwritten if required\ndef upper(s: str):\n    return s.upper()\n\ncfg = Config.read_file(\"\u003cfile\u003e\")\ncfg.variable  # 2\ncfg.foo  # 'X'\n```\n\nNote how we registered `my_func` with the same name it appeared in the YAML. However,\nthe name is completely arbitrary, and we can pass the function `upper` with the name `bar`.\n\nWith this approach one can add even further dynamism to both YAML and JSON config files.\n\nTo peek into the registry state, we can run:\n\n```python\nrender_reg.registry\n```\n\nWhich in the example will show us\n\n```\n{'env': \u003cfunction __main__.get_env(conf_str: str, default: Optional[str] = None) -\u003e str\u003e,\n 'my_func': \u003cfunction __main__.my_func(num: int)\u003e,\n 'bar': \u003cfunction __main__.upper(s: str)\u003e}\n```\n\n## Schema Validation\n\nAt some point it might be interesting to make sure that the config we are reading follows\nsome standards. That is why we have introduced the ability to pass a schema our file\nneeds to follow.\n\nThis feature is supported by [Pydantic](https://pydantic-docs.helpmanual.io/), \nand not only helps us to validate the schema, but even updating the values we're \nreading with `Optionals` and defaults.\n\nWe can get this running as\n\n```python\nfrom pydantic import BaseModel\n\n\nclass Friends(BaseModel):\n    name: str\n    type: str\n    fur: str = \"soft\"\n\nclass Kitten(BaseModel):\n    title: str\n    age: Optional[int]\n    colors: List[str]\n    hobby: Dict[str, Dict[str, str]]\n    friends: List[Friends]\n\ncfg = Config.read_file(\"\u003cfile\u003e\", datatype=Kitten)\n\n# We should have the data attribute now hosting the data class\nassert cfg.data is not None\n\n# We have optional values as None\nassert cfg.age is None\n\n# We have missing values with their default\nassert cfg.friends.lima.fur == \"soft\"\n```\n\nNote how this adds even another layer of flexibility, as after reading the file we will\nhave all the data we might require available to use.\n\n## Contributing\n\nYou can install the project requirements with `make install`. To run the tests, `make install_test`\nand `make unit`.\n\nWith `make precommit_install` you can install the pre-commit hooks.\n\nTo install the package from source, clone the repo, `pip install flit` and run `flit install`.\n\n## References\n\n- [pyconfs](https://github.com/gahjelle/pyconfs) as inspiration.\n- [pydantic](https://pydantic-docs.helpmanual.io/) - implementing the validation and\n    data filling.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpmbrull%2Flevy","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpmbrull%2Flevy","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpmbrull%2Flevy/lists"}