-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
0b6047f
commit 001d0ea
Showing
16 changed files
with
563 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
# This workflow will upload a Python Package using Twine when a release is created | ||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries | ||
|
||
# This workflow uses actions that are not certified by GitHub. | ||
# They are provided by a third-party and are governed by | ||
# separate terms of service, privacy policy, and support | ||
# documentation. | ||
|
||
name: Upload Python Package | ||
|
||
on: | ||
release: | ||
types: [published] | ||
|
||
permissions: | ||
contents: read | ||
|
||
jobs: | ||
deploy: | ||
|
||
runs-on: ubuntu-latest | ||
|
||
steps: | ||
- uses: actions/checkout@v3 | ||
- name: Set up Python | ||
uses: actions/setup-python@v3 | ||
with: | ||
python-version: '3.10' | ||
- name: Install dependencies | ||
run: | | ||
python -m pip install poetry | ||
- name: Build package | ||
run: poetry build | ||
- name: Publish package | ||
uses: pypa/gh-action-pypi-publish@release/v1 | ||
with: | ||
password: ${{ secrets.PYPI_API_TOKEN }} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
name: Runs tests | ||
|
||
on: | ||
pull_request: | ||
push: | ||
branches: | ||
- main | ||
|
||
jobs: | ||
runtests: | ||
runs-on: ubuntu-latest | ||
strategy: | ||
matrix: | ||
python-version: ['3.8', '3.9', '3.10' ] | ||
steps: | ||
- uses: actions/checkout@v3 | ||
- name: Set up Python ${{ matrix.python-version }} | ||
uses: actions/setup-python@v4 | ||
with: | ||
python-version: ${{ matrix.python-version }} | ||
- name: Install dependencies | ||
run: | | ||
pip install -r requirements-dev.txt | ||
- name: Run tests | ||
run: | | ||
tox |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
*.pyc | ||
*.egg | ||
*.egg-info | ||
|
||
docs/build | ||
dist | ||
build | ||
|
||
*.lock | ||
|
||
*.sqlite3 | ||
*.db | ||
|
||
*.DS_Store | ||
|
||
.cache | ||
__pycache__ | ||
.mypy_cache/ | ||
.pytest_cache/ | ||
.vscode/ | ||
.coverage | ||
docs/build | ||
|
||
node_modules/ | ||
|
||
*.bak | ||
|
||
logs | ||
*log | ||
npm-debug.log* | ||
|
||
# Translations | ||
# *.mo | ||
*.pot | ||
|
||
# Django media/static dirs | ||
media/ | ||
static/dist/ | ||
static/dev/ | ||
|
||
.ipython/ | ||
.env | ||
|
||
celerybeat.pid | ||
celerybeat-schedule | ||
|
||
# Common typos | ||
:w | ||
' | ||
.tox | ||
|
||
/venv/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
repos: | ||
- repo: https://github.com/pycqa/isort | ||
rev: 5.12.0 | ||
hooks: | ||
- id: isort | ||
- repo: https://github.com/psf/black | ||
rev: 23.10.1 | ||
hooks: | ||
- id: black | ||
- repo: https://github.com/PyCQA/flake8 | ||
rev: 6.0.0 | ||
hooks: | ||
- id: flake8 | ||
additional_dependencies: | ||
- flake8-bugbear | ||
- flake8-comprehensions | ||
- flake8-no-pep420 | ||
- flake8-print | ||
- flake8-tidy-imports | ||
- flake8-typing-imports | ||
- repo: https://github.com/pre-commit/mirrors-mypy | ||
rev: v1.6.1 | ||
hooks: | ||
- id: mypy |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
build: | ||
poetry build | ||
|
||
publish: | ||
poetry publish | ||
|
||
# poetry config repositories.testpypi https://test.pypi.org/legacy/ | ||
publish-test: | ||
poetry publish -r testpypi |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
[tool.poetry] | ||
name = "django-actioncable" | ||
version = "1.0.1" | ||
description = "This package provides Rails Action Cable support to Django" | ||
authors = ["Michael Yin <[email protected]>"] | ||
license = "MIT" | ||
readme = "README.md" | ||
packages = [{ include = "actioncable", from = "src" }] | ||
|
||
[tool.poetry.dependencies] | ||
python = ">=3.8" | ||
django = ">=3.0" | ||
channels = ">=3.0" | ||
|
||
[tool.poetry.dev-dependencies] | ||
|
||
[build-system] | ||
requires = [ | ||
"setuptools >= 61.0.0", | ||
"wheel" | ||
] | ||
build-backend = "setuptools.build_meta" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
pre-commit==2.9.2 | ||
tox==4.11.3 | ||
tox-gh-actions==3.1.3 | ||
|
||
django==4.2 # for local tests | ||
typing_extensions | ||
pytest | ||
pytest-django | ||
pytest-xdist | ||
pytest-mock | ||
jinja2 | ||
|
||
channels | ||
daphne | ||
pytest-asyncio | ||
channels_redis |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
[flake8] | ||
ignore = E203, E266, E501, W503, E231, E701, B950, B907 | ||
max-line-length = 88 | ||
max-complexity = 18 | ||
select = B,C,E,F,W,T4,B9 | ||
|
||
[isort] | ||
profile = black | ||
|
||
[mypy] | ||
python_version = 3.10 | ||
check_untyped_defs = False | ||
ignore_missing_imports = True | ||
warn_unused_ignores = False | ||
warn_redundant_casts = False | ||
warn_unused_configs = False | ||
|
||
[mypy-*.tests.*] | ||
ignore_errors = True | ||
|
||
[mypy-*.migrations.*] | ||
ignore_errors = True |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
from .utils import cable_broadcast | ||
from .registry import cable_channel_register | ||
from .consumer import ActionCableConsumer, CableChannel, compact_encode_json | ||
|
||
__all__ = [ | ||
"cable_broadcast", | ||
"cable_channel_register", | ||
"ActionCableConsumer", | ||
"CableChannel", | ||
"compact_encode_json", | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
import json | ||
import time | ||
import logging | ||
|
||
from collections import defaultdict | ||
import actioncable.registry | ||
import sys | ||
import os | ||
|
||
import asyncio | ||
from channels.generic.websocket import ( | ||
AsyncJsonWebsocketConsumer, | ||
) | ||
|
||
|
||
if "pytest" in sys.argv[0] or "TOX_ENV_NAME" in os.environ: | ||
TEST_ENV = True | ||
else: | ||
TEST_ENV = False | ||
|
||
|
||
LOGGER = logging.getLogger(__name__) | ||
|
||
|
||
def compact_encode_json(content): | ||
# compact encoding | ||
return json.dumps(content, separators=(",", ":"), sort_keys=True) | ||
|
||
|
||
class ActionCableConsumer(AsyncJsonWebsocketConsumer): | ||
""" | ||
Make Django Channels compatible with Rails ActionCable JS client | ||
""" | ||
def __init__(self, *args, **kwargs): | ||
super().__init__(args, kwargs) | ||
self.ping_task = None | ||
# the channel name to channel_cable_class | ||
self.channel_cls_dict = actioncable.registry.registered_classes | ||
self.identifier_to_channel_instance_map = {} | ||
self.group_channel_instance_map = defaultdict(set) | ||
|
||
async def connect(self): | ||
await self.accept("actioncable-v1-json") | ||
await self.send(text_data='{"type": "welcome"}') | ||
|
||
if not TEST_ENV: | ||
# periodically send ping message to the client | ||
# if the client did not receive the ping message within X seconds, it will try reconnect | ||
loop = asyncio.get_event_loop() | ||
self.ping_task = loop.create_task(self.send_ping()) | ||
|
||
async def disconnect(self, close_code): | ||
if not TEST_ENV: | ||
# Stop the ping task when disconnecting | ||
self.ping_task.cancel() | ||
|
||
await self.close() | ||
|
||
async def send_ping(self): | ||
while True: | ||
ping_message = {"type": "ping", "message": int(time.time())} | ||
await self.send_json(ping_message) # Send the ping message to the client | ||
await asyncio.sleep(5) # Wait for 5 seconds | ||
|
||
async def receive_json(self, content, **kwargs): | ||
command = content.get("command", None) | ||
identifier_dict = json.loads(content["identifier"]) | ||
unique_identifier_key = await self.encode_json(identifier_dict) | ||
|
||
# get or create channel instance for the identifier | ||
if unique_identifier_key in self.identifier_to_channel_instance_map: | ||
channel_instance = self.identifier_to_channel_instance_map[unique_identifier_key] | ||
else: | ||
channel_cls = self.channel_cls_dict[identifier_dict.get("channel")] | ||
channel_instance = channel_cls( | ||
consumer=self, | ||
identifier_key=unique_identifier_key, | ||
params=identifier_dict | ||
) | ||
self.identifier_to_channel_instance_map[unique_identifier_key] = channel_instance | ||
|
||
if command == "subscribe": | ||
await channel_instance.subscribe() | ||
await self.send_json( | ||
{ | ||
"identifier": await self.encode_json(identifier_dict), | ||
"type": "confirm_subscription", | ||
} | ||
) | ||
elif command == "unsubscribe": | ||
await channel_instance.unsubscribe() | ||
|
||
async def message(self, event): | ||
"""Send Turbo Stream HTML message back to the client""" | ||
group_name = event['group'] | ||
data = event['data'] | ||
|
||
if group_name in self.group_channel_instance_map: | ||
for channel_instance_unique_key in self.group_channel_instance_map[group_name]: | ||
# send message to all cable channels which subscribe to this group | ||
cable_channel_instance = self.identifier_to_channel_instance_map[channel_instance_unique_key] | ||
await self.send_json({ | ||
'identifier': cable_channel_instance.identifier_key, | ||
**data, | ||
}) | ||
else: | ||
LOGGER.warning("Group name %s not found in group_channel_instance_map", group_name) | ||
|
||
async def subscribe_group(self, group_name, cable_channel_instance): | ||
await self.channel_layer.group_add(group_name, self.channel_name) | ||
self.group_channel_instance_map[group_name].add(cable_channel_instance.identifier_key) | ||
|
||
async def unsubscribe_group(self, group_name, cable_channel_instance): | ||
has_other_cable_channel_subscribe = False | ||
if group_name in self.group_channel_instance_map: | ||
subscribed_channel_instance_keys = self.group_channel_instance_map[group_name] | ||
if len(subscribed_channel_instance_keys) > 1 and cable_channel_instance.identifier_key in subscribed_channel_instance_keys: | ||
has_other_cable_channel_subscribe = True | ||
self.group_channel_instance_map[group_name].discard(cable_channel_instance.identifier_key) | ||
|
||
if not has_other_cable_channel_subscribe: | ||
# if there is no other cable channel subscribe to this group | ||
# then we can let the consumer leave the group entirely | ||
await self.channel_layer.group_discard(group_name, self.channel_name) | ||
|
||
try: | ||
del self.identifier_to_channel_instance_map[cable_channel_instance.identifier_key] | ||
except KeyError: | ||
pass | ||
|
||
@classmethod | ||
async def encode_json(cls, content): | ||
return compact_encode_json(content) | ||
|
||
|
||
class CableChannel: | ||
|
||
def __init__(self, consumer: ActionCableConsumer, identifier_key, params=None): | ||
raise NotImplementedError("Please implement subscribe method") | ||
|
||
async def subscribe(self): | ||
""" | ||
callback to run when received subscribe command from the client | ||
""" | ||
raise NotImplementedError("Please implement subscribe method") | ||
|
||
async def unsubscribe(self): | ||
""" | ||
callback to run when received unsubscribe command from the client | ||
""" | ||
raise NotImplementedError("Please implement unsubscribe method") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import logging | ||
|
||
LOGGER = logging.getLogger(__name__) | ||
|
||
registered_classes = {} | ||
|
||
|
||
def cable_channel_register(cls): | ||
class_name = cls.__name__ | ||
if class_name in registered_classes: | ||
logging.warning(f"Class '{class_name}' is already registered in cable_channel_register and will be overwritten") | ||
registered_classes[class_name] = cls | ||
return cls |
Oops, something went wrong.