Metadata-Version: 2.1
Name: judoscale
Version: 1.1.0
Summary: Official Python adapter for Judoscale — the advanced autoscaler for Heroku
Home-page: https://github.com/judoscale/judoscale-python
License: MIT
Author: Adam McCrea
Author-email: adam@adamlogic.com
Requires-Python: >=3.8,<4.0
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3
Provides-Extra: celery-redis
Provides-Extra: django
Provides-Extra: flask
Provides-Extra: rq
Requires-Dist: celery[redis] (>=4.4.0,<6.0.0); extra == "celery-redis"
Requires-Dist: django (>=2.1.0,<5.0.0); extra == "django"
Requires-Dist: flask (>=1.1.0,<3.0.0); extra == "flask"
Requires-Dist: requests (<3.0.0)
Requires-Dist: rq (>=1.0.0,<2.0.0); extra == "rq"
Project-URL: Repository, https://github.com/judoscale/judoscale-python
Description-Content-Type: text/markdown

# Judoscale

This is the official Python adapter for [Judoscale](https://elements.heroku.com/addons/judoscale). You can use Judoscale without it, but this gives you request queue time metrics and job queue time (for supported job processors).

It is recommended to install the specific web framework and/or background job library support as "extras" to the `judoscale` PyPI package. This ensures that checking if the installed web framework and/or background task processing library is supported happens at dependency resolution time.

## Supported web frameworks

- [x] [Django](#using-judoscale-with-django)
- [x] [Flask](#using-judoscale-with-flask)
- [ ] FastAPI

## Supported job processors

- [x] [Celery](#using-judoscale-with-celery-and-redis) (with Redis as the broker)
- [x] [RQ](#using-judoscale-with-rq)

# Using Judoscale with Django

Install Judoscale for Django with:

```sh
$ pip install 'judoscale[django]'
```

Add Judoscale app to `settings.py`:

```python
INSTALLED_APPS = [
    "judoscale.django",
    # ... other apps
]
```

This sets up the Judoscale middleware to capture request queue times.

Optionally, you can customize Judoscale in `settings.py`:

```python
JUDOSCALE = {
    # LOG_LEVEL defaults to ENV["LOG_LEVEL"] or "INFO".
    "LOG_LEVEL": "DEBUG",

    # API_BASE_URL defaults to ENV["JUDOSCALE_URL"], which is set for you when you install Judoscale.
    # This is only exposed for testing purposes.
    "API_BASE_URL": "https://example.com",

    # REPORT_INTERVAL_SECONDS defaults to 10 seconds.
    "REPORT_INTERVAL_SECONDS": 5,
}
```

Once deployed, you will see your request queue time metrics available in the Judoscale UI.

# Using Judoscale with Flask

Install Judoscale for Flask with:

```sh
$ pip install 'judoscale[flask]'
```

The Flask support for Judoscale is packaged into a Flask extension. Import the extension class and use like you normally would in a Flask application:

```py
# app.py

from judoscale.flask import Judoscale

# If your app is a top-level global

app = Flask("MyFlaskApp")
app.config.from_object('...')  # or however you configure your app
judoscale = Judoscale(app)


# If your app uses the application factory pattern

judoscale = Judoscale()

def create_app():
    app = Flask("MyFlaskApp")
    app.config.from_object('...')  # or however you configure your app
    judoscale.init_app(app)
    return app
```

This sets up the Judoscale extension to capture request queue times.

Optionally, you can override Judoscale's own configuration via your application's [configuration dictionary](https://flask.palletsprojects.com/en/2.2.x/api/#flask.Flask.config). The Judoscale Flask extension looks for a top-level `"JUDOSCALE"` key in `app.config`, which should be a dictionary, and which the extension uses to configure itself as soon as `judoscale.init_app()` is called.

```python
JUDOSCALE = {
    # LOG_LEVEL defaults to ENV["LOG_LEVEL"] or "INFO".
    "LOG_LEVEL": "DEBUG",

    # API_BASE_URL defaults to ENV["JUDOSCALE_URL"], which is set for you when you install Judoscale.
    # This is only exposed for testing purposes.
    "API_BASE_URL": "https://example.com",

    # REPORT_INTERVAL_SECONDS defaults to 10 seconds.
    "REPORT_INTERVAL_SECONDS": 5,
}
```

Note the [official recommendations for configuring Flask](https://flask.palletsprojects.com/en/2.2.x/config/#configuration-best-practices).

# Using Judoscale with Celery and Redis

Install Judoscale for Celery with:

```sh
$ pip install 'judoscale[celery-redis]'
```

> :warning: **NOTE 1:** The Judoscale Celery integration currently only works with the [Redis broker](https://docs.celeryq.dev/en/stable/getting-started/backends-and-brokers/index.html#redis).

> :warning: **NOTE 2:** Using [task priorities](https://docs.celeryq.dev/en/latest/userguide/calling.html#advanced-options) is currently not supported by `judoscale`. You can still use task priorities, but `judoscale` won't see and report metrics on any queues other than the default, unprioritised queue.

Judoscale can automatically scale the number of Celery workers based on the queue latency (the age of the oldest pending task in the queue).

## Setting up the integration

To use the Celery integration, import `judoscale_celery` and call it with the Celery app instance. `judoscale_celery` should be called after you have set up and configured the Celery instance.

```py
from celery import Celery
from judoscale.celery import judoscale_celery

celery_app = Celery(broker="redis://localhost:6379/0")
# Further setup
# celery_app.conf.update(...)
# ...

judoscale_celery(celery_app)
```

This sets up Judoscale to periodically calculate and report queue latency for each Celery queue.

If you need to change the Judoscale integration configuration, you can pass a dictionary of Judoscale configuration options to `judoscale_celery` to override the default Judoscale config variables:

```py
judoscale_celery(celery_app, extra_config={"LOG_LEVEL": "DEBUG"})
```

> :warning: **NOTE:** Calling `judoscale_celery` turns on sending [`task-sent`](https://docs.celeryq.dev/en/stable/userguide/configuration.html#task-send-sent-event) events. This is required for the Celery integration with Judoscale to work.

### Judoscale with Celery and Flask

Depending on how you've structured your Flask app, you should call `judoscale_celery` after your application has finished configuring the Celery app instance. If you have followed the [Flask guide](https://flask.palletsprojects.com/en/2.2.x/patterns/celery/) in the Flask documentation, the easiest place to initialise the Judoscale integration is in the application factory:

```py
def create_app():
    app = Flask(__name__)
    app.config.from_object(...) # or however you configure your app
    celery_app = celery_init_app(app)
    # Initialise the Judoscale integration
    judoscale_celery(celery_app, extra_config=app.config["JUDOSCALE"])
    return app
```

### Judoscale with Celery and Django

If you have followed the [Django guide](https://docs.celeryq.dev/en/stable/django/first-steps-with-django.html) in the Celery documentation, you should have a module where you initialise the Celery app instance, auto-discover tasks, etc. You should call `judoscale_celery` after you have configured the Celery app instance:

```py
from celery import Celery
from django.conf import settings
from judoscale.celery import judoscale_celery

app = Celery()
app.config_from_object("django.conf:settings", namespace="CELERY")
app.autodiscover_tasks()
# Initialise the Judoscale integration
judoscale_celery(app, extra_config=settings.JUDOSCALE)
```

# Using Judoscale with RQ

Install Judoscale for RQ with:

```sh
$ pip install 'judoscale[rq]'
```

Judoscale can automatically scale the number of RQ workers based on the queue latency (the age of the oldest pending task in the queue).

## Setting up the integration

To use the RQ integration, import `judoscale_rq` and call it with an instance of `Redis` pointing to the same Redis database that RQ uses.

```py
from redis import Redis
from judoscale.rq import judoscale_rq

redis = Redis(...)
judoscale_rq(redis)
```

This sets up Judoscale to periodically calculate and report queue latency for each RQ queue.

If you need to change the Judoscale integration configuration, you can pass a dictionary of Judoscale configuration options to `judoscale_rq` to override the default Judoscale config variables:

```py
judoscale_rq(redis, extra_config={"LOG_LEVEL": "DEBUG"})
```

### Judoscale with RQ and Flask

The recommended way to initialise Judoscale for RQ is in the application factory:

```py
judoscale = Judoscale()

def create_app():
    app = Flask(__name__)
    app.config.from_object("...") # or however you configure your application
    app.redis = Redis()

    # Initialise the Judoscale integration for Flask
    judoscale.init_app(app)

    # Initialise the Judoscale integration for RQ
    judoscale_rq(app.redis)

    return app
```

Then, in your worker script, make sure that you create an app, which will initialise the Judoscale integration with RQ. Although not required, it's useful to run the worker within the Flask app context. If you have followed the [RQ on Heroku pattern](https://python-rq.org/patterns/) for setting up your RQ workers on Heroku, your worker script should look something like this:

```py
from rq.worker import HerokuWorker as Worker

app = create_app()

worker = Worker(..., connection=app.redis)
with app.app_context():
    worker.work()
```

See the [run-worker.py script](./sample-apps/flask_rq_sample/run-worker.py) for reference.

### Judoscale with RQ and Django

The Judoscale integration for RQ is packaged into a separate Django app.

You should already have `judoscale.django` in your `INSTALLED_APPS`. Next, add the RQ integration app `judoscale.rq`:

```python
INSTALLED_APPS = [
    "judoscale.django",
    "judoscale.rq",
    # ... other apps
]
```

By default, `judoscale.rq` will connect to the Redis instance as specified by the `REDIS_URL` environment variable. If that is not suitable, you can supply Redis connection information in the `JUDOSCALE` configuration dictionary under the `"REDIS"` key.

Accepted formats are:

- a dictionary containing a single key `"URL"` pointing to a Redis server URL, or;
- a dictionary of configuration options corresponding to the keyword arguments of the [`Redis` constructor](https://github.com/redis/redis-py/blob/6c708c2e0511364c2c3f21fa1259de05e590632d/redis/client.py#L905).

```py
JUDOSCALE = {
    # Configuring with a Redis server URL
    "REDIS": {
        "URL": os.getenv("REDISTOGO_URL")
    }

    # Configuring as kwargs to Redis(...)
    "REDIS": {
        "HOST": "localhost",
        "PORT": 6379,
        "DB": 0
    }
}
```

If you are using [Django-RQ](https://github.com/rq/django-rq/tree/master), you can also pull configuration from `RQ_QUEUES` directly:

```py
RQ_QUEUES = {
    "high_priority": {
        "HOST": "...",
        "PORT": 6379,
        "DB": 0
    },
}

JUDOSCALE = {
    # ... other configuration options
    "REDIS": RQ_QUEUES["high_priority"]
}
```

> :warning: **NOTE:** Django-RQ enables configuring RQ such that different queues and workers use _different_ Redis instances. Judoscale currently only supports connecting to and monitoring queue latency on a single Redis instance.

## Development

This repo includes a `sample-apps` directory containing apps you can run locally. These apps use the `judoscale` adapter, but they override `API_BASE_URL` so they're not connected to the real Judoscale API. Instead, they post API requests to https://requestinspector.com so you can observe the API behavior.

See the `README` in a sample app for details on how to set it up and run locally.

### Contributing

`judoscale` uses [Poetry](https://python-poetry.org/) for managing dependencies and packaging the project. Head over to the [installations instructions](https://python-poetry.org/docs/#installing-with-the-official-installer) and install Poetry, if needed.

Clone the repo with

```sh
$ git clone git@github.com:judoscale/judoscale-python.git
$ cd judoscale-python
```

Verify that you are on a recent version of Poetry:

```sh
$ poetry --version
Poetry (version 1.3.1)
```

Install dependencies with Poetry and activate the virtualenv

```sh
$ poetry install --all-extras
$ poetry shell
```

Run tests with

```sh
$ python -m unittest discover -s tests
```

