dispatcher-enum


Namedispatcher-enum JSON
Version 0.1.1 PyPI version JSON
download
home_pagehttps://github.com/asemic-horizon/dispatcher
SummaryConfig-file strategy pattern enabler: easily create Pydantic-friendly Enums with a function to call for each member
upload_time2024-06-15 19:10:20
maintainerNone
docs_urlNone
authorDiego Navarro
requires_python>=3.6
licenseNone
keywords
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # ͱ Start with the "why" 

DispatchEnum is *the* Pythonic way to deal with the "strategy in config" pattern, where
we want choices in implementation details ("strategies") to be available outside 
Python code proper.

Consider this cfg file:
```yaml
aggregation: mean
length: square
```

We see this typically when we want to allow for different aggregating functions (mean, median...) to be
used in a functionality that meaningfully accepts them.

## Not good

```py
from numpy import mean, median, abs # rock the global namespace!
import yaml

square = lambda x: x*x

def excess(lst, cfg):
    agg = eval(cfg['aggregation'])(lst) # OUCH executable YAML
    return [eval(cfg['length'])(val - agg) for val in lst] # OOF right in the feels

cfg = yaml.safe_load(config.yaml)
print(excess([1,2,3], cfg)) # prints [1,0,1] 
```

## Much better, but still hella wobbly

```py
import numpy as np, yaml

agg_dispatcher = {"mean": np.mean, "median": np.median}
len_dispatcher = {"square": lambda x: x*x, "abs": np.abs}

def excess(lst, cfg):
    agg = agg_dispatcher[cfg['aggregation']](lst)
    return [len_dispatcher[cfg['length']](val - agg) for val in lst]
cfg = yaml.safe_load(config.yaml)
print(excess([1,2,3], cfg)) # same as above 
```

## Safer with Pydantic but drowning in boilerplate

```py
import numpy as np, yaml
from pydantic import BaseModel, field_validator

agg_dispatcher = {"mean": np.mean, "median": np.median}
len_dispatcher = {"square": lambda x: x*x, "abs": np.abs}

class Config(BaseModel):
    aggregation: str
    length: str

    @field_validator('aggregation')
    @classmethod
    def agg_must_be_valid(cls, v: str) -> str:
        if v not in agg_dispatcher:
            raise ValueError('Invalid aggregation')
        return v

    @field_validator('length')
    @classmethod
    def len_must_be_valid(cls, v: str) -> str:
        if v not in len_dispatcher:
            raise ValueError('Invalid length')
        return v

def excess(lst, cfg):
    agg = agg_dispatcher[cfg.aggregation](lst)
    return [len_dispatcher[cfg.length](val - agg) for val in lst]

cfg = yaml.safe_load(config.yaml)
print(excess([1,2,3], cfg)) # same as above 

```

## Class and quality
```py
import numpy as np, yaml
from pydantic import BaseModel
from dispatcher import Dispatcher

# shortcut utility that creates a DispatchEnum object
AggregationStrategy = Dispatcher(
    mean = np.mean,
    median = np.median
)
LengthStrategy = Dispatcher(
    square = lambda x: x*x,
    abs = np.abs
) 
class Config:
    aggregation: AggregationStrategy = AggregationStrategy.MEAN
    length:  LengthStrategy 

cfg = Config(yaml.safe_load(config.yaml))
def excess(lst, cfg):
    agg = cfg.aggregation(lst)   # ding ding ding ding
    return [cfg.length(val - agg) for val in lst]

```

# The "what"

This code provides a `DispatchEnum` class that subclasses from Enum but holds an
additional value for each member. This is most useful in combination with Pydantic,
which is able to parse Enum-valued fields received as strings, i.e.

```py
class Parity(Enum):
    ODD = "odd"
    EVEN = "even"

class Parser(BaseModel):
     check_parity: Parity

cfg = Parser({"check_parity": "odd" })
print(cfg.check_parity) # prints Parity.ODD
```

With `DispatchEnum` we're able to assign an additional property to each Enum member:

