[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![PyPI Stats: downloads](https://img.shields.io/pypi/dm/runcon.svg)](https://pypi.org/project/runcon)
[![pre-commit](https://github.com/demmerichs/runcon/actions/workflows/pre-commit.yml/badge.svg)](https://github.com/demmerichs/runcon/actions/workflows/pre-commit.yml)
[![Python Version](https://img.shields.io/pypi/pyversions/runcon)](https://github.com/demmerichs/runcon)
[![Package Version](https://img.shields.io/pypi/v/runcon)](https://pypi.org/project/runcon)
[![Package Status](https://img.shields.io/pypi/status/runcon)](https://github.com/demmerichs/runcon)
[![Tests](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/demmerichs/7e5c41da825c828d7db05e41cdaf5bc2/raw/test_badge.json)](https://github.com/demmerichs/runcon/actions/workflows/test_coverage.yml)
[![Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/demmerichs/7e5c41da825c828d7db05e41cdaf5bc2/raw/coverage_badge.json)](https://github.com/demmerichs/runcon/actions/workflows/test_coverage.yml)
# runcon <!-- omit in toc -->
runcon is an MIT-licensed package that provides a `Config` class with a lot of functionality that helps and simplifies organizing many, differently configured runs (hence the name **Run** **Con**figuration). Its main target audience are scientists and researchers who run many different experiments either in the real world or a computer-simulated environment and want to control the runs through a base configuration as well as save each run's settings in configuration files. The `Config` class helps creating differently configured runs through user-configurable hierarchical configuration layouts, it automatically creates paths for each run which can be used to save results, and it helps in comparing the config files of each run during the step of analyzing and comparing different runs.
This package was developed with Deep Learning experiments in mind. These usually consist of large and complex configurations and will therefore also be the basis for the upcoming examples of usage.
You can find the API reference [here](https://demmerichs.github.io/runcon/)!
<a id="toc"></a>
- [Installation](#installation)
- [Basic Usage](#basic-usage)
- [Loading configurations](#loading-configurations)
- [Accessing configuration values](#accessing-configuration-values)
- [Creating runs](#creating-runs)
- [Organizing runs](#organizing-runs)
# Installation<a id="installation"></a> [`↩`](#toc)
runcon is in PyPI, so it can be installed directly using:
```bash
pip install runcon
```
Or from GitHub:
```bash
git clone https://github.com/demmerichs/runcon.git
cd runcon
pip install .
```
# Basic Usage<a id="basic-usage"></a> [`↩`](#toc)
This package builts upon `PyYAML` as a parser for loading and saving configuration files, therefor you should adhere to the YAML-Syntax when writing your configuration.
## Loading configurations<a id="loading-configurations"></a> [`↩`](#toc)
You can load from a single file:
<!--phmdoctest-share-names-->
```python
from runcon import Config
cfg = Config.from_file("cfgs/file_example.cfg")
print(cfg, end="")
```
produces
```
_CFG_ID: 1d4d313eedb05ae00c98ac8cb0a34946
top_level:
more_levels:
deep_level_list:
- list_value
- null
- 3+4j
- 3.14
- true
```
Or you can load from a directory, in which case the filenames will become the toplevel keys. The following layout
```bash
cfgs
├── dir_example
│ ├── forest.cfg
│ └── garden.cfg
```
with the following code
```python
cfg = Config.from_dir("cfgs/dir_example", file_ending=".cfg")
print(cfg, end="")
```
produces
```
_CFG_ID: 705951e95af9b1f6cf314e0f96835349
forest:
trees: 1000
animals: 20
garden:
trees: 2
animals: 0
```
Another way to load multiple configuration files at once is by specifying all the files and their corresponding keys manually.
```python
key_file_dict = {
"black_forest": "cfgs/dir_example/forest.cfg",
"random_values": "cfgs/file_example.cfg",
}
cfg = Config.from_key_file_dict(key_file_dict)
print(cfg, end="")
```
produces
```
_CFG_ID: 60b454fb7619eb972cec13e99ff6addf
black_forest:
trees: 1000
animals: 20
random_values:
top_level:
more_levels:
deep_level_list:
- list_value
- null
- 3+4j
- 3.14
- true
```
## Accessing configuration values<a id="accessing-configuration-values"></a> [`↩`](#toc)
The `Config` object inherets `AttrDict` (a support class by `runcon`). Therefore, values can either be accessed the same way as in a `dict`, or via attribute-access.
Additionally, `Config` supports access via string-concatenation of the keys using a dot as delimiter, e.g.
```python
>>> from runcon import Config
>>> cfg = Config({
... "top": {
... "middle": {"bottom": 3.14},
... "cfg": "value",
... }
... })
>>> print(cfg.top.middle["bottom"])
3.14
>>> print(cfg["top"].cfg)
value
>>> print(cfg["top.middle.bottom"])
3.14
```
## Creating runs<a id="creating-runs"></a> [`↩`](#toc)
Most projects managing multiple runs do this by manually labeling different configuration setups for each run. The main drawbacks of this approach for a larger set of runs are:
- non-deterministic: Different people might label the same configuration differently or different configurations the same way. Even the same person might not remember after a week which settings exactly were changed based on their labeling.
- non-descriptive: In complex configurations a short label cannot capture all setting changes. Finding these via a diff-view can become daunting and unstructured, making it complicated to easliy grasp all the changes made.
Together with this package we propose an alternate way of structuring runs and configurations and trading of slightly longer "labels" for the removal of the above drawbacks.
Most projects start with a single default configuration, and going from there apply one or more change of settings to produce differently configured runs.
We suggest moving all this information into one or multiple configuration files, e.g. a single default configuration, and multiple named setting changes:
```yaml
# dl_example.cfg
default:
model:
name: ResNet
layers: 50
batchsize: 16
optimizer:
name: Adam
learningrate: 1e-3
loss: MSE
small_net:
model:
layers: 5
large_net:
model:
layers: 100
alex:
model:
name: AlexNet
optimizer:
name: SGD
large_bs:
batchsize: 64
optimizer:
learningrate: 4e-3
```
You could now create in code your run configuration like this (but not miss the shortcut after this example):
```python
from copy import deepcopy
base_cfgs = Config.from_file("cfgs/dl_example.cfg")
cfg = deepcopy(base_cfgs.default)
# rupdate works similar to dict.update, but recursivly updates lower layers
cfg.rupdate(base_cfgs.large_net)
cfg.rupdate(base_cfgs.alex)
cfg.loss = "SmoothL1"
cfg.optimizer.learningrate = 1e-4
print(cfg, end="")
```
produces
```
_CFG_ID: be99468b9911c12ccba140ae5d9f487a
model:
name: AlexNet
layers: 100
batchsize: 16
optimizer:
name: SGD
learningrate: 0.0001
weightdecay: 1.0e-06
loss: SmoothL1
```
As this pattern of stacking/merging configurations and possibly modifying a few single values is very common or at least the intended way for using this package, there is a simple shortcut function which operates on string input such that a CLI parser can easily pass values to this function.
For example, you might want to run a script specifying the above constructed configuration like this:
```
python your_runner_script.py \
--cfg default large_net alex \
--set \
loss SmoothL1 \
optimizer.learningrate 1e-4
```
The details of how your CLI interface should look and how you want to parse the values is left to you, (e.g. you could leave out `default` if you have only a single default configuration and just add it inside your code after CLI invocation), but parsing the above command options into the following all-strings variables
<!--phmdoctest-share-names-->
```python
cfg_chain = ["default", "large_net", "alex"]
set_values = [
"loss", "SmoothL1",
"optimizer.learningrate", "1e-4",
]
```
would allow you to call
<!--phmdoctest-share-names-->
```python
base_cfgs = Config.from_file("cfgs/dl_example.cfg")
cfg = base_cfgs.create(cfg_chain, kv=set_values)
print(cfg, end="")
```
and produces (using internally `ast.literal_eval` to parse non-string values, like booleans or floats, in this example `1e-4`)
```
_CFG_ID: be99468b9911c12ccba140ae5d9f487a
model:
name: AlexNet
layers: 100
batchsize: 16
optimizer:
name: SGD
learningrate: 0.0001
weightdecay: 1.0e-06
loss: SmoothL1
```
The resulting label for this configuration would then consist of the configuration chain and the single key-value pairs, and can be automatically reconstructed from the base configs, e.g.
```python
print(cfg.create_auto_label(base_cfgs))
```
produces
```
default alex large_net -s optimizer.learningrate 0.0001 loss SmoothL1
```
Given the run configuration and the set of base configurations, this label can always deterministically be created, and making it shorter is just a matter of wrapping more key-value pairs or base configs into meta configurations.
For the above example this could mean just adding a `smoothl1` sub config which also changes the learning rate, e.g.
```python
base_cfgs.smoothl1 = Config({"loss": "SmoothL1", "optimizer": {"learningrate": 0.0001}})
print(cfg.create_auto_label(base_cfgs))
```
produces
```
default smoothl1 alex large_net
```
This approach mitigates both drawbacks mentioned earlier. The labels are deterministic, and based on the labels, it is quite easy to read of the changes made to the default configuration, as the label itself describes hierarchical changes and the base configurations modifying the default configuration are considered to be minimalistic.
## Organizing runs<a id="organizing-runs"></a> [`↩`](#toc)
After creating your run configuration in your script, it is time to create a directory for your new run, and using it to dump your results from that run.
<!--phmdoctest-share-names-->
```python
cfg_dir = cfg.initialize_cfg_path(base_path="/tmp/Config_test", timestamp=False)
print(type(cfg_dir), cfg_dir)
```
produces
```
<class 'pathlib.PosixPath'> /tmp/Config_test/8614010d20024c05f815cc8edcc8982f
```
The path mainly consists of two parts, a time stamp allowing you to store multiple runs with the same configuration (if you specify `timestampe=True`), and a hash produced by the configuration. Assuming hash collisions are too rare to be ever a problem, two configurations that differ somehow, will always produce different hashes. The hash is used, as it only depends on the configuration, whereas the automatic labeling depends also on the base configuration. The previous section demonstrated, how a change in the base configurations can produce a change in the automatic label. The `initialize_cfg_path` routine also produces a `description` folder next to the configuration folders, where symlinks are stored to the configuration folders, but with the automatic labels. This ensures, that the symlinks can easily be recreated based on a changed configuration, without the need to touch the actual run directories.
Another thing that happens during the path initialization is a call to `cfg.finalize()`. This should mimic the behavior of making all values constant and ensures that the configuration file that was created on disk actually represents all values used during the run execution, and accidental in-place value changes can be mostly ruled out.
```python
try:
cfg.loss = "new loss"
except ValueError as e:
print(e)
print(cfg.loss)
cfg.unfinalize()
cfg.loss = "new loss"
print(cfg.loss)
```
produces
```
This Config was already finalized! Setting attribute or item with name loss to value new loss failed!
SmoothL1
new loss
```
# License <!-- omit in toc -->
runcon is released under a MIT license.
Raw data
{
"_id": null,
"home_page": "",
"name": "runcon",
"maintainer": "",
"docs_url": null,
"requires_python": ">=3.7",
"maintainer_email": "",
"keywords": "config,cfg,configuration,dict,dictionary,attribute,hierarchical,hierarchy,experiments,runs,deep learning,command line,organization,structure",
"author": "",
"author_email": "David Emmerichs <davidj.emmerichs+runcon@gmail.com>",
"download_url": "https://files.pythonhosted.org/packages/45/d5/b1daf9d4a31c412f26523f67a65c974d7d99c94e9d320c1c5a849f242010/runcon-1.1.11.tar.gz",
"platform": null,
"description": "[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n[![PyPI Stats: downloads](https://img.shields.io/pypi/dm/runcon.svg)](https://pypi.org/project/runcon)\n[![pre-commit](https://github.com/demmerichs/runcon/actions/workflows/pre-commit.yml/badge.svg)](https://github.com/demmerichs/runcon/actions/workflows/pre-commit.yml)\n[![Python Version](https://img.shields.io/pypi/pyversions/runcon)](https://github.com/demmerichs/runcon)\n[![Package Version](https://img.shields.io/pypi/v/runcon)](https://pypi.org/project/runcon)\n[![Package Status](https://img.shields.io/pypi/status/runcon)](https://github.com/demmerichs/runcon)\n[![Tests](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/demmerichs/7e5c41da825c828d7db05e41cdaf5bc2/raw/test_badge.json)](https://github.com/demmerichs/runcon/actions/workflows/test_coverage.yml)\n[![Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/demmerichs/7e5c41da825c828d7db05e41cdaf5bc2/raw/coverage_badge.json)](https://github.com/demmerichs/runcon/actions/workflows/test_coverage.yml)\n\n# runcon <!-- omit in toc -->\n\nruncon is an MIT-licensed package that provides a `Config` class with a lot of functionality that helps and simplifies organizing many, differently configured runs (hence the name **Run** **Con**figuration). Its main target audience are scientists and researchers who run many different experiments either in the real world or a computer-simulated environment and want to control the runs through a base configuration as well as save each run's settings in configuration files. The `Config` class helps creating differently configured runs through user-configurable hierarchical configuration layouts, it automatically creates paths for each run which can be used to save results, and it helps in comparing the config files of each run during the step of analyzing and comparing different runs.\n\nThis package was developed with Deep Learning experiments in mind. These usually consist of large and complex configurations and will therefore also be the basis for the upcoming examples of usage.\n\nYou can find the API reference [here](https://demmerichs.github.io/runcon/)!\n\n<a id=\"toc\"></a>\n- [Installation](#installation)\n- [Basic Usage](#basic-usage)\n - [Loading configurations](#loading-configurations)\n - [Accessing configuration values](#accessing-configuration-values)\n - [Creating runs](#creating-runs)\n - [Organizing runs](#organizing-runs)\n\n# Installation<a id=\"installation\"></a> [`\u21a9`](#toc)\n\nruncon is in PyPI, so it can be installed directly using:\n```bash\npip install runcon\n```\n\nOr from GitHub:\n```bash\ngit clone https://github.com/demmerichs/runcon.git\ncd runcon\npip install .\n```\n\n# Basic Usage<a id=\"basic-usage\"></a> [`\u21a9`](#toc)\n\nThis package builts upon `PyYAML` as a parser for loading and saving configuration files, therefor you should adhere to the YAML-Syntax when writing your configuration.\n\n## Loading configurations<a id=\"loading-configurations\"></a> [`\u21a9`](#toc)\n\nYou can load from a single file:\n<!--phmdoctest-share-names-->\n```python\nfrom runcon import Config\n\ncfg = Config.from_file(\"cfgs/file_example.cfg\")\nprint(cfg, end=\"\")\n```\nproduces\n```\n_CFG_ID: 1d4d313eedb05ae00c98ac8cb0a34946\n\ntop_level:\n more_levels:\n deep_level_list:\n - list_value\n - null\n - 3+4j\n - 3.14\n - true\n```\n\nOr you can load from a directory, in which case the filenames will become the toplevel keys. The following layout\n```bash\ncfgs\n\u251c\u2500\u2500 dir_example\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 forest.cfg\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 garden.cfg\n```\nwith the following code\n```python\ncfg = Config.from_dir(\"cfgs/dir_example\", file_ending=\".cfg\")\nprint(cfg, end=\"\")\n```\nproduces\n```\n_CFG_ID: 705951e95af9b1f6cf314e0f96835349\n\nforest:\n trees: 1000\n animals: 20\n\ngarden:\n trees: 2\n animals: 0\n```\n\nAnother way to load multiple configuration files at once is by specifying all the files and their corresponding keys manually.\n\n```python\nkey_file_dict = {\n \"black_forest\": \"cfgs/dir_example/forest.cfg\",\n \"random_values\": \"cfgs/file_example.cfg\",\n}\ncfg = Config.from_key_file_dict(key_file_dict)\nprint(cfg, end=\"\")\n```\nproduces\n```\n_CFG_ID: 60b454fb7619eb972cec13e99ff6addf\n\nblack_forest:\n trees: 1000\n animals: 20\n\nrandom_values:\n top_level:\n more_levels:\n deep_level_list:\n - list_value\n - null\n - 3+4j\n - 3.14\n - true\n```\n\n## Accessing configuration values<a id=\"accessing-configuration-values\"></a> [`\u21a9`](#toc)\n\nThe `Config` object inherets `AttrDict` (a support class by `runcon`). Therefore, values can either be accessed the same way as in a `dict`, or via attribute-access.\nAdditionally, `Config` supports access via string-concatenation of the keys using a dot as delimiter, e.g.\n\n```python\n>>> from runcon import Config\n>>> cfg = Config({\n... \"top\": {\n... \"middle\": {\"bottom\": 3.14},\n... \"cfg\": \"value\",\n... }\n... })\n>>> print(cfg.top.middle[\"bottom\"])\n3.14\n>>> print(cfg[\"top\"].cfg)\nvalue\n>>> print(cfg[\"top.middle.bottom\"])\n3.14\n```\n\n## Creating runs<a id=\"creating-runs\"></a> [`\u21a9`](#toc)\n\nMost projects managing multiple runs do this by manually labeling different configuration setups for each run. The main drawbacks of this approach for a larger set of runs are:\n- non-deterministic: Different people might label the same configuration differently or different configurations the same way. Even the same person might not remember after a week which settings exactly were changed based on their labeling.\n- non-descriptive: In complex configurations a short label cannot capture all setting changes. Finding these via a diff-view can become daunting and unstructured, making it complicated to easliy grasp all the changes made.\n\nTogether with this package we propose an alternate way of structuring runs and configurations and trading of slightly longer \"labels\" for the removal of the above drawbacks.\n\nMost projects start with a single default configuration, and going from there apply one or more change of settings to produce differently configured runs.\nWe suggest moving all this information into one or multiple configuration files, e.g. a single default configuration, and multiple named setting changes:\n```yaml\n# dl_example.cfg\ndefault:\n model:\n name: ResNet\n layers: 50\n batchsize: 16\n optimizer:\n name: Adam\n learningrate: 1e-3\n loss: MSE\n\nsmall_net:\n model:\n layers: 5\n\nlarge_net:\n model:\n layers: 100\n\nalex:\n model:\n name: AlexNet\n optimizer:\n name: SGD\n\nlarge_bs:\n batchsize: 64\n optimizer:\n learningrate: 4e-3\n```\n\nYou could now create in code your run configuration like this (but not miss the shortcut after this example):\n\n```python\nfrom copy import deepcopy\n\nbase_cfgs = Config.from_file(\"cfgs/dl_example.cfg\")\ncfg = deepcopy(base_cfgs.default)\n# rupdate works similar to dict.update, but recursivly updates lower layers\ncfg.rupdate(base_cfgs.large_net)\ncfg.rupdate(base_cfgs.alex)\ncfg.loss = \"SmoothL1\"\ncfg.optimizer.learningrate = 1e-4\nprint(cfg, end=\"\")\n```\nproduces\n```\n_CFG_ID: be99468b9911c12ccba140ae5d9f487a\n\nmodel:\n name: AlexNet\n layers: 100\n\nbatchsize: 16\n\noptimizer:\n name: SGD\n learningrate: 0.0001\n weightdecay: 1.0e-06\n\nloss: SmoothL1\n```\n\nAs this pattern of stacking/merging configurations and possibly modifying a few single values is very common or at least the intended way for using this package, there is a simple shortcut function which operates on string input such that a CLI parser can easily pass values to this function.\n\nFor example, you might want to run a script specifying the above constructed configuration like this:\n```\npython your_runner_script.py \\\n --cfg default large_net alex \\\n --set \\\n loss SmoothL1 \\\n optimizer.learningrate 1e-4\n```\nThe details of how your CLI interface should look and how you want to parse the values is left to you, (e.g. you could leave out `default` if you have only a single default configuration and just add it inside your code after CLI invocation), but parsing the above command options into the following all-strings variables\n<!--phmdoctest-share-names-->\n```python\ncfg_chain = [\"default\", \"large_net\", \"alex\"]\nset_values = [\n \"loss\", \"SmoothL1\",\n \"optimizer.learningrate\", \"1e-4\",\n]\n```\nwould allow you to call\n<!--phmdoctest-share-names-->\n```python\nbase_cfgs = Config.from_file(\"cfgs/dl_example.cfg\")\ncfg = base_cfgs.create(cfg_chain, kv=set_values)\nprint(cfg, end=\"\")\n```\nand produces (using internally `ast.literal_eval` to parse non-string values, like booleans or floats, in this example `1e-4`)\n```\n_CFG_ID: be99468b9911c12ccba140ae5d9f487a\n\nmodel:\n name: AlexNet\n layers: 100\n\nbatchsize: 16\n\noptimizer:\n name: SGD\n learningrate: 0.0001\n weightdecay: 1.0e-06\n\nloss: SmoothL1\n```\n\nThe resulting label for this configuration would then consist of the configuration chain and the single key-value pairs, and can be automatically reconstructed from the base configs, e.g.\n```python\nprint(cfg.create_auto_label(base_cfgs))\n```\nproduces\n```\ndefault alex large_net -s optimizer.learningrate 0.0001 loss SmoothL1\n```\nGiven the run configuration and the set of base configurations, this label can always deterministically be created, and making it shorter is just a matter of wrapping more key-value pairs or base configs into meta configurations.\n\nFor the above example this could mean just adding a `smoothl1` sub config which also changes the learning rate, e.g.\n```python\nbase_cfgs.smoothl1 = Config({\"loss\": \"SmoothL1\", \"optimizer\": {\"learningrate\": 0.0001}})\nprint(cfg.create_auto_label(base_cfgs))\n```\nproduces\n```\ndefault smoothl1 alex large_net\n```\n\nThis approach mitigates both drawbacks mentioned earlier. The labels are deterministic, and based on the labels, it is quite easy to read of the changes made to the default configuration, as the label itself describes hierarchical changes and the base configurations modifying the default configuration are considered to be minimalistic.\n\n## Organizing runs<a id=\"organizing-runs\"></a> [`\u21a9`](#toc)\n\nAfter creating your run configuration in your script, it is time to create a directory for your new run, and using it to dump your results from that run.\n\n<!--phmdoctest-share-names-->\n```python\ncfg_dir = cfg.initialize_cfg_path(base_path=\"/tmp/Config_test\", timestamp=False)\nprint(type(cfg_dir), cfg_dir)\n```\nproduces\n```\n<class 'pathlib.PosixPath'> /tmp/Config_test/8614010d20024c05f815cc8edcc8982f\n```\n\nThe path mainly consists of two parts, a time stamp allowing you to store multiple runs with the same configuration (if you specify `timestampe=True`), and a hash produced by the configuration. Assuming hash collisions are too rare to be ever a problem, two configurations that differ somehow, will always produce different hashes. The hash is used, as it only depends on the configuration, whereas the automatic labeling depends also on the base configuration. The previous section demonstrated, how a change in the base configurations can produce a change in the automatic label. The `initialize_cfg_path` routine also produces a `description` folder next to the configuration folders, where symlinks are stored to the configuration folders, but with the automatic labels. This ensures, that the symlinks can easily be recreated based on a changed configuration, without the need to touch the actual run directories.\n\nAnother thing that happens during the path initialization is a call to `cfg.finalize()`. This should mimic the behavior of making all values constant and ensures that the configuration file that was created on disk actually represents all values used during the run execution, and accidental in-place value changes can be mostly ruled out.\n\n```python\ntry:\n cfg.loss = \"new loss\"\nexcept ValueError as e:\n print(e)\nprint(cfg.loss)\ncfg.unfinalize()\ncfg.loss = \"new loss\"\nprint(cfg.loss)\n```\nproduces\n```\nThis Config was already finalized! Setting attribute or item with name loss to value new loss failed!\nSmoothL1\nnew loss\n```\n\n# License <!-- omit in toc -->\nruncon is released under a MIT license.\n",
"bugtrack_url": null,
"license": "",
"summary": "Manage a multitude of runs through hierarchical configuration",
"version": "1.1.11",
"split_keywords": [
"config",
"cfg",
"configuration",
"dict",
"dictionary",
"attribute",
"hierarchical",
"hierarchy",
"experiments",
"runs",
"deep learning",
"command line",
"organization",
"structure"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "f922930f66c07ab92f401509b42178cc783837b7c9f0126b9b9b47274d031910",
"md5": "74c0ed80c4ebad1a4de96d4f67864f90",
"sha256": "62eda2eb745de8eedd63f3652d2c7cafcd78651a55eee874644bfbc092dbf5b8"
},
"downloads": -1,
"filename": "runcon-1.1.11-py3-none-any.whl",
"has_sig": false,
"md5_digest": "74c0ed80c4ebad1a4de96d4f67864f90",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.7",
"size": 19518,
"upload_time": "2023-03-22T21:34:18",
"upload_time_iso_8601": "2023-03-22T21:34:18.895468Z",
"url": "https://files.pythonhosted.org/packages/f9/22/930f66c07ab92f401509b42178cc783837b7c9f0126b9b9b47274d031910/runcon-1.1.11-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "45d5b1daf9d4a31c412f26523f67a65c974d7d99c94e9d320c1c5a849f242010",
"md5": "c9dd7b057fd930358d685f515645ed64",
"sha256": "883f267c78b7b8065413cd279704f369a4e65a42ff94334b3db56dadd951ec51"
},
"downloads": -1,
"filename": "runcon-1.1.11.tar.gz",
"has_sig": false,
"md5_digest": "c9dd7b057fd930358d685f515645ed64",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.7",
"size": 26556,
"upload_time": "2023-03-22T21:34:21",
"upload_time_iso_8601": "2023-03-22T21:34:21.678614Z",
"url": "https://files.pythonhosted.org/packages/45/d5/b1daf9d4a31c412f26523f67a65c974d7d99c94e9d320c1c5a849f242010/runcon-1.1.11.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2023-03-22 21:34:21",
"github": false,
"gitlab": false,
"bitbucket": false,
"lcname": "runcon"
}