Skip to content

Commit

Permalink
Merge pull request #5 from Friskes/add-support-for-nested-URLRouters
Browse files Browse the repository at this point in the history
support for nested routers and refactoring
  • Loading branch information
Friskes authored Jun 25, 2024
2 parents 0179a83 + f260fc5 commit 4c0cba1
Show file tree
Hide file tree
Showing 15 changed files with 788 additions and 262 deletions.
44 changes: 22 additions & 22 deletions .github/workflows/publish-to-pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
# max-parallel: 5
matrix:
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
django-version: ['Django>=3.2,<3.3', 'Django>=4.0,<4.1', 'Django>=4.1,<4.2', 'Django>=4.2,<4.3']
django-version: ['Django>=4.0,<4.1', 'Django>=4.1,<4.2', 'Django>=4.2,<4.3']
platform: [
ubuntu-latest,
# macos-latest,
Expand Down Expand Up @@ -124,30 +124,30 @@ jobs:
- name: Publish distribution 📦 to PyPI
uses: pypa/gh-action-pypi-publish@release/v1

# publish-to-testpypi:
# name: Publish Python 🐍 distribution 📦 to TestPyPI
publish-to-testpypi:
name: Publish Python 🐍 distribution 📦 to TestPyPI

# if: github.repository_owner == 'Friskes' && github.event_name == 'push'
if: github.repository_owner == 'Friskes' && github.event_name == 'push'

# needs:
# - build
# runs-on: ubuntu-latest
needs:
- build
runs-on: ubuntu-latest

# environment:
# name: testpypi
# url: https://test.pypi.org/p/drf-spectacular-websocket
environment:
name: testpypi
url: https://test.pypi.org/p/drf-spectacular-websocket

# permissions:
# id-token: write
permissions:
id-token: write

# steps:
# - name: Download all the dists
# uses: actions/download-artifact@v4
# with:
# name: python-package-distributions
# path: dist/
steps:
- name: Download all the dists
uses: actions/download-artifact@v4
with:
name: python-package-distributions
path: dist/

# - name: Publish distribution 📦 to TestPyPI
# uses: pypa/gh-action-pypi-publish@release/v1
# with:
# repository-url: https://test.pypi.org/legacy/
- name: Publish distribution 📦 to TestPyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ trash
.ruff_cache
.mypy_cache
.coverage
htmlcov
7 changes: 7 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,20 @@
# https://verdantfox.com/blog/how-to-use-git-pre-commit-hooks-the-hard-way-and-the-easy-way
# https://docs.astral.sh/ruff/integrations/
# https://pre-commit.com/

# Commands:
# pre-commit run # Запустить pre-commit проверку для теста
# pre-commit install # Создать файл в директории .git/hooks/pre-commit

# После создания файла, при любом git commit будет запускаться pre-commit проверка.

# Если возникли проблемы с pre-commit и нужно срочно сделать commit то можно
# временно удалить проверку до выяснения проблемы с помощью:
# pre-commit uninstall

# Либо можно воспользоваться флагом при коммите (--no-verify или -n):
# git commit --no-verify -m "msg"

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
Expand Down
23 changes: 19 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ classifiers = [
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Framework :: Django :: 3.2",
"Framework :: Django :: 4.0",
"Framework :: Django :: 4.1",
"Framework :: Django :: 4.2",
Expand All @@ -48,15 +47,25 @@ dev = [
"pytest>=7.0.1",
"mypy>=1.10.0",
"pytest-cov>=5.0.0",
"djangorestframework-simplejwt>=5.3.0",
"channels-auth-token-middlewares>=1.1.0",
]
ci = [
"ruff==0.4.1",
"pytest>=7.0.1",
"mypy>=1.10.0",
"pytest-cov>=5.0.0",
"djangorestframework-simplejwt>=5.3.0",
"channels-auth-token-middlewares>=1.1.0",
]


# https://packaging.python.org/en/latest/guides/single-sourcing-package-version/#single-sourcing-the-version
# Получать номер версии из переменной __version__ из __init__.py пакета
# [tool.setuptools.dynamic]
# version = {attr="drf_spectacular_websocket.__version__"}


[project.urls]
Homepage = "https://github.com/Friskes/drf-spectacular-websocket"
Changelog = "https://github.com/Friskes/drf-spectacular-websocket/releases/"
Expand Down Expand Up @@ -183,6 +192,7 @@ extend-select = [
"SIM", # https://docs.astral.sh/ruff/rules/#flake8-simplify-sim
"TID", # https://docs.astral.sh/ruff/rules/#flake8-tidy-imports-tid
"TCH", # https://docs.astral.sh/ruff/rules/#flake8-type-checking-tch
"TD", # https://docs.astral.sh/ruff/rules/#flake8-todos-td
"INT", # https://docs.astral.sh/ruff/rules/#flake8-gettext-int
"FLY", # https://docs.astral.sh/ruff/rules/#flynt-fly
"PERF", # https://docs.astral.sh/ruff/rules/#perflint-perf
Expand All @@ -193,8 +203,8 @@ extend-select = [
]
ignore = [
# https://docs.astral.sh/ruff/rules/#pyflakes-f
"F401", # (не ругаться на неиспользуемые импорты)
"F841", # (не ругаться на неиспользуемые переменные)
# "F401", # (не ругаться на неиспользуемые импорты)
# "F841", # (не ругаться на неиспользуемые переменные)
"F403", # (не ругаться на использование from ... import *)
# https://docs.astral.sh/ruff/rules/#pyupgrade-up
"UP031", # (не ругаться на форматирование с помощью %s)
Expand All @@ -221,11 +231,16 @@ ignore = [
"RET503", # (не ругаться на отсутствие return None в конце функций)
# https://docs.astral.sh/ruff/rules/#flake8-blind-except-ble
"BLE001", # (не ругаться на обработку обычного Exception)
# https://docs.astral.sh/ruff/rules/#flake8-django-dj #! (ВРЕМЕННО)
# https://docs.astral.sh/ruff/rules/#flake8-django-dj
# https://docs.djangoproject.com/en/4.2/ref/models/fields/#null
# https://sentry.io/answers/django-difference-between-null-and-blank/
# https://www.django-rest-framework.org/api-guide/fields/#charfield
"DJ001", # (не ругаться на использование null в моделях для текстовых полей)
# https://docs.astral.sh/ruff/rules/#flake8-comprehensions-c4
# {} VS dict() # https://switowski.com/blog/dict-function-vs-literal-syntax/
"C408", # (не ругаться на использование классов коллекций вместо их литералов)
"TD001", # (не ругаться на использование FIXME и XXX)
"TD003", # (не ругаться на отсутствие ссылки на issues)
]
# Не давать исправлять эти ошибки в тултипе, и в том числе автоматически при линте через команду
unfixable = [
Expand Down
7 changes: 4 additions & 3 deletions src/drf_spectacular_websocket/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
from .schemas.consumer_schema import ConsumerAutoSchema

if TYPE_CHECKING:
from src.drf_spectacular_websocket.types import DecoratedCallable, _Type
from src.drf_spectacular_websocket.types import DecoratedCallable

from drf_spectacular_websocket.types import WsMethod

__all__ = ('extend_ws_schema',)


def extend_ws_schema(
type: _Type = 'send', # noqa: A002
type: WsMethod = 'send', # noqa: A002
**kwargs: Any,
) -> Callable[[DecoratedCallable], DecoratedCallable]:
"""
Expand All @@ -35,7 +37,6 @@ def decorator(func: DecoratedCallable) -> DecoratedCallable:
func.schema = ConsumerAutoSchema # type: ignore[attr-defined]
func.type = type # type: ignore[attr-defined]
func.event = func.__name__ # type: ignore[attr-defined]
func.include_event = False # type: ignore[attr-defined]
return extend_schema(type, **kwargs)(func)

return decorator
2 changes: 1 addition & 1 deletion src/drf_spectacular_websocket/schemas/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from .schema import WsSchemaGenerator
from .schema import WsSchemaGenerator # noqa: F401
78 changes: 11 additions & 67 deletions src/drf_spectacular_websocket/schemas/consumer_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,22 @@
build_media_type_object,
force_instance,
is_list_serializer,
is_serializer,
)
from inflection import camelize
from rest_framework import status
from rest_framework.parsers import JSONParser
from rest_framework.renderers import JSONRenderer
from rest_framework.serializers import CharField, Serializer
from rest_framework.serializers import Serializer

if TYPE_CHECKING:
from drf_spectacular.utils import Direction, _SchemaType

from drf_spectacular_websocket.types import _Type
from drf_spectacular_websocket.types import WsMethod


class EmptySerializer(Serializer):
pass


class NotReadyError(Exception):
pass


class EventHandler:
request = None
kwargs: dict[str, Any] = {}
Expand All @@ -50,21 +44,20 @@ def determine_version(self, *args: Any, **kwargs: Any) -> tuple[None, None]:
class ConsumerAutoSchema(AutoSchema):
""""""

method: _Type
method: WsMethod
method_name: str
event: str
include_event: bool

def __init__(self) -> None:
super().__init__()
self.prepared: dict[str, dict[str, Serializer]] = {'request': {}, 'response': {}}
self.prepared: dict[Direction, dict[str, Serializer]] = {'request': {}, 'response': {}}

@property
def view(self) -> EventHandler:
return EventHandler(name=self.event)

def get_operation_id(self) -> str:
return '%s_%s' % (self.method, self.method_name)
return f'{self.method}_{self.method_name}'

def get_request_body(self, serializer: Serializer) -> dict[str, dict[str, _SchemaType]] | None:
""""""
Expand Down Expand Up @@ -103,7 +96,7 @@ def _get_request_for_media_type(

def get_response_bodies(
self, response_serializers: Serializer | dict[int, Serializer]
) -> dict[str, Any] | None:
) -> _SchemaType | None:
""""""
if not response_serializers:
return None
Expand Down Expand Up @@ -133,74 +126,25 @@ def _get_serializer_name(
def get_tags(self) -> list[str]:
return ['web_socket']

def get_summary(self) -> str:
return ''

def _force_ws_serializer(
self, serializer: Serializer, serializer_type: Direction, direction: _Type | None = None
self, serializer: Serializer, serializer_type: Direction, direction: WsMethod | None = None
) -> dict[str, Serializer]:
""""""
if serializer is None:
return {self.event: force_instance(EmptySerializer)}

serializer = force_instance(serializer)

if isinstance(serializer, EmptySerializer):
return {self.event: serializer}

if direction is None:
direction = self.method

if isinstance(serializer, list):
return self._force_serializers_list(events=serializer, serializer_type=serializer_type)

if self.event in self.prepared[serializer_type]:
return {self.event: self.prepared[serializer_type][self.event]}

if is_list_serializer(serializer):
serializer_name = serializer.child.__class__.__name__
elif is_serializer(serializer):
serializer_name = serializer.__class__.__name__
else:
raise AssertionError('Invalid type of serializer')

name: str = self._get_forced_serializer_name(
direction=direction, serializer_name=serializer_name
)
inner_name: str = f'Ws{self.event.capitalize()}DataSerializer'

if is_list_serializer(serializer):
inner_name: str = 'Ws%sDataSerializer' % self.event.capitalize()
serializer = type(inner_name, (Serializer,), {self.event: serializer})()

attrs = {
'event': CharField(default=self.event),
'data': serializer,
}
else:
attrs = {
'event': CharField(default=self.event),
'data': serializer,
}

prepared = type(name, (Serializer,), attrs)() if self.include_event else serializer

self.prepared[serializer_type][self.event] = prepared
return {self.event: prepared}

def _force_serializers_list(self, events: list[str], serializer_type: str) -> dict[str, Serializer]:
result: dict[str, Serializer] = {}

for event in events:
forced = self.prepared[serializer_type].get(event)
if forced is None:
raise NotReadyError
result[event] = forced

return result

def _get_forced_serializer_name(self, direction: str, serializer_name: str) -> str:
name: str = 'Ws%s' % direction.capitalize()
event: str = camelize(self.event)
if serializer_name.startswith(event):
return '%s%s' % (name, serializer_name)

return '%s%s%s' % (name, event, serializer_name)
self.prepared[serializer_type][self.event] = serializer
return {self.event: serializer}
Loading

0 comments on commit 4c0cba1

Please sign in to comment.