springs


Namesprings JSON
Version 1.13.0 PyPI version JSON
download
home_page
SummaryA set of utilities to create and manage typed configuration files effectively, built on top of OmegaConf.
upload_time2023-08-02 06:41:02
maintainer
docs_urlNone
author
requires_python>=3.8
licenseApache-2.0
keywords configuration yaml json command line cli omegaconf structured config
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # Springs

<img style="  margin-left: auto; margin-right:auto; margin-bottom: 2em; display: block" id="hero" src="https://github.com/soldni/springs/raw/main/assets/img/logo.png" alt="Logo for Springs" width="200px" height="auto">

A set of utilities to turn [OmegaConf][1] into a fully fledge configuration utils.
Just like the springs inside an Omega watch, the Springs library helps you move on with your experiments.

Springs overlaps in functionality with [Hydra][2], but without all the unnecessary boilerplate.

The current logo for Springs was generated using [DALLĀ·E 2][5].

To install Springs from [PyPI][6], simply run:

```bash
pip install springs
```

**Table of Contents**:

- [Philosophy](#philosophy)
- [A Note About Unstructured Configs](#a-note-about-unstructured-configs)
- [Initializing Objects from Configurations](#initializing-objects-from-configurations)
- [Static and Dynamic Type Checking](#static-and-dynamic-type-checking)
- [Flexible Configurations](#flexible-configurations)
- [Adding Help Messages to Configurations](#adding-help-messages-to-configurations)
- [Command Line Options](#command-line-options)
- [Tips and Tricks](#tips-and-tricks)


## Philosophy

OmegaConf supports creating configurations in all sorts of manners, but we believe that there are benefits into defining configuration from structured objects, namely dataclass.
Springs is built around that notion: write one or more dataclass to compose a configuration (with appropriate defaults), then parse the remainder of options or missing values from command line/a yaml file.

Let's look at an example. Imagine we are building a configuration for a machine learning (ML) experiment, and we want to provide information about model and data to use.
We start by writing the following structure configuration

```python
import springs as sp
from dataclasses import dataclass

# Data config
# sp.dataclass is an alias for
# dataclasses.dataclass
@sp.dataclass
class DataConfig:
    # sp.MISSING is an alias to
    # omegaconf.MISSING
    path: str = sp.MISSING
    split: str = 'train'

# Model config
@sp.dataclass
class ModelConfig:
    name: str = sp.MISSING
    num_labels: int = 2


# Experiment config
@sp.dataclass
class ExperimentConfig:
    batch_size: int = 16
    seed: int = 42


# Overall config
# for our program
@sp.dataclass
class Config:
    data: DataConfig = DataConfig()
    model: ModelConfig = ModelConfig()
    exp: ExperimentConfig = ExperimentConfig()
```

Note how, in matching with OmegaConf syntax, we use `MISSING` to indicate any value that has no default and should be provided at runtime.

If we want to use this configuration with a function that actually runs this experiment, we can use `sp.cli` as follows:

```python
@sp.cli(Config)
def main(config: Config)
    # this will print the configuration
    # like a dict
    print(config)
    # you can use dot notation to
    # access attributes...
    config.exp.seed
    # ...or treat it like a dictionary!
    config['exp']['seed']


if __name__ == '__main__':
    main()

```

Notice how, in the configuration `Config` above, some parameters are missing.
We can specify them from command line...

```bash
python main.py \
    data.path=/path/to/data \
    model.name=bert-base-uncased
```

...or from one or more YAML config files (if multiple, *e.g.,* `-c /path/to/cfg1.yaml -c /path/to/cfg2.yaml` the latter ones override the former ones).


```YAML
data:
    path: /path/to/data

model:
    name: bert-base-uncased

# you can override any part of
# the config via YAML or CLI
# CLI takes precedence over YAML.
exp:
    seed: 1337

```

To run with from YAML, do:

```bash
python main.py -c config.yaml
```

Easy, right?


## A Note About Unstructured Configs

You are not required to used a structured config with Springs.
To use our CLI with a bunch of yaml files and/or command line arguments, simply decorate your main function with no arguments.

```python
@sp.cli()
def main(config)
    # do stuff
    ...
```

## Initializing Objects from Configurations

Sometimes a configuration contains all the necessary information to instantiate an object from it.
Springs supports this use case, and it is as easy as providing a `_target_` node in a configuration:

```python
@sp.dataclass
class ModelConfig:
    _target_: str = (
        'transformers.'
        'AutoModelForSequenceClassification.'
        'from_pretrained'
    )
    pretrained_model_name_or_path: str = \
        'bert-base-uncased'
    num_classes: int = 2
```

In your experiment code, run:

```python
def run_model(model_config: ModelConfig):
    ...
    model = sp.init.now(
        model_config, ModelConfig
    )
```

**Note:** While previous versions of Springs merely supported specifying the return type, now it is actively and strongly encouraged.
Running `sp.init.now(model_config)` will raise a warning if the type is not provided. To prevent this warning, use `sp.toggle_warnings(False)` before calling `sp.init.now`/ `sp.init.later`.

### `init.now` vs `init.later`

`init.now` is used to immediately initialize a class or run a method.
But what if the function you are not ready to run the `_target_` you want to initialize?
This is common for example if you receive a configuration in the init method of a class, but you don't have all parameters to run it until later in the object lifetime. In that case, you might want to use `init.later`.
Example:

```python
config = sp.from_dict({'_target_': 'str.lower'})
fn = sp.init.later(config, Callable[..., str])

... # much computation occurs

# returns `this to lowercase`
fn('THIS TO LOWERCASE')
```

Note that, for convenience `sp.init.now` is aliased to `sp.init`.

### Providing Class Paths as `_target_`

If, for some reason, cannot specify the path to a class as a string, you can use `sp.Target.to_string` to resolve a function, class, or method to its path:

```python
import transformers

@sp.dataclass
class ModelConfig:
    _target_: str = sp.Target.to_string(
        transformers.
        AutoModelForSequenceClassification.
        from_pretrained
    )
    pretrained_model_name_or_path: str = \
        'bert-base-uncased'
    num_classes: int = 2
```

## Static and Dynamic Type Checking

Springs supports both static and dynamic (at runtime) type checking when initializing objects.
To enable it, pass the expected return type when initializing an object:

```python
@sp.cli(TokenizerConfig)
def main(config: TokenizerConfig):
    tokenizer = sp.init(
        config, PreTrainedTokenizerBase
    )
    print(tokenizer)
```

This will raise an error when the tokenizer is not a subclass of `PreTrainedTokenizerBase`. Further, if you use a static type checker in your workflow (e.g., [Pylance][3] in [Visual Studio Code][4]), `springs.init` will also annotate its return type accordingly.

## Flexible Configurations

Sometimes a configuration has some default parameters, but others are optional and depend on other factors, such as the `_target_` class.  In these cases, it is convenient to set up a flexible dataclass, using `make_flexy` after the `dataclass` decorator.

```python
@sp.flexyclass
class MetricConfig:
    _target_: str = sp.MISSING
    average: str = 'macro'

config = sp.from_dataclass(MetricConfig)
overrides = {
    # we override the _target_
    '_target_': 'torchmetrics.F1Score',
    # this attribute does not exist in the
    # structured config
    'num_classes': 2
}

config = sp.merge(config,
                  sp.from_dict(overrides))
print(config)
# this will print the following:
# {
#    '_target_': 'torchmetrics.F1Score',
#    'average': 'macro',
#    'num_classes': 2
# }
```

## Adding Help Messages to Configurations

Springs supports adding help messages to your configurations; this is useful to provide a description of parameters to users of your application.

To add a help for a parameter, simply pass `help=...` to a field constructor:

```python
@sp.dataclass
class ModelConfig:
    _target_: str = sp.field(
        default='transformers.'\
            'AutoModelForSequenceClassification.'\
            'from_pretrained',
        help='The class for the model'
    )
    pretrained_model_name_or_path: str = sp.field(
        default='bert-base-uncased',
        help='The name of the model to use'
    )
    num_classes: int = sp.field(
        default=2,
        help='The number of classes for classification'
    )

@sp.dataclass
class ExperimentConfig:
    model: ModelConfig = sp.field(
        default_factory=ModelConfig,
        help='The model configuration'
    )

@sp.cli(ExperimentConfig)
def main(config: ExperimentConfig):
    ...

if __name__ == '__main__':
    main()
```

As you can see, you can also add help messages to nested configurations. Help messages are printed when you call `python my_script.py --options` or `python my_script.py --o`.

## Command Line Options

You can print all command line options by calling `python my_script.py -h` or `python my_script.py --help`. Currently, a Springs application supports the following:

|Flag | Default | Action | Description|
|----:|:-------:|:------:|:-----------|
|`-h/--help` | `False` | toggle | show the help message and exit|
|`-c/--config` | `[]` | append | either a path to a YAML file containing a configuration, or a nickname for a configuration in the registry; multiple configurations can be specified with additional `-c/--config` flags, and they will be merged in the order they are provided|
|`-o/--options` | `False` | toggle | print all default options and CLI flags|
|`-i/--inputs` | `False` | toggle | print the input configuration|
|`-p/--parsed` | `False` | toggle | print the parsed configuration|
|`-l/--log-level` | `WARNING` | set | logging level to use for this program; can be one of `CRITICAL`, `ERROR`, `WARNING`, `INFO`, or `DEBUG`; defaults to `WARNING`.|
|`-d/--debug` | `False` |toggle| enable debug mode; equivalent to `--log-level DEBUG`|
|`-q/--quiet` | `False` |toggle| if provided, it does not print the configuration when running|
|`-r/--resolvers` | `False` |toggle| print all registered resolvers in OmegaConf, Springs, and current codebase|
|`-n/--nicknames` | `False` |toggle| print all registered nicknames in Springs|
|`-s/--save` | `None` | set | save the configuration to a YAML file and exit|

## Tips and Tricks

This section includes a bunch of tips and tricks for working with OmegaConf and YAML.

### Tip 1: Repeating nodes in YAML input

In setting up YAML configuration files for ML experiments, it is common to
have almost-repeated sections.
In these cases, you can take advantage of YAML's built in variable mechanism and dictionary merging to remove duplicated imports:

```yaml
# &tc assigns an alias to this node
train_config: &tc
  path: /path/to/data
  src_field: full_text
  tgt_field: summary
  split_name: train

test_config:
  # << operator indicates merging,
  # *tc is a reference to the alias above
  << : *tc
  split_name: test
```

This will resolve to:

```yaml
train_config:
  path: /path/to/data
  split_name: train
  src_field: full_text
  tgt_field: summary

test_config:
  path: /path/to/data
  split_name: test
  src_field: full_text
  tgt_field: summary
```


[1]: https://omegaconf.readthedocs.io/
[2]: https://hydra.cc/
[3]: https://devblogs.microsoft.com/python/announcing-pylance-fast-feature-rich-language-support-for-python-in-visual-studio-code/
[4]: https://code.visualstudio.com
[5]: https://openai.com/dall-e-2/
[6]: https://pypi.org/project/springs/

            

Raw data

            {
    "_id": null,
    "home_page": "",
    "name": "springs",
    "maintainer": "",
    "docs_url": null,
    "requires_python": ">=3.8",
    "maintainer_email": "",
    "keywords": "configuration,yaml,json,command line,cli,omegaconf,structured,config",
    "author": "",
    "author_email": "Luca Soldaini <luca@soldaini.net>",
    "download_url": "https://files.pythonhosted.org/packages/29/3a/c6cd6f308255f810a7638497b1ae339345403c210d2d06e3dc437eb93d75/springs-1.13.0.tar.gz",
    "platform": null,
    "description": "# Springs\n\n<img style=\"  margin-left: auto; margin-right:auto; margin-bottom: 2em; display: block\" id=\"hero\" src=\"https://github.com/soldni/springs/raw/main/assets/img/logo.png\" alt=\"Logo for Springs\" width=\"200px\" height=\"auto\">\n\nA set of utilities to turn [OmegaConf][1] into a fully fledge configuration utils.\nJust like the springs inside an Omega watch, the Springs library helps you move on with your experiments.\n\nSprings overlaps in functionality with [Hydra][2], but without all the unnecessary boilerplate.\n\nThe current logo for Springs was generated using [DALL\u00b7E 2][5].\n\nTo install Springs from [PyPI][6], simply run:\n\n```bash\npip install springs\n```\n\n**Table of Contents**:\n\n- [Philosophy](#philosophy)\n- [A Note About Unstructured Configs](#a-note-about-unstructured-configs)\n- [Initializing Objects from Configurations](#initializing-objects-from-configurations)\n- [Static and Dynamic Type Checking](#static-and-dynamic-type-checking)\n- [Flexible Configurations](#flexible-configurations)\n- [Adding Help Messages to Configurations](#adding-help-messages-to-configurations)\n- [Command Line Options](#command-line-options)\n- [Tips and Tricks](#tips-and-tricks)\n\n\n## Philosophy\n\nOmegaConf supports creating configurations in all sorts of manners, but we believe that there are benefits into defining configuration from structured objects, namely dataclass.\nSprings is built around that notion: write one or more dataclass to compose a configuration (with appropriate defaults), then parse the remainder of options or missing values from command line/a yaml file.\n\nLet's look at an example. Imagine we are building a configuration for a machine learning (ML) experiment, and we want to provide information about model and data to use.\nWe start by writing the following structure configuration\n\n```python\nimport springs as sp\nfrom dataclasses import dataclass\n\n# Data config\n# sp.dataclass is an alias for\n# dataclasses.dataclass\n@sp.dataclass\nclass DataConfig:\n    # sp.MISSING is an alias to\n    # omegaconf.MISSING\n    path: str = sp.MISSING\n    split: str = 'train'\n\n# Model config\n@sp.dataclass\nclass ModelConfig:\n    name: str = sp.MISSING\n    num_labels: int = 2\n\n\n# Experiment config\n@sp.dataclass\nclass ExperimentConfig:\n    batch_size: int = 16\n    seed: int = 42\n\n\n# Overall config\n# for our program\n@sp.dataclass\nclass Config:\n    data: DataConfig = DataConfig()\n    model: ModelConfig = ModelConfig()\n    exp: ExperimentConfig = ExperimentConfig()\n```\n\nNote how, in matching with OmegaConf syntax, we use `MISSING` to indicate any value that has no default and should be provided at runtime.\n\nIf we want to use this configuration with a function that actually runs this experiment, we can use `sp.cli` as follows:\n\n```python\n@sp.cli(Config)\ndef main(config: Config)\n    # this will print the configuration\n    # like a dict\n    print(config)\n    # you can use dot notation to\n    # access attributes...\n    config.exp.seed\n    # ...or treat it like a dictionary!\n    config['exp']['seed']\n\n\nif __name__ == '__main__':\n    main()\n\n```\n\nNotice how, in the configuration `Config` above, some parameters are missing.\nWe can specify them from command line...\n\n```bash\npython main.py \\\n    data.path=/path/to/data \\\n    model.name=bert-base-uncased\n```\n\n...or from one or more YAML config files (if multiple, *e.g.,* `-c /path/to/cfg1.yaml -c /path/to/cfg2.yaml` the latter ones override the former ones).\n\n\n```YAML\ndata:\n    path: /path/to/data\n\nmodel:\n    name: bert-base-uncased\n\n# you can override any part of\n# the config via YAML or CLI\n# CLI takes precedence over YAML.\nexp:\n    seed: 1337\n\n```\n\nTo run with from YAML, do:\n\n```bash\npython main.py -c config.yaml\n```\n\nEasy, right?\n\n\n## A Note About Unstructured Configs\n\nYou are not required to used a structured config with Springs.\nTo use our CLI with a bunch of yaml files and/or command line arguments, simply decorate your main function with no arguments.\n\n```python\n@sp.cli()\ndef main(config)\n    # do stuff\n    ...\n```\n\n## Initializing Objects from Configurations\n\nSometimes a configuration contains all the necessary information to instantiate an object from it.\nSprings supports this use case, and it is as easy as providing a `_target_` node in a configuration:\n\n```python\n@sp.dataclass\nclass ModelConfig:\n    _target_: str = (\n        'transformers.'\n        'AutoModelForSequenceClassification.'\n        'from_pretrained'\n    )\n    pretrained_model_name_or_path: str = \\\n        'bert-base-uncased'\n    num_classes: int = 2\n```\n\nIn your experiment code, run:\n\n```python\ndef run_model(model_config: ModelConfig):\n    ...\n    model = sp.init.now(\n        model_config, ModelConfig\n    )\n```\n\n**Note:** While previous versions of Springs merely supported specifying the return type, now it is actively and strongly encouraged.\nRunning `sp.init.now(model_config)` will raise a warning if the type is not provided. To prevent this warning, use `sp.toggle_warnings(False)` before calling `sp.init.now`/ `sp.init.later`.\n\n### `init.now` vs `init.later`\n\n`init.now` is used to immediately initialize a class or run a method.\nBut what if the function you are not ready to run the `_target_` you want to initialize?\nThis is common for example if you receive a configuration in the init method of a class, but you don't have all parameters to run it until later in the object lifetime. In that case, you might want to use `init.later`.\nExample:\n\n```python\nconfig = sp.from_dict({'_target_': 'str.lower'})\nfn = sp.init.later(config, Callable[..., str])\n\n... # much computation occurs\n\n# returns `this to lowercase`\nfn('THIS TO LOWERCASE')\n```\n\nNote that, for convenience `sp.init.now` is aliased to `sp.init`.\n\n### Providing Class Paths as `_target_`\n\nIf, for some reason, cannot specify the path to a class as a string, you can use `sp.Target.to_string` to resolve a function, class, or method to its path:\n\n```python\nimport transformers\n\n@sp.dataclass\nclass ModelConfig:\n    _target_: str = sp.Target.to_string(\n        transformers.\n        AutoModelForSequenceClassification.\n        from_pretrained\n    )\n    pretrained_model_name_or_path: str = \\\n        'bert-base-uncased'\n    num_classes: int = 2\n```\n\n## Static and Dynamic Type Checking\n\nSprings supports both static and dynamic (at runtime) type checking when initializing objects.\nTo enable it, pass the expected return type when initializing an object:\n\n```python\n@sp.cli(TokenizerConfig)\ndef main(config: TokenizerConfig):\n    tokenizer = sp.init(\n        config, PreTrainedTokenizerBase\n    )\n    print(tokenizer)\n```\n\nThis will raise an error when the tokenizer is not a subclass of `PreTrainedTokenizerBase`. Further, if you use a static type checker in your workflow (e.g., [Pylance][3] in [Visual Studio Code][4]), `springs.init` will also annotate its return type accordingly.\n\n## Flexible Configurations\n\nSometimes a configuration has some default parameters, but others are optional and depend on other factors, such as the `_target_` class.  In these cases, it is convenient to set up a flexible dataclass, using `make_flexy` after the `dataclass` decorator.\n\n```python\n@sp.flexyclass\nclass MetricConfig:\n    _target_: str = sp.MISSING\n    average: str = 'macro'\n\nconfig = sp.from_dataclass(MetricConfig)\noverrides = {\n    # we override the _target_\n    '_target_': 'torchmetrics.F1Score',\n    # this attribute does not exist in the\n    # structured config\n    'num_classes': 2\n}\n\nconfig = sp.merge(config,\n                  sp.from_dict(overrides))\nprint(config)\n# this will print the following:\n# {\n#    '_target_': 'torchmetrics.F1Score',\n#    'average': 'macro',\n#    'num_classes': 2\n# }\n```\n\n## Adding Help Messages to Configurations\n\nSprings supports adding help messages to your configurations; this is useful to provide a description of parameters to users of your application.\n\nTo add a help for a parameter, simply pass `help=...` to a field constructor:\n\n```python\n@sp.dataclass\nclass ModelConfig:\n    _target_: str = sp.field(\n        default='transformers.'\\\n            'AutoModelForSequenceClassification.'\\\n            'from_pretrained',\n        help='The class for the model'\n    )\n    pretrained_model_name_or_path: str = sp.field(\n        default='bert-base-uncased',\n        help='The name of the model to use'\n    )\n    num_classes: int = sp.field(\n        default=2,\n        help='The number of classes for classification'\n    )\n\n@sp.dataclass\nclass ExperimentConfig:\n    model: ModelConfig = sp.field(\n        default_factory=ModelConfig,\n        help='The model configuration'\n    )\n\n@sp.cli(ExperimentConfig)\ndef main(config: ExperimentConfig):\n    ...\n\nif __name__ == '__main__':\n    main()\n```\n\nAs you can see, you can also add help messages to nested configurations. Help messages are printed when you call `python my_script.py --options` or `python my_script.py --o`.\n\n## Command Line Options\n\nYou can print all command line options by calling `python my_script.py -h` or `python my_script.py --help`. Currently, a Springs application supports the following:\n\n|Flag | Default | Action | Description|\n|----:|:-------:|:------:|:-----------|\n|`-h/--help` | `False` | toggle | show the help message and exit|\n|`-c/--config` | `[]` | append | either a path to a YAML file containing a configuration, or a nickname for a configuration in the registry; multiple configurations can be specified with additional `-c/--config` flags, and they will be merged in the order they are provided|\n|`-o/--options` | `False` | toggle | print all default options and CLI flags|\n|`-i/--inputs` | `False` | toggle | print the input configuration|\n|`-p/--parsed` | `False` | toggle | print the parsed configuration|\n|`-l/--log-level` | `WARNING` | set | logging level to use for this program; can be one of `CRITICAL`, `ERROR`, `WARNING`, `INFO`, or `DEBUG`; defaults to `WARNING`.|\n|`-d/--debug` | `False` |toggle| enable debug mode; equivalent to `--log-level DEBUG`|\n|`-q/--quiet` | `False` |toggle| if provided, it does not print the configuration when running|\n|`-r/--resolvers` | `False` |toggle| print all registered resolvers in OmegaConf, Springs, and current codebase|\n|`-n/--nicknames` | `False` |toggle| print all registered nicknames in Springs|\n|`-s/--save` | `None` | set | save the configuration to a YAML file and exit|\n\n## Tips and Tricks\n\nThis section includes a bunch of tips and tricks for working with OmegaConf and YAML.\n\n### Tip 1: Repeating nodes in YAML input\n\nIn setting up YAML configuration files for ML experiments, it is common to\nhave almost-repeated sections.\nIn these cases, you can take advantage of YAML's built in variable mechanism and dictionary merging to remove duplicated imports:\n\n```yaml\n# &tc assigns an alias to this node\ntrain_config: &tc\n  path: /path/to/data\n  src_field: full_text\n  tgt_field: summary\n  split_name: train\n\ntest_config:\n  # << operator indicates merging,\n  # *tc is a reference to the alias above\n  << : *tc\n  split_name: test\n```\n\nThis will resolve to:\n\n```yaml\ntrain_config:\n  path: /path/to/data\n  split_name: train\n  src_field: full_text\n  tgt_field: summary\n\ntest_config:\n  path: /path/to/data\n  split_name: test\n  src_field: full_text\n  tgt_field: summary\n```\n\n\n[1]: https://omegaconf.readthedocs.io/\n[2]: https://hydra.cc/\n[3]: https://devblogs.microsoft.com/python/announcing-pylance-fast-feature-rich-language-support-for-python-in-visual-studio-code/\n[4]: https://code.visualstudio.com\n[5]: https://openai.com/dall-e-2/\n[6]: https://pypi.org/project/springs/\n",
    "bugtrack_url": null,
    "license": "Apache-2.0",
    "summary": "A set of utilities to create and manage typed configuration files effectively, built on top of OmegaConf.",
    "version": "1.13.0",
    "project_urls": {
        "Homepage": "https://springs.soldaini.net",
        "Source": "https://github.com/soldni/Springs",
        "Tracker": "https://github.com/soldni/Springs/issues"
    },
    "split_keywords": [
        "configuration",
        "yaml",
        "json",
        "command line",
        "cli",
        "omegaconf",
        "structured",
        "config"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "50656f45fb5907742d259bc5863bde5c02699fd9cf47f50fde2041fd171b9abb",
                "md5": "5173809e2c5365795c1083ed5cb9c8ac",
                "sha256": "9bf6a28ce5dd5d7aff0566520f6b1ba8d747812274a02b9ec3e34ae256db7236"
            },
            "downloads": -1,
            "filename": "springs-1.13.0-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "5173809e2c5365795c1083ed5cb9c8ac",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.8",
            "size": 42059,
            "upload_time": "2023-08-02T06:41:00",
            "upload_time_iso_8601": "2023-08-02T06:41:00.586876Z",
            "url": "https://files.pythonhosted.org/packages/50/65/6f45fb5907742d259bc5863bde5c02699fd9cf47f50fde2041fd171b9abb/springs-1.13.0-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "293ac6cd6f308255f810a7638497b1ae339345403c210d2d06e3dc437eb93d75",
                "md5": "4c2c3f3b65383d4bf08796ba7c49db5e",
                "sha256": "9232e64504f55ea5d8a0666ad7e44db2aee277460652fc1317d73ed37369e7d7"
            },
            "downloads": -1,
            "filename": "springs-1.13.0.tar.gz",
            "has_sig": false,
            "md5_digest": "4c2c3f3b65383d4bf08796ba7c49db5e",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.8",
            "size": 45319,
            "upload_time": "2023-08-02T06:41:02",
            "upload_time_iso_8601": "2023-08-02T06:41:02.430968Z",
            "url": "https://files.pythonhosted.org/packages/29/3a/c6cd6f308255f810a7638497b1ae339345403c210d2d06e3dc437eb93d75/springs-1.13.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2023-08-02 06:41:02",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "soldni",
    "github_project": "Springs",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "springs"
}
        
Elapsed time: 0.09968s