hookedin


Namehookedin JSON
Version 1.0.0 PyPI version JSON
download
home_pageNone
SummaryLightweight async-friendly hook/plug-in manager with tags, priorities, and reducers.
upload_time2025-08-26 18:44:29
maintainerNone
docs_urlNone
authorNone
requires_python>=3.9
licenseMIT License Copyright (c) 2025 Kevin d'Anunciacao Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
keywords asyncio events hooks middleware plugins
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # Hookedin

Lightweight, async‑friendly hook and plug‑in manager with tags, priorities, and reducer‑based result merging. Designed for clean composition of middleware, event pipelines, and extension points.

> Batteries included: decorators and programmatic registration, tag filtering, stable priority ordering, async concurrency, looped handlers, reducer‑based dict merges, metrics sink, and easy introspection.

---

## Installation

```bash
pip install hookedin
```

For development:

```bash
pip install -e .[dev]
pytest -v
```

Python 3.9 or newer.

---

## Quick start

```python
from hookedin import get_hook_manager, Behavior, get_reducer_manager

h = get_hook_manager()  # get a manager instance

# Register with a decorator
@h.on("message", tags=["audit"], priority=10)
def audit(ctx=None):
    # ctx is the dict payload for dict inputs
    return {"seen": True, "shared": 1}

# Register async handler
@h.on("message", priority=0)
async def do_work(ctx=None):
    # this one runs before audit due to priority=0
    return {"ok": True, "shared": 2}

# Trigger in parallel and merge dicts using a reducer
result = await h.trigger(
    "message",
    payload={"start": True, "shared": 0},
    parallel=True,
    reducer="last_wins",   # or "first_wins", "sum_numbers", or a custom reducer
)
print(result)  # {'start': True, 'seen': True, 'ok': True, 'shared': 2}
```

> Prefer `trigger()` when you want a final dict. Use `gather()` to get a list of detailed results. Use `fire()` for fire‑and‑forget semantics.

---

## Core concepts

### Hooks and handlers

* A **hook** is a named event channel like `"on_connect"` or `"message"`.
* A **handler** is any sync or async callable you register to a hook.
* Register with a decorator or programmatically.

```python
# decorator
@h.on("reg", tags=["red"], priority=1)
def decorated(payload=None):
    return "ok"

# programmatic
async def async_handler(ctx=None):
    return {"mark": "async"}

tok = h.add(async_handler, "reg", priority=0)
```

Each registration returns a **token** you can use to manage the entry later.

### Priority and order

* Lower numeric priority runs earlier.
* Equal priorities are stable by registration sequence.

```python
order = []

@h.on("prio", priority=0)
def a(payload=None): order.append("a")

@h.on("prio", priority=0)
def b(payload=None): order.append("b")

await h.fire("prio")
assert order == ["a", "b"]

# Raise b to the front
h.change_priority(h.token_of(b), -10)
order.clear(); await h.fire("prio")
assert order == ["b", "a"]
```

### Tags and filtering

* Handlers can have zero or more string **tags**.
* When firing, you can filter by tags and include or exclude untagged handlers.

```python
@h.on("t", tags=["red", "fast"])  
@h.on("t", tags=["red"])  
@h.on("t")  # untagged
async def _(...): ...

# Only tag‑matched
await h.gather("t", tags={"red"}, include_untagged=False)

# Tag‑matched plus untagged
await h.gather("t", tags={"red"}, include_untagged=True)

# Update tags later
tok = h.token_of(_)
h.add_tags(tok, ["red"])   # now matches
h.remove_tags(tok, ["red"]) # no longer matches
```

### Execution modes

* `fire()` – run handlers without collecting values. Errors bubble only if `strict=True`.
* `gather()` – run handlers and collect `HookResult` objects with `.value`, `.ok`, `.error`, `.elapsed_ms`, and `.entry`.
* `trigger()` – run handlers and merge the dict outputs into a single dict using a reducer. This is ideal for middleware‑style edits.

```python
# Strict error propagation
@h.on("boom")
def boom(payload=None):
    raise RuntimeError("boom")

with pytest.raises(RuntimeError):
    await h.fire("boom", strict=True)
```