```py
class Parity(DispatchEnum):
    ODD = "odd"
    EVEN = "even"

Parity.from_dict({"ODD": lambda x: x % 2 == 1, "EVEN": lambda x: x % 2 == 0})
print(Parity.ODD(2)) # prints False
```

Therefore `DispatchEnum`is both a "dispatcher" (mapping a string identifier to a function)
and an `Enum` (enabling Pydantic goodness).

For further convenience, the `Dispatcher` function creates a DispatchEnum filling in member names:

```py
AggregationStrategy = Dispatcher(
    mean = np.mean,
    median = np.median
)
```
which is shorthand for 
```py
class AggregationStrategy(DispatchEnum):
    MEAN: "mean"
    MEDIAN: "median"
AggregationStrategy.from_dict({"mean": np.mean, "median": np.median})
```

# Installation

Right now you should download `dispatch.py` and vendor it in. Soonishly a more mature
version will be hitting PyPI too.


            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/asemic-horizon/dispatcher",
    "name": "dispatcher-enum",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.6",
    "maintainer_email": null,
    "keywords": null,
    "author": "Diego Navarro",
    "author_email": "the.electric.me@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/e3/7c/51ace662361b79ebe15ede87af8cce903bd2045225c88bc14725b86deb86/dispatcher-enum-0.1.1.tar.gz",
    "platform": null,
    "description": "# \u0371 Start with the \"why\" \n\nDispatchEnum is *the* Pythonic way to deal with the \"strategy in config\" pattern, where\nwe want choices in implementation details (\"strategies\") to be available outside \nPython code proper.\n\nConsider this cfg file:\n```yaml\naggregation: mean\nlength: square\n```\n\nWe see this typically when we want to allow for different aggregating functions (mean, median...) to be\nused in a functionality that meaningfully accepts them.\n\n## Not good\n\n```py\nfrom numpy import mean, median, abs # rock the global namespace!\nimport yaml\n\nsquare = lambda x: x*x\n\ndef excess(lst, cfg):\n    agg = eval(cfg['aggregation'])(lst) # OUCH executable YAML\n    return [eval(cfg['length'])(val - agg) for val in lst] # OOF right in the feels\n\ncfg = yaml.safe_load(config.yaml)\nprint(excess([1,2,3], cfg)) # prints [1,0,1] \n```\n\n## Much better, but still hella wobbly\n\n```py\nimport numpy as np, yaml\n\nagg_dispatcher = {\"mean\": np.mean, \"median\": np.median}\nlen_dispatcher = {\"square\": lambda x: x*x, \"abs\": np.abs}\n\ndef excess(lst, cfg):\n    agg = agg_dispatcher[cfg['aggregation']](lst)\n    return [len_dispatcher[cfg['length']](val - agg) for val in lst]\ncfg = yaml.safe_load(config.yaml)\nprint(excess([1,2,3], cfg)) # same as above \n```\n\n## Safer with Pydantic but drowning in boilerplate\n\n```py\nimport numpy as np, yaml\nfrom pydantic import BaseModel, field_validator\n\nagg_dispatcher = {\"mean\": np.mean, \"median\": np.median}\nlen_dispatcher = {\"square\": lambda x: x*x, \"abs\": np.abs}\n\nclass Config(BaseModel):\n    aggregation: str\n    length: str\n\n    @field_validator('aggregation')\n    @classmethod\n    def agg_must_be_valid(cls, v: str) -> str:\n        if v not in agg_dispatcher:\n            raise ValueError('Invalid aggregation')\n        return v\n\n    @field_validator('length')\n    @classmethod\n    def len_must_be_valid(cls, v: str) -> str:\n        if v not in len_dispatcher:\n            raise ValueError('Invalid length')\n        return v\n\ndef excess(lst, cfg):\n    agg = agg_dispatcher[cfg.aggregation](lst)\n    return [len_dispatcher[cfg.length](val - agg) for val in lst]\n\ncfg = yaml.safe_load(config.yaml)\nprint(excess([1,2,3], cfg)) # same as above \n\n```\n\n## Class and quality\n```py\nimport numpy as np, yaml\nfrom pydantic import BaseModel\nfrom dispatcher import Dispatcher\n\n# shortcut utility that creates a DispatchEnum object\nAggregationStrategy = Dispatcher(\n    mean = np.mean,\n    median = np.median\n)\nLengthStrategy = Dispatcher(\n    square = lambda x: x*x,\n    abs = np.abs\n) \nclass Config:\n    aggregation: AggregationStrategy = AggregationStrategy.MEAN\n    length:  LengthStrategy \n\ncfg = Config(yaml.safe_load(config.yaml))\ndef excess(lst, cfg):\n    agg = cfg.aggregation(lst)   # ding ding ding ding\n    return [cfg.length(val - agg) for val in lst]\n\n```\n\n# The \"what\"\n\nThis code provides a `DispatchEnum` class that subclasses from Enum but holds an\nadditional value for each member. This is most useful in combination with Pydantic,\nwhich is able to parse Enum-valued fields received as strings, i.e.\n\n```py\nclass Parity(Enum):\n    ODD = \"odd\"\n    EVEN = \"even\"\n\nclass Parser(BaseModel):\n     check_parity: Parity\n\ncfg = Parser({\"check_parity\": \"odd\" })\nprint(cfg.check_parity) # prints Parity.ODD\n```\n\nWith `DispatchEnum` we're able to assign an additional property to each Enum member:\n\n```py\nclass Parity(DispatchEnum):\n    ODD = \"odd\"\n    EVEN = \"even\"\n\nParity.from_dict({\"ODD\": lambda x: x % 2 == 1, \"EVEN\": lambda x: x % 2 == 0})\nprint(Parity.ODD(2)) # prints False\n```\n\nTherefore `DispatchEnum`is both a \"dispatcher\" (mapping a string identifier to a function)\nand an `Enum` (enabling Pydantic goodness).\n\nFor further convenience, the `Dispatcher` function creates a DispatchEnum filling in member names:\n\n```py\nAggregationStrategy = Dispatcher(\n    mean = np.mean,\n    median = np.median\n)\n```\nwhich is shorthand for \n```py\nclass AggregationStrategy(DispatchEnum):\n    MEAN: \"mean\"\n    MEDIAN: \"median\"\nAggregationStrategy.from_dict({\"mean\": np.mean, \"median\": np.median})\n```\n\n# Installation\n\nRight now you should download `dispatch.py` and vendor it in. Soonishly a more mature\nversion will be hitting PyPI too.\n\n",
    "bugtrack_url": null,
    "license": null,
    "summary": "Config-file strategy pattern enabler: easily create Pydantic-friendly Enums with a function to call for each member",
    "version": "0.1.1",
    "project_urls": {
        "Homepage": "https://github.com/asemic-horizon/dispatcher"
    },
    "split_keywords": [],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "e37c51ace662361b79ebe15ede87af8cce903bd2045225c88bc14725b86deb86",
                "md5": "dbf20fecd450afa1b83100d731af4a2a",
                "sha256": "264d463f060ec77ab766157f98506fd083a09690099e3240b813a142e96d70b9"
            },
            "downloads": -1,
            "filename": "dispatcher-enum-0.1.1.tar.gz",
            "has_sig": false,
            "md5_digest": "dbf20fecd450afa1b83100d731af4a2a",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.6",
            "size": 7367,
            "upload_time": "2024-06-15T19:10:20",
            "upload_time_iso_8601": "2024-06-15T19:10:20.093432Z",
            "url": "https://files.pythonhosted.org/packages/e3/7c/51ace662361b79ebe15ede87af8cce903bd2045225c88bc14725b86deb86/dispatcher-enum-0.1.1.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-06-15 19:10:20",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "asemic-horizon",
    "github_project": "dispatcher",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": false,
    "lcname": "dispatcher-enum"
}
        
Elapsed time: 0.50801s