df-websockets


Namedf-websockets JSON
Version 1.0.9 PyPI version JSON
download
home_pagehttps://github.com/d9pouces/df_websockets
SummaryWebsocket integration for Django
upload_time2023-12-04 06:18:33
maintainerMatthieu Gallet
docs_urlNone
authorMatthieu Gallet
requires_python
licenseCeCILL-B
keywords
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI
coveralls test coverage No coveralls.
            df_websockets
=============

`df_websockets` extends [django-channels](https://channels.readthedocs.io) to simplify communications between 
clients and servers and to process heavy tasks in background processes.

`df_websockets` is based on two main concepts:

* _signals_, that are functions triggered both on the server or the browser  window by either the server or the client,
* _topics_ to allow the server to send signals to any group of browser windows.

Signals are exchanged between the browser window and the server using a single websocket.
Signals that are triggered by the browser on the server are processed as background tasks (so the websocket endpoint does almost nothing).
Signals that are triggered by the server can be processed as background tasks on the serveur and as Javascript functions on the browser.

Background processes can use [celery](https://docs.celeryproject.org/en/stable/), [channels](https://pypi.org/project/channels/) workers, or simply different processes or threads.


Requirements
------------

`df_websockets` works with:

  * [Python](https://www.python.org) >= 3.6,
  * [django](https://pypi.org/project/Django/) >= 2.0,
  * [channels](https://pypi.org/project/channels/) >= 2.0,
  * [daphne](https://pypi.python.org/pypi/daphne) >= 2.0.


For production use or any multiprocess setup (even in development mode), you also need:

  * [redis](https://redis.io) >= 5.0,
  * [channels_redis](https://pypi.org/project/channels-redis/) >= 3.3,
  * [daphne](https://pypi.python.org/pypi/daphne) >= 2.0.

If you want to process signals in Celery tasks rather in Channel workers, you need to setup a Celery infrastructure:
[Celery setup](https://docs.celeryproject.org/en/stable/django/first-steps-with-django.html).

Installation
------------

```bash
python -m pip install df_websockets
```

In your settings, you must add the following values:
```python
# the ASGI application to use with gunicorn or daphne
ASGI_APPLICATION = "df_websockets.routing.application"
# add the required Middleware
MIDDLEWARES = [..., "df_websockets.middleware.WebsocketMiddleware", ...]
INSTALLED_APPS = [..., "channels", "daphne", "df_websockets", ...]
WEBSOCKET_WORKERS = "thread"
# a channel layer, required by channels_redis
CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            "hosts": [('localhost', 6379)],
        },
    },
}
```

If you use `df_config` and you use a local Redis, you have nothing to do: settings are automatically set and everything is working as soon as a Redis is running on your machine.

Now, include `js/df_websockets.min.js` in your HTML and call `df_websockets.tasks.set_websocket_topics(request)` somewhere in the Django view.
A bidirectionnal websocket connection will be established in your page.

You can start the development server:
```bash
python manage.py runserver
```

_`daphne` must be separately installed and added to `INSTALLED_APPS` with `channels>=4.0`_ 

If you use Channels workers (WEBSOCKET_WORKERS = "channels"), you also need to start a Channel worker:
```bash
python manage.py run_worker celery
```
If you use Celery (WEBSOCKET_WORKERS = "celery"), you also need to start a Celery worker:
```bash
python manage.py worker -Q celery
```

basic usage
-----------

A _signal_ is a string attached to Python or Javascript functions. When this signal is triggered, all these functions are called.
Of course, you can target the platforms on which the functions will be executed: the server (for Python code) or chosen browser windows.

First, we connect our code to the signal `"myproject.first_signal"`.
```python
from df_websockets.decorators import everyone, signal
import time

@signal(path="myproject.first_signal", is_allowed_to=everyone, queue="celery")
def my_first_signal(window_info, content=None):
    print(content)

@signal(path="myproject.first_signal", is_allowed_to=everyone, queue="slow")
def my_first_signal_slow(window_info, content=None):
    time.sleep(100)
    print(content)
```

```javascript
/* static file "js/df_websockets.min.js" must be included first */
document.addEventListener("DOMContentLoaded", () => {
    window.DFSignals.connect('myproject.first_signal', (opts) => {
        console.warn(opts.content);
    });
});
```

Now, we can trigger this signal to call this functions.
In both cases, both functions will be called on the server and in the browser window.
```javascript
window.DFSignals.call('myproject.first_signal', {content: "Hello from browser"});
```

```python
from df_websockets.tasks import WINDOW, trigger, SERVER
from df_websockets.decorators import everyone, signal
from django.http.response import HttpResponse

def any_view(request):  # this is a standard Django view
    trigger(request, 'myproject.first_signal', to=[SERVER, WINDOW], content="hello from a view")
    return HttpResponse()

@signal(path="myproject.second_signal", is_allowed_to=everyone, queue="slow")
def second_signal(window_info):
    trigger(window_info, 'myproject.first_signal', to=[SERVER, WINDOW], content="hello from Celery")
```

In this case, the `to` parameter targets both the server and the window.
You can even open a shell and call `df_websockets.tasks.trigger(None, 'myproject.first_signal', to=[BROADCAST], content="hello from a shell")`.
All open windows will react.

Topics
------

You can select the set of connected browser windows that receive a signal triggered by the server, in addition of processing this signal on the server.

A Django view using this signal system must call `set_websocket_topics` to add some ”topics” to this view.
When you trigger a signal on the server, you can target any set topic. All windows featuring this topic will receive this signal.

_For example, assume that multiple clients open a specific article on a blog. At any time, you can open Python shell in a terminal and trigger a signal on all these windows._  


```python
from df_websockets.tasks import set_websocket_topics
from django.contrib.auth.models import Group
from django.template.response import TemplateResponse

def any_view(request):  # this is a standard Django view
    # useful code
    obj1 = Group.objects.get(id=42)
    set_websocket_topics(request, [obj1])
    return TemplateResponse("my/template.html", {})  # do not forget to add `js/df_websockets.min.js` to this HTML
```

`obj1` must be a Python object that is handled by the `WEBSOCKET_TOPIC_SERIALIZER` function. By default, any string and Django models are valid.
Each window also has a unique identifier that is automatically added to this list, as well as the connected user id and the `BROADCAST`.

The following code will call the JS function on every browser window having the `obj` topic and to the displayed window.
```python
from df_websockets.tasks import WINDOW, trigger
from df_websockets.tasks import set_websocket_topics
from django.contrib.auth.models import Group
from django.http.response import HttpResponse
def another_view(request, obj_id):
    obj = Group.objects.get(id=42)
    trigger(request, 'myproject.first_signal', to=[WINDOW, obj], content="hello from a view")
    set_websocket_topics(request, ["other topics"])
    return HttpResponse()
```

There are three special values:

* `df_websockets.tasks.WINDOW`: the original browser window,
* `df_websockets.tasks.USER`: all windows currently displayed by the connected user,
* `df_websockets.tasks.BROADCAST`: all active windows.

Some information about the original window (like its unique identifier or the connected user) must be provided to the triggered Python code, allowing it to trigger JS events on any selected window.  
These data are stored in the `WindowInfo` object, automatically built from the HTTP request by the trigger function and provided as first argument to the triggered code.
The `trigger` function accepts `WindowInfo` or `HTTPRequest` objects as first argument.


settings
--------

There are a few settings:

- `WEBSOCKET_WORKERS`: one of "celery" (use Celery tasks), "channels" (use Channels workers), "multithread" (process signals in threads), "multiprocess" (process signals in new processes).
  The first two choices require at least one valid worker. 
- `WEBSOCKET_DEFAULT_QUEUE` the default queue for signals (`"celery"` by default)

Other settings are:
- `WEBSOCKET_CACHE_EXPIRE`: the validity of the association between a websocket connection and the associated topics
- `WEBSOCKET_CACHE_PREFIX`: prefix of keys used to cache data
- `WEBSOCKET_CACHE_BACKEND`: the cache backend (`default` by default) — you cannot use LocMemCache with Celery or Channels workers, nor DummyCache with any kind of workers 
- `WEBSOCKET_SIGNAL_ENCODER`: the JSON encoder to encode signal arguments 
- `WEBSOCKET_SIGNAL_DECODER`: the JSON decoder to decode signal arguments
- `WEBSOCKET_TOPIC_SERIALIZER`: the function used to transform Python topics into valid topic names  
- `WEBSOCKET_POOL_SIZES`: a dict associating a queue name to a number of threads (or processes)
- `WINDOW_INFO_MIDDLEWARES`: a list of middlewares for transforming a HttpRequest to a WindowInfo 
- `WEBSOCKET_URL`: URL prefix (`/ws/` by default)
- `ASGI_APPLICATION`: the ASGI application 

Cache backends
--------------

Task data are passed by the server process to the workers using the Django cache infrastructure. 
For obvious reasons, you cannot use DummyCache nor LocMemCache with Celery or Channels workers, since these caching methods are not shared accross processes.
So, you need either to use a shared cache backend as default backend, or dedicate a cache backend to websockets. 

First case needs to update your `settings.py` file:
```python
CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379",
    }
}
```

Second case, still in your `settings.py` file:
```python
CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
        "LOCATION": "unique-snowflake",
    },
    "websockets": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379",
    },
}
WEBSOCKET_CACHE_BACKEND = "websockets"
```

HTML forms
----------

`df_websockets` comes with some helper functions when you signals to be trigger on the server when a form is submitted or changed.
Assuming that you have a `signals.py` file that contains:
```python
from df_websockets.decorators import signal
from df_websockets.tasks import WINDOW, trigger
from df_websockets.utils import SerializedForm
from django import forms


class MyForm(forms.Form): 
    title = forms.CharField()

@signal(path='signal.name')
def my_signal_function(window_info, form_data: SerializedForm(MyForm)=None, title=None, id=None):
    print(form_data and form_data.is_valid())
    trigger(window_info, 'myproject.first_signal', to=WINDOW, title=title)

@signal(path='signal.name')
def my_signal_function_raw(window_info, form_data=None, title=None, id=None):
    print(form_data and form_data.is_valid())
    trigger(window_info, 'myproject.first_signal', to=WINDOW, title=title)
```


Using on a HTML form:
```html
<form data-df-signal='[{"name": "signal.name", "on": "change", "form": "form_data", "opts": {"id": 42} }]'>
    <input type="text" name="title" value="df_websockets">
</form>
```

or, using the Django templating system:

```html
{% load websockets %}
<form {% js_call "signal.name" on="change" form="form_data" id=42 %}>
    <input type="text" name="title" value="df_websockets">
</form>
```

When the field "title" is modified, `my_signal_function(window_info, form_data = [{"name": "title", "value": "df_websockets"}], id=43)` is called.



Using on a HTML form input field:
```html
<form>
    <input type="text" name="title" data-df-signal='[{"name": "signal.name", "on": "change", "value": "title", "opts": {"id": 42} }]'>
</form>
```

or, using the Django templating system:

```html
{% load websockets %}
<form>
    <input type="text" name="title" {% js_call "signal.name" on="change" value="title" id=42 %}>
</form>
```

When the field "title" is modified, `my_signal_function(window_info, title="new title value", id=43)` is called.



Testing signals
---------------

In production, the signal framework requires a working Redis and worker processes.
However, if you only want to check if a signal has been called in unitary tests, you can use :class:`df_websockets.utils.SignalQueue`.
Both server-side and client-side signals are kept into memory:

* `df_websockets.testing.SignalQueue.ws_signals`,

    * keys are the serialized topics
    * values are lists of tuples `(signal name, arguments as dict)`

* `df_websockets.testing.SignalQueue.python_signals`

    * keys are the name of the queue
    * values are lists of `(signal_name, window_info_dict, kwargs=None, from_client=False, serialized_client_topics=None, to_server=False, queue=None)`

      * `signal_name` is … the name of the signal
      * `window_info_dict` is a WindowInfo serialized as a dict,
      * `kwargs` is a dict representing the signal arguments,
      * `from_client` is `True` if this signal has been emitted by a web browser,
      * `serialized_client_topics` is not `None` if this signal must be re-emitted to some client topics,
      * `to_server` is `True` if this signal must be processed server-side,
      * `queue` is the name of the selected Celery queue.

```python
from df_websockets.tasks import trigger, SERVER
from df_websockets.window_info import WindowInfo
from df_websockets.testing import SignalQueue

from df_websockets.decorators import signal
# noinspection PyUnusedLocal
@signal(path='test.signal', queue='demo-queue')
def test_signal(window_info, value=None):
  print(value)

wi = WindowInfo()
with SignalQueue() as fd:
  trigger(wi, 'test.signal1', to=[SERVER, 1], value="value1")
  trigger(wi, 'test.signal2', to=[SERVER, 1], value="value2")

# fd.python_signals looks like {'demo-queue': [ ['test.signal1', {…}, {'value': 'value1'}, False, None, True, None], 
# # ['test.signal2', {…}, {'value': 'value2'}, False, None, True, None]]}
# fd.ws_signals looks like {'-int.1': [('test.signal1', {'value': 'value1'}), ('test.signal2', {'value': 'value2'})]}
```

JavaScript signals
------------------

Many [JS signals](https://github.com/d9pouces/df_websockets/blob/master/npm/df_websockets/base.js) are available out-of-the-box.
These signals can be triggered either by the JS code or by the Python code.

Signals must be defined in a Python file that is imported during Django's startup, or in any `signals.py` file inside a Django app (like `models.py`). 
For example, you can update the content of a HTML node with the following lines:

```python
from df_websockets.tasks import trigger, WINDOW
from df_websockets.decorators import signal

@signal(path='test.signal', queue='demo-queue')
def test_signal(window_info, word="hellow"):
    trigger(window_info, 'html.content', to=WINDOW, selector="#obj", content= "<span>%s</span>" % word)
```

```javascript
window.DFSignals.call('html.content', {selector: "#obj", content: "<span>hello</span>"});
```

Please read the content of `npm/df_websockets/base.js` for the whole list of available signals. 
You can also create some shortcuts for the most common signals.
Another way is to run the demo:

```bash
python demo_manage.py runserver
```

Checklist 
---------

Everything must be correctly setup to have working signals.

The first step is to test tasks from the command-line:

1. Redis is running and accepting connections
2. Celery is working
2. at least one worker is running with all required queues
3. the triggered signal is exists
4. open a console `python manage.py shell` and manually trigger a task
```python
from df_websockets.tasks import trigger, SERVER
from df_websockets.window_info import WindowInfo
trigger(WindowInfo(), 'test.signal', to=[SERVER], value="value2")
```


The second step is to check the web part:

1. the web server must be running and accepting connections
2. Celery is working
2. at least one worker is running with all required queues
3. the triggered signal exists
3. `df_websockets.middleware.WebsocketMiddleware` is included
4. check the used domain name, since tokens are passed through cookies: **"localhost" is different than "127.0.0.1"**
5. `df_websockets.tasks.set_websocket_topics` is used somewhere in the view
6. `static/js/df_websockets.min.js` is included in the page
7. check if the WS tries to connect
8. check if the WS is connected
4. open a console `python manage.py shell` and manually trigger a task
```python
from df_websockets.tasks import trigger, BROADCAST
from df_websockets.window_info import WindowInfo
trigger(WindowInfo(), 'html.text', to=[BROADCAST], selector="body", content= "<span>hello</span>")
```

Do not hesitate to use a verbose logging:
```python
LOGGING = {
    "version": 1,
    "disable_existing_loggers": True,
    "formatters": {
        "verbose": {
            "format": (
                "%(asctime)s [%(process)d] [%(levelname)s] "
                + "pathname=%(pathname)s lineno=%(lineno)s "
                + "funcname=%(funcName)s %(message)s"
            ),
            "datefmt": "%Y-%m-%d %H:%M:%S",
        },
        "django.server": {
            "()": "django.utils.log.ServerFormatter",
        },
        "nocolor": {
            "()": "logging.Formatter",
            "fmt": "%(asctime)s [%(name)s] [%(levelname)s] %(message)s",
            "datefmt": "%Y-%m-%d %H:%M:%S",
        },
    },
    "filters": {
    },
    "handlers": {
        "stdout.info": {
            "class": "logging.StreamHandler",
            "level": "DEBUG",
            "stream": "ext://sys.stdout",
            "formatter": "verbose",
        },
        "stderr.debug.django.server": {
            "class": "logging.StreamHandler",
            "level": "DEBUG",
            "stream": "ext://sys.stderr",
            "formatter": "django.server",
        },
    },
    "loggers": {
        "django": {"handlers": [], "level": "INFO", "propagate": True},
        "django.db": {"handlers": [], "level": "INFO", "propagate": True},
        "django.db.backends": {"handlers": [], "level": "INFO", "propagate": True},
        "django.request": {"handlers": [], "level": "DEBUG", "propagate": True},
        "django.security": {"handlers": [], "level": "INFO", "propagate": True},
        "df_websockets.signals": {"handlers": [], "level": "DEBUG", "propagate": True},
        "gunicorn.error": {"handlers": [], "level": "DEBUG", "propagate": True},
        "pip.vcs": {"handlers": [], "level": "INFO", "propagate": True},
        "py.warnings": {
            "handlers": [],
            "level": "INFO",
            "propagate": True,
        },
        "daphne": {"handlers": [], "level": "INFO", "propagate": True},
        "mail.log": {"handlers": [], "level": "INFO", "propagate": True},
        "aiohttp.access": {
            "handlers": ["stderr.debug.django.server"],
            "level": "INFO",
            "propagate": False,
        },
        "django.server": {
            "handlers": ["stderr.debug.django.server"],
            "level": "INFO",
            "propagate": False,
        },
        "django.channels.server": {
            "handlers": ["stderr.debug.django.server"],
            "level": "INFO",
            "propagate": False,
        },
        "gunicorn.access": {
            "handlers": ["stderr.debug.django.server"],
            "level": "INFO",
            "propagate": False,
        },
    },
    "root": {"handlers": ["stdout.info"], "level": "DEBUG"},
}

```

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/d9pouces/df_websockets",
    "name": "df-websockets",
    "maintainer": "Matthieu Gallet",
    "docs_url": null,
    "requires_python": "",
    "maintainer_email": "github@19pouces.net",
    "keywords": "",
    "author": "Matthieu Gallet",
    "author_email": "github@19pouces.net",
    "download_url": "https://files.pythonhosted.org/packages/7e/93/1334d49c08815a1b383a7f0fe6d1157c33174c680c2a6e4e6d546d8fdc86/df_websockets-1.0.9.tar.gz",
    "platform": null,
    "description": "df_websockets\n=============\n\n`df_websockets` extends [django-channels](https://channels.readthedocs.io) to simplify communications between \nclients and servers and to process heavy tasks in background processes.\n\n`df_websockets` is based on two main concepts:\n\n* _signals_, that are functions triggered both on the server or the browser  window by either the server or the client,\n* _topics_ to allow the server to send signals to any group of browser windows.\n\nSignals are exchanged between the browser window and the server using a single websocket.\nSignals that are triggered by the browser on the server are processed as background tasks (so the websocket endpoint does almost nothing).\nSignals that are triggered by the server can be processed as background tasks on the serveur and as Javascript functions on the browser.\n\nBackground processes can use [celery](https://docs.celeryproject.org/en/stable/), [channels](https://pypi.org/project/channels/) workers, or simply different processes or threads.\n\n\nRequirements\n------------\n\n`df_websockets` works with:\n\n  * [Python](https://www.python.org) >= 3.6,\n  * [django](https://pypi.org/project/Django/) >= 2.0,\n  * [channels](https://pypi.org/project/channels/) >= 2.0,\n  * [daphne](https://pypi.python.org/pypi/daphne) >= 2.0.\n\n\nFor production use or any multiprocess setup (even in development mode), you also need:\n\n  * [redis](https://redis.io) >= 5.0,\n  * [channels_redis](https://pypi.org/project/channels-redis/) >= 3.3,\n  * [daphne](https://pypi.python.org/pypi/daphne) >= 2.0.\n\nIf you want to process signals in Celery tasks rather in Channel workers, you need to setup a Celery infrastructure:\n[Celery setup](https://docs.celeryproject.org/en/stable/django/first-steps-with-django.html).\n\nInstallation\n------------\n\n```bash\npython -m pip install df_websockets\n```\n\nIn your settings, you must add the following values:\n```python\n# the ASGI application to use with gunicorn or daphne\nASGI_APPLICATION = \"df_websockets.routing.application\"\n# add the required Middleware\nMIDDLEWARES = [..., \"df_websockets.middleware.WebsocketMiddleware\", ...]\nINSTALLED_APPS = [..., \"channels\", \"daphne\", \"df_websockets\", ...]\nWEBSOCKET_WORKERS = \"thread\"\n# a channel layer, required by channels_redis\nCHANNEL_LAYERS = {\n    'default': {\n        'BACKEND': 'channels_redis.core.RedisChannelLayer',\n        'CONFIG': {\n            \"hosts\": [('localhost', 6379)],\n        },\n    },\n}\n```\n\nIf you use `df_config` and you use a local Redis, you have nothing to do: settings are automatically set and everything is working as soon as a Redis is running on your machine.\n\nNow, include `js/df_websockets.min.js` in your HTML and call `df_websockets.tasks.set_websocket_topics(request)` somewhere in the Django view.\nA bidirectionnal websocket connection will be established in your page.\n\nYou can start the development server:\n```bash\npython manage.py runserver\n```\n\n_`daphne` must be separately installed and added to `INSTALLED_APPS` with `channels>=4.0`_ \n\nIf you use Channels workers (WEBSOCKET_WORKERS = \"channels\"), you also need to start a Channel worker:\n```bash\npython manage.py run_worker celery\n```\nIf you use Celery (WEBSOCKET_WORKERS = \"celery\"), you also need to start a Celery worker:\n```bash\npython manage.py worker -Q celery\n```\n\nbasic usage\n-----------\n\nA _signal_ is a string attached to Python or Javascript functions. When this signal is triggered, all these functions are called.\nOf course, you can target the platforms on which the functions will be executed: the server (for Python code) or chosen browser windows.\n\nFirst, we connect our code to the signal `\"myproject.first_signal\"`.\n```python\nfrom df_websockets.decorators import everyone, signal\nimport time\n\n@signal(path=\"myproject.first_signal\", is_allowed_to=everyone, queue=\"celery\")\ndef my_first_signal(window_info, content=None):\n    print(content)\n\n@signal(path=\"myproject.first_signal\", is_allowed_to=everyone, queue=\"slow\")\ndef my_first_signal_slow(window_info, content=None):\n    time.sleep(100)\n    print(content)\n```\n\n```javascript\n/* static file \"js/df_websockets.min.js\" must be included first */\ndocument.addEventListener(\"DOMContentLoaded\", () => {\n    window.DFSignals.connect('myproject.first_signal', (opts) => {\n        console.warn(opts.content);\n    });\n});\n```\n\nNow, we can trigger this signal to call this functions.\nIn both cases, both functions will be called on the server and in the browser window.\n```javascript\nwindow.DFSignals.call('myproject.first_signal', {content: \"Hello from browser\"});\n```\n\n```python\nfrom df_websockets.tasks import WINDOW, trigger, SERVER\nfrom df_websockets.decorators import everyone, signal\nfrom django.http.response import HttpResponse\n\ndef any_view(request):  # this is a standard Django view\n    trigger(request, 'myproject.first_signal', to=[SERVER, WINDOW], content=\"hello from a view\")\n    return HttpResponse()\n\n@signal(path=\"myproject.second_signal\", is_allowed_to=everyone, queue=\"slow\")\ndef second_signal(window_info):\n    trigger(window_info, 'myproject.first_signal', to=[SERVER, WINDOW], content=\"hello from Celery\")\n```\n\nIn this case, the `to` parameter targets both the server and the window.\nYou can even open a shell and call `df_websockets.tasks.trigger(None, 'myproject.first_signal', to=[BROADCAST], content=\"hello from a shell\")`.\nAll open windows will react.\n\nTopics\n------\n\nYou can select the set of connected browser windows that receive a signal triggered by the server, in addition of processing this signal on the server.\n\nA Django view using this signal system must call `set_websocket_topics` to add some \u201dtopics\u201d to this view.\nWhen you trigger a signal on the server, you can target any set topic. All windows featuring this topic will receive this signal.\n\n_For example, assume that multiple clients open a specific article on a blog. At any time, you can open Python shell in a terminal and trigger a signal on all these windows._  \n\n\n```python\nfrom df_websockets.tasks import set_websocket_topics\nfrom django.contrib.auth.models import Group\nfrom django.template.response import TemplateResponse\n\ndef any_view(request):  # this is a standard Django view\n    # useful code\n    obj1 = Group.objects.get(id=42)\n    set_websocket_topics(request, [obj1])\n    return TemplateResponse(\"my/template.html\", {})  # do not forget to add `js/df_websockets.min.js` to this HTML\n```\n\n`obj1` must be a Python object that is handled by the `WEBSOCKET_TOPIC_SERIALIZER` function. By default, any string and Django models are valid.\nEach window also has a unique identifier that is automatically added to this list, as well as the connected user id and the `BROADCAST`.\n\nThe following code will call the JS function on every browser window having the `obj` topic and to the displayed window.\n```python\nfrom df_websockets.tasks import WINDOW, trigger\nfrom df_websockets.tasks import set_websocket_topics\nfrom django.contrib.auth.models import Group\nfrom django.http.response import HttpResponse\ndef another_view(request, obj_id):\n    obj = Group.objects.get(id=42)\n    trigger(request, 'myproject.first_signal', to=[WINDOW, obj], content=\"hello from a view\")\n    set_websocket_topics(request, [\"other topics\"])\n    return HttpResponse()\n```\n\nThere are three special values:\n\n* `df_websockets.tasks.WINDOW`: the original browser window,\n* `df_websockets.tasks.USER`: all windows currently displayed by the connected user,\n* `df_websockets.tasks.BROADCAST`: all active windows.\n\nSome information about the original window (like its unique identifier or the connected user) must be provided to the triggered Python code, allowing it to trigger JS events on any selected window.  \nThese data are stored in the `WindowInfo` object, automatically built from the HTTP request by the trigger function and provided as first argument to the triggered code.\nThe `trigger` function accepts `WindowInfo` or `HTTPRequest` objects as first argument.\n\n\nsettings\n--------\n\nThere are a few settings:\n\n- `WEBSOCKET_WORKERS`: one of \"celery\" (use Celery tasks), \"channels\" (use Channels workers), \"multithread\" (process signals in threads), \"multiprocess\" (process signals in new processes).\n  The first two choices require at least one valid worker. \n- `WEBSOCKET_DEFAULT_QUEUE` the default queue for signals (`\"celery\"` by default)\n\nOther settings are:\n- `WEBSOCKET_CACHE_EXPIRE`: the validity of the association between a websocket connection and the associated topics\n- `WEBSOCKET_CACHE_PREFIX`: prefix of keys used to cache data\n- `WEBSOCKET_CACHE_BACKEND`: the cache backend (`default` by default) \u2014 you cannot use LocMemCache with Celery or Channels workers, nor DummyCache with any kind of workers \n- `WEBSOCKET_SIGNAL_ENCODER`: the JSON encoder to encode signal arguments \n- `WEBSOCKET_SIGNAL_DECODER`: the JSON decoder to decode signal arguments\n- `WEBSOCKET_TOPIC_SERIALIZER`: the function used to transform Python topics into valid topic names  \n- `WEBSOCKET_POOL_SIZES`: a dict associating a queue name to a number of threads (or processes)\n- `WINDOW_INFO_MIDDLEWARES`: a list of middlewares for transforming a HttpRequest to a WindowInfo \n- `WEBSOCKET_URL`: URL prefix (`/ws/` by default)\n- `ASGI_APPLICATION`: the ASGI application \n\nCache backends\n--------------\n\nTask data are passed by the server process to the workers using the Django cache infrastructure. \nFor obvious reasons, you cannot use DummyCache nor LocMemCache with Celery or Channels workers, since these caching methods are not shared accross processes.\nSo, you need either to use a shared cache backend as default backend, or dedicate a cache backend to websockets. \n\nFirst case needs to update your `settings.py` file:\n```python\nCACHES = {\n    \"default\": {\n        \"BACKEND\": \"django.core.cache.backends.redis.RedisCache\",\n        \"LOCATION\": \"redis://127.0.0.1:6379\",\n    }\n}\n```\n\nSecond case, still in your `settings.py` file:\n```python\nCACHES = {\n    \"default\": {\n        \"BACKEND\": \"django.core.cache.backends.locmem.LocMemCache\",\n        \"LOCATION\": \"unique-snowflake\",\n    },\n    \"websockets\": {\n        \"BACKEND\": \"django.core.cache.backends.redis.RedisCache\",\n        \"LOCATION\": \"redis://127.0.0.1:6379\",\n    },\n}\nWEBSOCKET_CACHE_BACKEND = \"websockets\"\n```\n\nHTML forms\n----------\n\n`df_websockets` comes with some helper functions when you signals to be trigger on the server when a form is submitted or changed.\nAssuming that you have a `signals.py` file that contains:\n```python\nfrom df_websockets.decorators import signal\nfrom df_websockets.tasks import WINDOW, trigger\nfrom df_websockets.utils import SerializedForm\nfrom django import forms\n\n\nclass MyForm(forms.Form): \n    title = forms.CharField()\n\n@signal(path='signal.name')\ndef my_signal_function(window_info, form_data: SerializedForm(MyForm)=None, title=None, id=None):\n    print(form_data and form_data.is_valid())\n    trigger(window_info, 'myproject.first_signal', to=WINDOW, title=title)\n\n@signal(path='signal.name')\ndef my_signal_function_raw(window_info, form_data=None, title=None, id=None):\n    print(form_data and form_data.is_valid())\n    trigger(window_info, 'myproject.first_signal', to=WINDOW, title=title)\n```\n\n\nUsing on a HTML form:\n```html\n<form data-df-signal='[{\"name\": \"signal.name\", \"on\": \"change\", \"form\": \"form_data\", \"opts\": {\"id\": 42} }]'>\n    <input type=\"text\" name=\"title\" value=\"df_websockets\">\n</form>\n```\n\nor, using the Django templating system:\n\n```html\n{% load websockets %}\n<form {% js_call \"signal.name\" on=\"change\" form=\"form_data\" id=42 %}>\n    <input type=\"text\" name=\"title\" value=\"df_websockets\">\n</form>\n```\n\nWhen the field \"title\" is modified, `my_signal_function(window_info, form_data = [{\"name\": \"title\", \"value\": \"df_websockets\"}], id=43)` is called.\n\n\n\nUsing on a HTML form input field:\n```html\n<form>\n    <input type=\"text\" name=\"title\" data-df-signal='[{\"name\": \"signal.name\", \"on\": \"change\", \"value\": \"title\", \"opts\": {\"id\": 42} }]'>\n</form>\n```\n\nor, using the Django templating system:\n\n```html\n{% load websockets %}\n<form>\n    <input type=\"text\" name=\"title\" {% js_call \"signal.name\" on=\"change\" value=\"title\" id=42 %}>\n</form>\n```\n\nWhen the field \"title\" is modified, `my_signal_function(window_info, title=\"new title value\", id=43)` is called.\n\n\n\nTesting signals\n---------------\n\nIn production, the signal framework requires a working Redis and worker processes.\nHowever, if you only want to check if a signal has been called in unitary tests, you can use :class:`df_websockets.utils.SignalQueue`.\nBoth server-side and client-side signals are kept into memory:\n\n* `df_websockets.testing.SignalQueue.ws_signals`,\n\n    * keys are the serialized topics\n    * values are lists of tuples `(signal name, arguments as dict)`\n\n* `df_websockets.testing.SignalQueue.python_signals`\n\n    * keys are the name of the queue\n    * values are lists of `(signal_name, window_info_dict, kwargs=None, from_client=False, serialized_client_topics=None, to_server=False, queue=None)`\n\n      * `signal_name` is \u2026 the name of the signal\n      * `window_info_dict` is a WindowInfo serialized as a dict,\n      * `kwargs` is a dict representing the signal arguments,\n      * `from_client` is `True` if this signal has been emitted by a web browser,\n      * `serialized_client_topics` is not `None` if this signal must be re-emitted to some client topics,\n      * `to_server` is `True` if this signal must be processed server-side,\n      * `queue` is the name of the selected Celery queue.\n\n```python\nfrom df_websockets.tasks import trigger, SERVER\nfrom df_websockets.window_info import WindowInfo\nfrom df_websockets.testing import SignalQueue\n\nfrom df_websockets.decorators import signal\n# noinspection PyUnusedLocal\n@signal(path='test.signal', queue='demo-queue')\ndef test_signal(window_info, value=None):\n  print(value)\n\nwi = WindowInfo()\nwith SignalQueue() as fd:\n  trigger(wi, 'test.signal1', to=[SERVER, 1], value=\"value1\")\n  trigger(wi, 'test.signal2', to=[SERVER, 1], value=\"value2\")\n\n# fd.python_signals looks like {'demo-queue': [ ['test.signal1', {\u2026}, {'value': 'value1'}, False, None, True, None], \n# # ['test.signal2', {\u2026}, {'value': 'value2'}, False, None, True, None]]}\n# fd.ws_signals looks like {'-int.1': [('test.signal1', {'value': 'value1'}), ('test.signal2', {'value': 'value2'})]}\n```\n\nJavaScript signals\n------------------\n\nMany [JS signals](https://github.com/d9pouces/df_websockets/blob/master/npm/df_websockets/base.js) are available out-of-the-box.\nThese signals can be triggered either by the JS code or by the Python code.\n\nSignals must be defined in a Python file that is imported during Django's startup, or in any `signals.py` file inside a Django app (like `models.py`). \nFor example, you can update the content of a HTML node with the following lines:\n\n```python\nfrom df_websockets.tasks import trigger, WINDOW\nfrom df_websockets.decorators import signal\n\n@signal(path='test.signal', queue='demo-queue')\ndef test_signal(window_info, word=\"hellow\"):\n    trigger(window_info, 'html.content', to=WINDOW, selector=\"#obj\", content= \"<span>%s</span>\" % word)\n```\n\n```javascript\nwindow.DFSignals.call('html.content', {selector: \"#obj\", content: \"<span>hello</span>\"});\n```\n\nPlease read the content of `npm/df_websockets/base.js` for the whole list of available signals. \nYou can also create some shortcuts for the most common signals.\nAnother way is to run the demo:\n\n```bash\npython demo_manage.py runserver\n```\n\nChecklist \n---------\n\nEverything must be correctly setup to have working signals.\n\nThe first step is to test tasks from the command-line:\n\n1. Redis is running and accepting connections\n2. Celery is working\n2. at least one worker is running with all required queues\n3. the triggered signal is exists\n4. open a console `python manage.py shell` and manually trigger a task\n```python\nfrom df_websockets.tasks import trigger, SERVER\nfrom df_websockets.window_info import WindowInfo\ntrigger(WindowInfo(), 'test.signal', to=[SERVER], value=\"value2\")\n```\n\n\nThe second step is to check the web part:\n\n1. the web server must be running and accepting connections\n2. Celery is working\n2. at least one worker is running with all required queues\n3. the triggered signal exists\n3. `df_websockets.middleware.WebsocketMiddleware` is included\n4. check the used domain name, since tokens are passed through cookies: **\"localhost\" is different than \"127.0.0.1\"**\n5. `df_websockets.tasks.set_websocket_topics` is used somewhere in the view\n6. `static/js/df_websockets.min.js` is included in the page\n7. check if the WS tries to connect\n8. check if the WS is connected\n4. open a console `python manage.py shell` and manually trigger a task\n```python\nfrom df_websockets.tasks import trigger, BROADCAST\nfrom df_websockets.window_info import WindowInfo\ntrigger(WindowInfo(), 'html.text', to=[BROADCAST], selector=\"body\", content= \"<span>hello</span>\")\n```\n\nDo not hesitate to use a verbose logging:\n```python\nLOGGING = {\n    \"version\": 1,\n    \"disable_existing_loggers\": True,\n    \"formatters\": {\n        \"verbose\": {\n            \"format\": (\n                \"%(asctime)s [%(process)d] [%(levelname)s] \"\n                + \"pathname=%(pathname)s lineno=%(lineno)s \"\n                + \"funcname=%(funcName)s %(message)s\"\n            ),\n            \"datefmt\": \"%Y-%m-%d %H:%M:%S\",\n        },\n        \"django.server\": {\n            \"()\": \"django.utils.log.ServerFormatter\",\n        },\n        \"nocolor\": {\n            \"()\": \"logging.Formatter\",\n            \"fmt\": \"%(asctime)s [%(name)s] [%(levelname)s] %(message)s\",\n            \"datefmt\": \"%Y-%m-%d %H:%M:%S\",\n        },\n    },\n    \"filters\": {\n    },\n    \"handlers\": {\n        \"stdout.info\": {\n            \"class\": \"logging.StreamHandler\",\n            \"level\": \"DEBUG\",\n            \"stream\": \"ext://sys.stdout\",\n            \"formatter\": \"verbose\",\n        },\n        \"stderr.debug.django.server\": {\n            \"class\": \"logging.StreamHandler\",\n            \"level\": \"DEBUG\",\n            \"stream\": \"ext://sys.stderr\",\n            \"formatter\": \"django.server\",\n        },\n    },\n    \"loggers\": {\n        \"django\": {\"handlers\": [], \"level\": \"INFO\", \"propagate\": True},\n        \"django.db\": {\"handlers\": [], \"level\": \"INFO\", \"propagate\": True},\n        \"django.db.backends\": {\"handlers\": [], \"level\": \"INFO\", \"propagate\": True},\n        \"django.request\": {\"handlers\": [], \"level\": \"DEBUG\", \"propagate\": True},\n        \"django.security\": {\"handlers\": [], \"level\": \"INFO\", \"propagate\": True},\n        \"df_websockets.signals\": {\"handlers\": [], \"level\": \"DEBUG\", \"propagate\": True},\n        \"gunicorn.error\": {\"handlers\": [], \"level\": \"DEBUG\", \"propagate\": True},\n        \"pip.vcs\": {\"handlers\": [], \"level\": \"INFO\", \"propagate\": True},\n        \"py.warnings\": {\n            \"handlers\": [],\n            \"level\": \"INFO\",\n            \"propagate\": True,\n        },\n        \"daphne\": {\"handlers\": [], \"level\": \"INFO\", \"propagate\": True},\n        \"mail.log\": {\"handlers\": [], \"level\": \"INFO\", \"propagate\": True},\n        \"aiohttp.access\": {\n            \"handlers\": [\"stderr.debug.django.server\"],\n            \"level\": \"INFO\",\n            \"propagate\": False,\n        },\n        \"django.server\": {\n            \"handlers\": [\"stderr.debug.django.server\"],\n            \"level\": \"INFO\",\n            \"propagate\": False,\n        },\n        \"django.channels.server\": {\n            \"handlers\": [\"stderr.debug.django.server\"],\n            \"level\": \"INFO\",\n            \"propagate\": False,\n        },\n        \"gunicorn.access\": {\n            \"handlers\": [\"stderr.debug.django.server\"],\n            \"level\": \"INFO\",\n            \"propagate\": False,\n        },\n    },\n    \"root\": {\"handlers\": [\"stdout.info\"], \"level\": \"DEBUG\"},\n}\n\n```\n",
    "bugtrack_url": null,
    "license": "CeCILL-B",
    "summary": "Websocket integration for Django",
    "version": "1.0.9",
    "project_urls": {
        "Homepage": "https://github.com/d9pouces/df_websockets"
    },
    "split_keywords": [],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "7eafeebca1f8502f07175210f0daa87441e1b1f5cd85f72f0a7ea2fcb2b4d001",
                "md5": "c809912887f2eaf7d7e5a7c35cc7a895",
                "sha256": "91a29a0aaacdb941d593210d34509ba9213ef40f633e866a0e641e1ef1d5e3bd"
            },
            "downloads": -1,
            "filename": "df_websockets-1.0.9-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "c809912887f2eaf7d7e5a7c35cc7a895",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": null,
            "size": 88684,
            "upload_time": "2023-12-04T06:18:30",
            "upload_time_iso_8601": "2023-12-04T06:18:30.945118Z",
            "url": "https://files.pythonhosted.org/packages/7e/af/eebca1f8502f07175210f0daa87441e1b1f5cd85f72f0a7ea2fcb2b4d001/df_websockets-1.0.9-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "7e931334d49c08815a1b383a7f0fe6d1157c33174c680c2a6e4e6d546d8fdc86",
                "md5": "6e4c1e70390ef941880fdd74c690d386",
                "sha256": "585afb1d2762ee1e1828d97a07abcfd95780a693e2d9d27fb1c2d659508e48d8"
            },
            "downloads": -1,
            "filename": "df_websockets-1.0.9.tar.gz",
            "has_sig": false,
            "md5_digest": "6e4c1e70390ef941880fdd74c690d386",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": null,
            "size": 123083,
            "upload_time": "2023-12-04T06:18:33",
            "upload_time_iso_8601": "2023-12-04T06:18:33.178374Z",
            "url": "https://files.pythonhosted.org/packages/7e/93/1334d49c08815a1b383a7f0fe6d1157c33174c680c2a6e4e6d546d8fdc86/df_websockets-1.0.9.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2023-12-04 06:18:33",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "d9pouces",
    "github_project": "df_websockets",
    "travis_ci": true,
    "coveralls": false,
    "github_actions": true,
    "requirements": [],
    "lcname": "df-websockets"
}
        
Elapsed time: 0.29535s