#### Common keyword arguments for execution methods

* `payload` – The data passed to handlers. If it’s a `dict`, handlers receive it as `ctx`. Otherwise it is passed as `payload`.
* `tags` – A set of tags to filter which handlers run.
* `include_untagged` – Whether to include untagged handlers when filtering by tags (default `True`).
* `parallel` – If `True`, handlers run concurrently instead of sequentially.
* `strict` – If `True`, exceptions in handlers are re‑raised immediately; otherwise errors are captured in the result objects.
* `unique_inputs` – If `True` (only valid with `parallel=True`), each handler receives its own copy of the payload, preventing shared mutation.
* `reducer` – Only for `trigger()`. Chooses how multiple dict results are merged (`last_wins`, `first_wins`, `sum_numbers`, or custom).
* `**extra` – Any additional keyword arguments are forwarded to handlers as named arguments, making it easy to inject context like `user_id=123`.

This flexibility makes `fire`, `gather`, and `trigger` suitable for a wide range of use cases, from simple event dispatch to complex middleware pipelines.


### Parallelism and reducers

* Set `parallel=True` to run handlers concurrently.
* Choose how dict outputs merge:

  * `"last_wins"` – later handlers override earlier ones
  * `"first_wins"` – first value wins
  * `"sum_numbers"` – numeric values are summed, others use last wins
* Provide a custom reducer as a name you registered or as a callable.

```python
mgr = get_reducer_manager()

def my_merge(base: dict, edits: list[dict]) -> dict:
    out = base.copy()
    out["sum_b"] = sum(d.get("b", 0) for d in edits)
    return out

mgr.register_reducer("my_merge", my_merge, overwrite=True)

merged = await h.trigger("red", payload={"b": 0}, parallel=True, reducer="my_merge")
```

### Payload passing

* If the payload is a dict, it is given to handlers as `ctx` to encourage structured edits.
* If the payload is not a dict, it is passed as `payload`.
* Set `unique_inputs=True` with `parallel=True` to give each handler its own deep copy so the caller’s input is not mutated by handlers.

```python
def uses_ctx(ctx=None):  # receives dict
    ctx["mutated"] = True

# sequential allows mutation of the original dict
original = {"k": 1}
await h.trigger("u", payload=original, parallel=False)
assert "mutated" in original

# parallel unique inputs preserve the caller’s dict
original2 = {"k": 2}
await h.trigger("u2", payload=original2, parallel=True, unique_inputs=True)
assert "mutated" not in original2
```

### Looped handlers

* Handlers can run in a loop with `behavior=Behavior.LOOP` and an `interval` in seconds.
* Start and stop loops across the manager. You can toggle or reschedule a loop by token.

```python
@h.on("heartbeat", behavior=Behavior.LOOP, interval=0.50)
def tick(payload=None):
    print("tick")

await h.start_loops()
...
# pause then resume a specific loop entry
h.toggle(h.token_of(tick), toggle_amounts=1); h.toggle(h.token_of(tick), toggle_amounts=1)
# change its interval
h.reschedule(h.token_of(tick), 0.25)
...
await h.stop_loops()
```

### Introspection and management

* `token_of(fn)` – get the token of a decorated handler.
* `has(token)` – check presence.
* `remove(token)` or `remove_by_callback(fn)` – remove handlers.
* `list_entries(name)` – get entries registered to a hook.
* `find_tokens(name)` – get tokens under a hook.
* `count(name, tags={...})` – count entries by hook and optional tags.
* `info(token, debug=False)` – view a dict of entry properties.

---

## Metrics

Provide a sink to observe each handler’s timings and outcome. Great for logging and dashboards.

```python
stats = []

def sink(m):
    stats.append({
        "hook_name": m.hook_name,
        "ok": m.ok,
        "elapsed_ms": m.elapsed_ms,
        "callback": getattr(m.entry.callback, "__name__", "anon"),
        "error": type(m.error).__name__ if m.error else None,
    })

h.set_metrics_sink(sink)
await h.fire("myhook")
```

The metrics object includes at least: `hook_name`, `entry`, `ok`, `error`, `elapsed_ms`.

