Metadata-Version: 2.4
Name: gevent-toolbelt
Version: 1.0.12
Summary: Utilities for gevent-based services: graceful shutdown, health checks, StatsD metrics, and testing helpers.
Project-URL: Homepage, https://github.com/adalekin/gevent-toolbelt
Project-URL: Repository, https://github.com/adalekin/gevent-toolbelt
Project-URL: Issues, https://github.com/adalekin/gevent-toolbelt/issues
Author: Alexey Dalekin
License: MIT
License-File: LICENSE
Keywords: gevent,graceful-shutdown,greenlet,healthcheck,statsd
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Distributed Computing
Requires-Python: >=3.10
Requires-Dist: environs>=11.0.0
Requires-Dist: gevent>=23.9.0
Requires-Dist: psutil>=5.6.5
Requires-Dist: statsd>=4.0.1
Requires-Dist: zope-event>=5.0
Description-Content-Type: text/markdown

# gevent-toolbelt

Small utilities for [`gevent`](https://www.gevent.org/) that do not belong in gevent itself: cooperative services, graceful shutdown, optional StatsD health metrics, and test helpers.

## Install

From [PyPI](https://pypi.org/project/gevent-toolbelt/) (after you publish a release):

```sh
pip install gevent-toolbelt
```

With [uv](https://docs.astral.sh/uv/):

```sh
uv add gevent-toolbelt
```

## Requirements

Python 3.10 or newer.

## Table of contents

- [Configuration](#configuration)
- [Usage](#usage)
- [Development](#development)
- [Publishing to PyPI](#publishing-to-pypi)
- [License](#license)

## Configuration

### GEVENT_MAX_BLOCKING_TIME

Default: `5`

If the monitor thread is enabled, this is approximately how long (in seconds) the event loop may block before a warning is issued.

### GEVENT_MONITOR_THREAD_ENABLE

Default: `True`

If true, the monitor thread is created the first time the hub is switched to.

### STATSD_HOST

Default: `'localhost'`

StatsD host for metrics when using the StatsD-backed health helpers.

### STATSD_PORT

Default: `8125`

StatsD port.

## Usage

### `gevent_toolbelt.service.Service`

```python
import gevent
from gevent_toolbelt.service import Service


class MyService(Service):
    def handle(self):
        while not self.stop_event.is_set():
            gevent.sleep()


my_service = MyService()
my_service.serve_forever()
```

### `gevent_toolbelt.service.ServiceFunc`

```python
import gevent
from gevent_toolbelt.service import ServiceFunc


def my_service_func(stop_event):
    while not stop_event.is_set():
        gevent.sleep()


my_service = ServiceFunc(my_service_func)
my_service.serve_forever()
```

### `gevent_toolbelt.service.ServiceGroup`

```python
import gevent
from gevent_toolbelt.service import ServiceFunc, ServiceGroup


def my_service_func(stop_event):
    while not stop_event.is_set():
        gevent.sleep()


my_service_group = ServiceGroup(
    [
        ServiceFunc(my_service_func),
        ServiceFunc(my_service_func),
        ServiceFunc(my_service_func),
    ]
)
my_service_group.serve_forever()
```

### StatsD metrics (`ServiceWithMetrics`, `ServiceFuncWithMetrics`, `ServiceGroupWithMetrics`)

Same roles as `Service`, `ServiceFunc`, and `ServiceGroup`, but emit worker gauges over StatsD when the event loop reports blocking. See `gevent_toolbelt.service.healthcheck.statsd`.

### Test helpers

`gevent_toolbelt.test.joinall_gracefully` joins greenlets and runs a sidecar that calls `stop_func` after a timeout.

```python
import gevent
from gevent.event import Event
from gevent_toolbelt.test import joinall_gracefully

stop_event = Event()


def my_service_func(stop_event):
    while not stop_event.is_set():
        gevent.sleep()


joinall_gracefully(gevent.spawn(my_service_func, stop_event), timeout=5)
```

### Graceful shutdown

```python
from gevent_toolbelt.service import Service
from gevent_toolbelt.signals import install_signal_handler

my_service = Service(...)

install_signal_handler(my_service.stop)

my_service.serve_forever()
```

## Development

Clone the repository, then install dependencies with uv (includes dev tools such as pytest and Ruff):

```sh
uv sync
```

Run the linter and formatter:

```sh
uv run ruff check gevent_toolbelt tests
uv run ruff format gevent_toolbelt tests
```

Run tests (pytest is started via gevent’s monkey entry point so `socket` patches in tests line up with StatsD and the hub):

```sh
uv run python -m gevent.monkey "$(uv run which pytest)"
```

## Publishing to PyPI

1. Bump the version in `gevent_toolbelt/__init__.py` (constant `VERSION`; `__version__` follows it), then create and push an annotated tag `vX.Y.Z` (for example `v1.0.13`). You can also use [bump2version](https://github.com/c4urself/bump2version) with `.bumpversion.cfg`.
2. Build and publish are handled by **`.github/workflows/release.yml`** when the tag is pushed: it builds with `uv build --no-sources`, smoke-tests the wheel and sdist, then runs `uv publish` using **PyPI Trusted Publishing (OIDC)**. On GitHub, create an environment named **`pypi`** (optionally with required reviewers) and register that publisher on PyPI so uploads are approved the same way as in [wsgiven](https://github.com/adalekin/wsgiven).

Alternatively, build locally and upload with a [PyPI API token](https://pypi.org/help/#apitoken):

```sh
uv build --no-sources
UV_PUBLISH_TOKEN=… uv publish
```

Configure the `Homepage` / `Repository` URLs in `pyproject.toml` if your GitHub organization or repository name differs from the placeholders.

## License

MIT — see [LICENSE](LICENSE).