---

## API summary

> Signatures are shown in a friendly form. Types may be more specific in code.

**Registration**

* `on(hook_name, *, tags=None, priority=0, behavior=Behavior.DEFAULT, interval=None, on_fail=None)` – decorator
* `add(callback, hook_name, *, tags=None, priority=0, behavior=Behavior.DEFAULT, interval=None, on_fail=None) -> token`

**Execution**

* `fire(name, *, payload=None, tags=None, include_untagged=True, parallel=False, strict=False, **extra)`
* `gather(name, *, payload=None, tags=None, include_untagged=True, parallel=False, strict=False, unique_inputs=None, **extra) -> list[HookResult]`
* `trigger(name, *, payload: dict, tags=None, include_untagged=True, parallel=False, reducer="last_wins", unique_inputs=None, **extra) -> dict`

**Reducers**

* Built‑ins: `"last_wins"`, `"first_wins"`, `"sum_numbers"`
* `get_reducer_manager().register_reducer(name, func, overwrite=False)`

**Looping**

* `start_loops()` – start all loop entries
* `stop_loops()` – stop them
* `toggle(token, toggle_amounts=1)` – enable or disable a specific entry
* `reschedule(token, interval)` – change loop interval

**Introspection and edit**

* `token_of(callback) -> token | None`
* `has(token) -> bool`
* `remove(token) -> bool`
* `remove_by_callback(callback) -> bool`
* `change_priority(token, new_priority) -> bool`
* `add_tags(token, tags: Iterable[str]) -> bool`
* `remove_tags(token, tags: Iterable[str]) -> bool`
* `list_entries(hook_name) -> list[Entry]`
* `find_tokens(hook_name) -> list[token]`
* `count(hook_name, tags=None) -> int`
* `info(token, debug=False) -> dict`

**Metrics**

* `set_metrics_sink(callable)` – receive per‑handler timing and status

**Factory and module exports**

* `get_hook_manager()` – create or return a manager instance
* `Behavior` – behaviors: `DEFAULT`, `ONESHOT`, `LOOP`
* `get_reducer_manager()` – reducer registry for merges

---

## Custom reducers

Reducers take `(base: dict, edits: list[dict]) -> dict` and return a new dict.

```python
from hookedin import get_reducer_manager

mgr = get_reducer_manager()

def only_truthy(base, edits):
    out = base.copy()
    for d in edits:
        for k, v in d.items():
            if v:
                out[k] = v
    return out

mgr.register_reducer("only_truthy", only_truthy, overwrite=True)
```

Use by name in `trigger(..., reducer="only_truthy")` or pass the function.

---

## Error handling

* By default errors are captured in `HookResult.error` and do not stop other handlers.
* Set `strict=True` in `fire()` or `gather()` to re‑raise the first error.
* You can also provide per‑entry `on_fail` callbacks when registering if you prefer local handling.

---

## Patterns

### Middleware style edits

Group ordered steps that progressively transform a dict, then reduce the outputs into a final view.

### Feature flags and tags

Tag handlers with features or environments, then filter at call time.

### Background ticks

Use `Behavior.LOOP` for lightweight heartbeats. Example: emit periodic metrics or refresh caches.

---

## Versioning and stability

* Follows semver. Breaking changes increase the major version.
* No runtime dependencies.

---

## Contributing

Issues and pull requests are welcome. Please include tests where possible.

---

## Contact

Created by [Kevin d'Anunciacao](mailto:kmdjr.dev@gmail.com).  
Feel free to reach out via email or open an issue on [GitHub](https://github.com/Kmdjr/hookedin).

---

## License

MIT License. See `LICENSE` for details.

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "hookedin",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.9",
    "maintainer_email": null,
    "keywords": "asyncio, events, hooks, middleware, plugins",
    "author": null,
    "author_email": "Kevin d'Anunciacao <kmdjr.dev@gmail.com>",
    "download_url": "https://files.pythonhosted.org/packages/75/c4/4b7257c2559688275318f8ce0d260ec8447013a32c01ea281f642fe8429a/hookedin-1.0.0.tar.gz",
    "platform": null,
    "description": "# Hookedin\n\nLightweight, async\u2011friendly hook and plug\u2011in manager with tags, priorities, and reducer\u2011based result merging. Designed for clean composition of middleware, event pipelines, and extension points.\n\n> Batteries included: decorators and programmatic registration, tag filtering, stable priority ordering, async concurrency, looped handlers, reducer\u2011based dict merges, metrics sink, and easy introspection.\n\n---\n\n## Installation\n\n```bash\npip install hookedin\n```\n\nFor development:\n\n```bash\npip install -e .[dev]\npytest -v\n```\n\nPython 3.9 or newer.\n\n---\n\n## Quick start\n\n```python\nfrom hookedin import get_hook_manager, Behavior, get_reducer_manager\n\nh = get_hook_manager()  # get a manager instance\n\n# Register with a decorator\n@h.on(\"message\", tags=[\"audit\"], priority=10)\ndef audit(ctx=None):\n    # ctx is the dict payload for dict inputs\n    return {\"seen\": True, \"shared\": 1}\n\n# Register async handler\n@h.on(\"message\", priority=0)\nasync def do_work(ctx=None):\n    # this one runs before audit due to priority=0\n    return {\"ok\": True, \"shared\": 2}\n\n# Trigger in parallel and merge dicts using a reducer\nresult = await h.trigger(\n    \"message\",\n    payload={\"start\": True, \"shared\": 0},\n    parallel=True,\n    reducer=\"last_wins\",   # or \"first_wins\", \"sum_numbers\", or a custom reducer\n)\nprint(result)  # {'start': True, 'seen': True, 'ok': True, 'shared': 2}\n```\n\n> Prefer `trigger()` when you want a final dict. Use `gather()` to get a list of detailed results. Use `fire()` for fire\u2011and\u2011forget semantics.\n\n---\n\n## Core concepts\n\n### Hooks and handlers\n\n* A **hook** is a named event channel like `\"on_connect\"` or `\"message\"`.\n* A **handler** is any sync or async callable you register to a hook.\n* Register with a decorator or programmatically.\n\n```python\n# decorator\n@h.on(\"reg\", tags=[\"red\"], priority=1)\ndef decorated(payload=None):\n    return \"ok\"\n\n# programmatic\nasync def async_handler(ctx=None):\n    return {\"mark\": \"async\"}\n\ntok = h.add(async_handler, \"reg\", priority=0)\n```\n\nEach registration returns a **token** you can use to manage the entry later.\n\n### Priority and order\n\n* Lower numeric priority runs earlier.\n* Equal priorities are stable by registration sequence.\n\n```python\norder = []\n\n@h.on(\"prio\", priority=0)\ndef a(payload=None): order.append(\"a\")\n\n@h.on(\"prio\", priority=0)\ndef b(payload=None): order.append(\"b\")\n\nawait h.fire(\"prio\")\nassert order == [\"a\", \"b\"]\n\n# Raise b to the front\nh.change_priority(h.token_of(b), -10)\norder.clear(); await h.fire(\"prio\")\nassert order == [\"b\", \"a\"]\n```\n\n### Tags and filtering\n\n* Handlers can have zero or more string **tags**.\n* When firing, you can filter by tags and include or exclude untagged handlers.\n\n```python\n@h.on(\"t\", tags=[\"red\", \"fast\"])  \n@h.on(\"t\", tags=[\"red\"])  \n@h.on(\"t\")  # untagged\nasync def _(...): ...\n\n# Only tag\u2011matched\nawait h.gather(\"t\", tags={\"red\"}, include_untagged=False)\n\n# Tag\u2011matched plus untagged\nawait h.gather(\"t\", tags={\"red\"}, include_untagged=True)\n\n# Update tags later\ntok = h.token_of(_)\nh.add_tags(tok, [\"red\"])   # now matches\nh.remove_tags(tok, [\"red\"]) # no longer matches\n```\n\n### Execution modes\n\n* `fire()` \u2013 run handlers without collecting values. Errors bubble only if `strict=True`.\n* `gather()` \u2013 run handlers and collect `HookResult` objects with `.value`, `.ok`, `.error`, `.elapsed_ms`, and `.entry`.\n* `trigger()` \u2013 run handlers and merge the dict outputs into a single dict using a reducer. This is ideal for middleware\u2011style edits.\n\n```python\n# Strict error propagation\n@h.on(\"boom\")\ndef boom(payload=None):\n    raise RuntimeError(\"boom\")\n\nwith pytest.raises(RuntimeError):\n    await h.fire(\"boom\", strict=True)\n```\n\n#### Common keyword arguments for execution methods\n\n* `payload` \u2013 The data passed to handlers. If it\u2019s a `dict`, handlers receive it as `ctx`. Otherwise it is passed as `payload`.\n* `tags` \u2013 A set of tags to filter which handlers run.\n* `include_untagged` \u2013 Whether to include untagged handlers when filtering by tags (default `True`).\n* `parallel` \u2013 If `True`, handlers run concurrently instead of sequentially.\n* `strict` \u2013 If `True`, exceptions in handlers are re\u2011raised immediately; otherwise errors are captured in the result objects.\n* `unique_inputs` \u2013 If `True` (only valid with `parallel=True`), each handler receives its own copy of the payload, preventing shared mutation.\n* `reducer` \u2013 Only for `trigger()`. Chooses how multiple dict results are merged (`last_wins`, `first_wins`, `sum_numbers`, or custom).\n* `**extra` \u2013 Any additional keyword arguments are forwarded to handlers as named arguments, making it easy to inject context like `user_id=123`.\n\nThis flexibility makes `fire`, `gather`, and `trigger` suitable for a wide range of use cases, from simple event dispatch to complex middleware pipelines.\n\n\n### Parallelism and reducers\n\n* Set `parallel=True` to run handlers concurrently.\n* Choose how dict outputs merge:\n\n  * `\"last_wins\"` \u2013 later handlers override earlier ones\n  * `\"first_wins\"` \u2013 first value wins\n  * `\"sum_numbers\"` \u2013 numeric values are summed, others use last wins\n* Provide a custom reducer as a name you registered or as a callable.\n\n```python\nmgr = get_reducer_manager()\n\ndef my_merge(base: dict, edits: list[dict]) -> dict:\n    out = base.copy()\n    out[\"sum_b\"] = sum(d.get(\"b\", 0) for d in edits)\n    return out\n\nmgr.register_reducer(\"my_merge\", my_merge, overwrite=True)\n\nmerged = await h.trigger(\"red\", payload={\"b\": 0}, parallel=True, reducer=\"my_merge\")\n```\n\n### Payload passing\n\n* If the payload is a dict, it is given to handlers as `ctx` to encourage structured edits.\n* If the payload is not a dict, it is passed as `payload`.\n* Set `unique_inputs=True` with `parallel=True` to give each handler its own deep copy so the caller\u2019s input is not mutated by handlers.\n\n```python\ndef uses_ctx(ctx=None):  # receives dict\n    ctx[\"mutated\"] = True\n\n# sequential allows mutation of the original dict\noriginal = {\"k\": 1}\nawait h.trigger(\"u\", payload=original, parallel=False)\nassert \"mutated\" in original\n\n# parallel unique inputs preserve the caller\u2019s dict\noriginal2 = {\"k\": 2}\nawait h.trigger(\"u2\", payload=original2, parallel=True, unique_inputs=True)\nassert \"mutated\" not in original2\n```\n\n### Looped handlers\n\n* Handlers can run in a loop with `behavior=Behavior.LOOP` and an `interval` in seconds.\n* Start and stop loops across the manager. You can toggle or reschedule a loop by token.\n\n```python\n@h.on(\"heartbeat\", behavior=Behavior.LOOP, interval=0.50)\ndef tick(payload=None):\n    print(\"tick\")\n\nawait h.start_loops()\n...\n# pause then resume a specific loop entry\nh.toggle(h.token_of(tick), toggle_amounts=1); h.toggle(h.token_of(tick), toggle_amounts=1)\n# change its interval\nh.reschedule(h.token_of(tick), 0.25)\n...\nawait h.stop_loops()\n```\n\n### Introspection and management\n\n* `token_of(fn)` \u2013 get the token of a decorated handler.\n* `has(token)` \u2013 check presence.\n* `remove(token)` or `remove_by_callback(fn)` \u2013 remove handlers.\n* `list_entries(name)` \u2013 get entries registered to a hook.\n* `find_tokens(name)` \u2013 get tokens under a hook.\n* `count(name, tags={...})` \u2013 count entries by hook and optional tags.\n* `info(token, debug=False)` \u2013 view a dict of entry properties.\n\n---\n\n## Metrics\n\nProvide a sink to observe each handler\u2019s timings and outcome. Great for logging and dashboards.\n\n```python\nstats = []\n\ndef sink(m):\n    stats.append({\n        \"hook_name\": m.hook_name,\n        \"ok\": m.ok,\n        \"elapsed_ms\": m.elapsed_ms,\n        \"callback\": getattr(m.entry.callback, \"__name__\", \"anon\"),\n        \"error\": type(m.error).__name__ if m.error else None,\n    })\n\nh.set_metrics_sink(sink)\nawait h.fire(\"myhook\")\n```\n\nThe metrics object includes at least: `hook_name`, `entry`, `ok`, `error`, `elapsed_ms`.\n\n---\n\n## API summary\n\n> Signatures are shown in a friendly form. Types may be more specific in code.\n\n**Registration**\n\n* `on(hook_name, *, tags=None, priority=0, behavior=Behavior.DEFAULT, interval=None, on_fail=None)` \u2013 decorator\n* `add(callback, hook_name, *, tags=None, priority=0, behavior=Behavior.DEFAULT, interval=None, on_fail=None) -> token`\n\n**Execution**\n\n* `fire(name, *, payload=None, tags=None, include_untagged=True, parallel=False, strict=False, **extra)`\n* `gather(name, *, payload=None, tags=None, include_untagged=True, parallel=False, strict=False, unique_inputs=None, **extra) -> list[HookResult]`\n* `trigger(name, *, payload: dict, tags=None, include_untagged=True, parallel=False, reducer=\"last_wins\", unique_inputs=None, **extra) -> dict`\n\n**Reducers**\n\n* Built\u2011ins: `\"last_wins\"`, `\"first_wins\"`, `\"sum_numbers\"`\n* `get_reducer_manager().register_reducer(name, func, overwrite=False)`\n\n**Looping**\n\n* `start_loops()` \u2013 start all loop entries\n* `stop_loops()` \u2013 stop them\n* `toggle(token, toggle_amounts=1)` \u2013 enable or disable a specific entry\n* `reschedule(token, interval)` \u2013 change loop interval\n\n**Introspection and edit**\n\n* `token_of(callback) -> token | None`\n* `has(token) -> bool`\n* `remove(token) -> bool`\n* `remove_by_callback(callback) -> bool`\n* `change_priority(token, new_priority) -> bool`\n* `add_tags(token, tags: Iterable[str]) -> bool`\n* `remove_tags(token, tags: Iterable[str]) -> bool`\n* `list_entries(hook_name) -> list[Entry]`\n* `find_tokens(hook_name) -> list[token]`\n* `count(hook_name, tags=None) -> int`\n* `info(token, debug=False) -> dict`\n\n**Metrics**\n\n* `set_metrics_sink(callable)` \u2013 receive per\u2011handler timing and status\n\n**Factory and module exports**\n\n* `get_hook_manager()` \u2013 create or return a manager instance\n* `Behavior` \u2013 behaviors: `DEFAULT`, `ONESHOT`, `LOOP`\n* `get_reducer_manager()` \u2013 reducer registry for merges\n\n---\n\n## Custom reducers\n\nReducers take `(base: dict, edits: list[dict]) -> dict` and return a new dict.\n\n```python\nfrom hookedin import get_reducer_manager\n\nmgr = get_reducer_manager()\n\ndef only_truthy(base, edits):\n    out = base.copy()\n    for d in edits:\n        for k, v in d.items():\n            if v:\n                out[k] = v\n    return out\n\nmgr.register_reducer(\"only_truthy\", only_truthy, overwrite=True)\n```\n\nUse by name in `trigger(..., reducer=\"only_truthy\")` or pass the function.\n\n---\n\n## Error handling\n\n* By default errors are captured in `HookResult.error` and do not stop other handlers.\n* Set `strict=True` in `fire()` or `gather()` to re\u2011raise the first error.\n* You can also provide per\u2011entry `on_fail` callbacks when registering if you prefer local handling.\n\n---\n\n## Patterns\n\n### Middleware style edits\n\nGroup ordered steps that progressively transform a dict, then reduce the outputs into a final view.\n\n### Feature flags and tags\n\nTag handlers with features or environments, then filter at call time.\n\n### Background ticks\n\nUse `Behavior.LOOP` for lightweight heartbeats. Example: emit periodic metrics or refresh caches.\n\n---\n\n## Versioning and stability\n\n* Follows semver. Breaking changes increase the major version.\n* No runtime dependencies.\n\n---\n\n## Contributing\n\nIssues and pull requests are welcome. Please include tests where possible.\n\n---\n\n## Contact\n\nCreated by [Kevin d'Anunciacao](mailto:kmdjr.dev@gmail.com).  \nFeel free to reach out via email or open an issue on [GitHub](https://github.com/Kmdjr/hookedin).\n\n---\n\n## License\n\nMIT License. See `LICENSE` for details.\n",
    "bugtrack_url": null,
    "license": "MIT License\n        \n        Copyright (c) 2025 Kevin d'Anunciacao\n        \n        Permission is hereby granted, free of charge, to any person obtaining a copy\n        of this software and associated documentation files (the \"Software\"), to deal\n        in the Software without restriction, including without limitation the rights\n        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n        copies of the Software, and to permit persons to whom the Software is\n        furnished to do so, subject to the following conditions:\n        \n        The above copyright notice and this permission notice shall be included in all\n        copies or substantial portions of the Software.\n        \n        THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n        SOFTWARE.",
    "summary": "Lightweight async-friendly hook/plug-in manager with tags, priorities, and reducers.",
    "version": "1.0.0",
    "project_urls": {
        "Homepage": "https://github.com/Kmdjr/hookedin",
        "Issues": "https://github.com/Kmdjr/hookedin/issues"
    },
    "split_keywords": [
        "asyncio",
        " events",
        " hooks",
        " middleware",
        " plugins"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "b2b33713d780e7b66fdca548170d7f7caf42154e05f81aea0f2a9081cfbdd9d8",
                "md5": "9c316243cd825e848827d81fe961ce37",
                "sha256": "decc4cb1b7c3d7af37a77ecc86d0e5b0ad71b26133682cf2d81dae6ccea713a9"
            },
            "downloads": -1,
            "filename": "hookedin-1.0.0-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "9c316243cd825e848827d81fe961ce37",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.9",
            "size": 19870,
            "upload_time": "2025-08-26T18:44:28",
            "upload_time_iso_8601": "2025-08-26T18:44:28.100862Z",
            "url": "https://files.pythonhosted.org/packages/b2/b3/3713d780e7b66fdca548170d7f7caf42154e05f81aea0f2a9081cfbdd9d8/hookedin-1.0.0-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "75c44b7257c2559688275318f8ce0d260ec8447013a32c01ea281f642fe8429a",
                "md5": "9a1e578400f7b73165a5c495873b7ec5",
                "sha256": "78ee22af5b4cf985ed31e14f1683e7aed24a5f1ea22ef5022ec383fb2b6e4a64"
            },
            "downloads": -1,
            "filename": "hookedin-1.0.0.tar.gz",
            "has_sig": false,
            "md5_digest": "9a1e578400f7b73165a5c495873b7ec5",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.9",
            "size": 21121,
            "upload_time": "2025-08-26T18:44:29",
            "upload_time_iso_8601": "2025-08-26T18:44:29.523857Z",
            "url": "https://files.pythonhosted.org/packages/75/c4/4b7257c2559688275318f8ce0d260ec8447013a32c01ea281f642fe8429a/hookedin-1.0.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-08-26 18:44:29",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "Kmdjr",
    "github_project": "hookedin",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": false,
    "lcname": "hookedin"
}
        
Elapsed time: 1.36891s