dynamicforms-fastapi-viewsets 0.11.2__tar.gz → 0.11.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/PKG-INFO +3 -3
  2. dynamicforms_fastapi_viewsets-0.11.3/fastapi_viewsets/__init__.py +1 -0
  3. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/mux_ws/__init__.py +4 -10
  4. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/mux_ws/server.py +1 -4
  5. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/pyproject.toml +2 -2
  6. dynamicforms_fastapi_viewsets-0.11.2/fastapi_viewsets/__init__.py +0 -1
  7. dynamicforms_fastapi_viewsets-0.11.2/fastapi_viewsets/mux_ws/origin.py +0 -103
  8. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/.gitignore +0 -0
  9. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/LICENSE +0 -0
  10. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/README.md +0 -0
  11. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/action_configuration.py +0 -0
  12. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/backends/__init__.py +0 -0
  13. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/backends/django_orm.py +0 -0
  14. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/collection_viewset.py +0 -0
  15. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/conf.py +0 -0
  16. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/context/__init__.py +0 -0
  17. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/context/auth/__init__.py +0 -0
  18. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/context/auth/django.py +0 -0
  19. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/context/auth/jwt.py +0 -0
  20. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/context/auth/static.py +0 -0
  21. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/cursor.py +0 -0
  22. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/decorators/__init__.py +0 -0
  23. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/decorators/build_schema.py +0 -0
  24. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/decorators/celery_viewset/__init__.py +0 -0
  25. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/decorators/celery_viewset/client.py +0 -0
  26. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/decorators/celery_viewset/payload.py +0 -0
  27. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/decorators/celery_viewset/result_reader.py +0 -0
  28. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/decorators/celery_viewset/server.py +0 -0
  29. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/decorators/conftest.py +0 -0
  30. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/decorators/lifecycle_runner.py +0 -0
  31. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/decorators/primary_key_model_helper.py +0 -0
  32. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/decorators/route_viewset.py +0 -0
  33. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/endpoint_docs.py +0 -0
  34. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/events/__init__.py +0 -0
  35. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/events/authorizers.py +0 -0
  36. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/events/changes.py +0 -0
  37. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/events/conftest.py +0 -0
  38. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/events/delivery.py +0 -0
  39. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/events/event.py +0 -0
  40. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/events/lifecycle.py +0 -0
  41. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/events/redis.py +0 -0
  42. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/events/scopes.py +0 -0
  43. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/events/sources.py +0 -0
  44. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/events/store.py +0 -0
  45. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/events/topics.py +0 -0
  46. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/events/viewset.py +0 -0
  47. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/exceptions.py +0 -0
  48. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/filters/__init__.py +0 -0
  49. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/filters/base.py +0 -0
  50. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/filters/declaration.py +0 -0
  51. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/filters/operators.py +0 -0
  52. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/filters/registry.py +0 -0
  53. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/list_query.py +0 -0
  54. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/list_shapes.py +0 -0
  55. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/middleware/__init__.py +0 -0
  56. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/middleware/auth/__init__.py +0 -0
  57. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/middleware/auth/authorization.py +0 -0
  58. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/middleware/idempotency.py +0 -0
  59. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/middleware/rate_limiter.py +0 -0
  60. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/mixins.py +0 -0
  61. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/mux_ws/asgi.py +0 -0
  62. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/mux_ws/protocol.py +0 -0
  63. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/mux_ws/registry.py +0 -0
  64. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/mux_ws/transports.py +0 -0
  65. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/py.typed +0 -0
  66. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/response_classes.py +0 -0
  67. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/routing.py +0 -0
  68. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/save_state/__init__.py +0 -0
  69. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/save_state/save_state.py +0 -0
  70. {dynamicforms_fastapi_viewsets-0.11.2 → dynamicforms_fastapi_viewsets-0.11.3}/fastapi_viewsets/save_state/serialize_state.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: dynamicforms-fastapi-viewsets
3
- Version: 0.11.2
3
+ Version: 0.11.3
4
4
  Summary: Django REST Framework-style viewsets for FastAPI, with optional Celery-backed async execution and a Vue/TypeScript client counterpart.
5
5
  Project-URL: Homepage, https://github.com/dynamicforms/fastapi-viewsets
6
6
  Project-URL: Repository, https://github.com/dynamicforms/fastapi-viewsets
@@ -35,7 +35,7 @@ Requires-Dist: coverage; extra == 'dev'
35
35
  Requires-Dist: django; extra == 'dev'
36
36
  Requires-Dist: gevent; extra == 'dev'
37
37
  Requires-Dist: httpx2; extra == 'dev'
38
- Requires-Dist: muxws[starlette,websockets]>=0.4.1; extra == 'dev'
38
+ Requires-Dist: muxws[starlette,websockets]>=0.4.2; extra == 'dev'
39
39
  Requires-Dist: pyjwt; extra == 'dev'
40
40
  Requires-Dist: pytest; extra == 'dev'
41
41
  Requires-Dist: pytest-asyncio; extra == 'dev'
@@ -49,7 +49,7 @@ Requires-Dist: django>=4.2; extra == 'django'
49
49
  Provides-Extra: jwt
50
50
  Requires-Dist: pyjwt>=2.8; extra == 'jwt'
51
51
  Provides-Extra: muxws
52
- Requires-Dist: muxws[starlette]>=0.4.1; extra == 'muxws'
52
+ Requires-Dist: muxws[starlette]>=0.4.2; extra == 'muxws'
53
53
  Description-Content-Type: text/markdown
54
54
 
55
55
  # DynamicForms FastAPI Viewsets
@@ -0,0 +1 @@
1
+ __version__ = "0.11.3"
@@ -16,18 +16,14 @@ Server side:
16
16
  from fastapi import FastAPI, WebSocket
17
17
  from muxws import accept
18
18
 
19
- from fastapi_viewsets.mux_ws import origin_allowed, process_command
19
+ from fastapi_viewsets.mux_ws import process_command
20
20
 
21
21
  @app.websocket("/ws")
22
22
  async def muxws_endpoint(websocket: WebSocket) -> None:
23
- # A cookie-authenticated WebSocket has no CSRF protection: refuse a foreign Origin before
24
- # accept(), which the server answers with HTTP 403.
25
- if not origin_allowed(websocket):
26
- await websocket.close(code=1008)
27
- return
28
23
  # Do not call websocket.accept() yourself - muxws performs the upgrade, because only it
29
- # knows which muxws.v1.<codec> subprotocol to select.
30
- peer = await accept(websocket)
24
+ # knows which muxws.v1.<codec> subprotocol to select. A cookie-authenticated WebSocket has
25
+ # no CSRF protection: allowed_origins refuses a foreign Origin with HTTP 403.
26
+ peer = await accept(websocket, allowed_origins=["https://app.example"])
31
27
  peer.on_stream(
32
28
  lambda payload, stream: process_command(payload, stream, connection=websocket, peer=peer)
33
29
  )
@@ -38,7 +34,6 @@ Which viewsets get registered is decided at three levels, each able to defer to
38
34
  `settings.viewsets_register_muxws` globally (default True).
39
35
  """
40
36
 
41
- from .origin import origin_allowed
42
37
  from .protocol import METHOD_KEY, PATH_KEY, QUERY_KEY, STATUS_KEY
43
38
  from .registry import (
44
39
  is_registered,
@@ -58,7 +53,6 @@ __all__ = [
58
53
  "QUERY_KEY",
59
54
  "STATUS_KEY",
60
55
  "is_registered",
61
- "origin_allowed",
62
56
  "process_command",
63
57
  "register_viewset",
64
58
  "registered_viewsets",
@@ -76,10 +76,7 @@ async def process_command(
76
76
 
77
77
  @app.websocket("/ws")
78
78
  async def endpoint(websocket: WebSocket) -> None:
79
- if not origin_allowed(websocket):
80
- await websocket.close(code=1008) # before accept(): the handshake is answered with HTTP 403
81
- return
82
- peer = await accept(websocket)
79
+ peer = await accept(websocket, allowed_origins=["https://app.example"])
83
80
 
84
81
  async def on_stream(payload, stream):
85
82
  if await process_command(payload, stream, connection=websocket, peer=peer):
@@ -34,7 +34,7 @@ dependencies = [
34
34
 
35
35
  [project.optional-dependencies]
36
36
  celery = ["celery>=5.3", "redis>=5.0"]
37
- muxws = ["muxws[starlette]>=0.4.1"]
37
+ muxws = ["muxws[starlette]>=0.4.2"]
38
38
  django = ["django>=4.2", "asgiref>=3.6"]
39
39
  jwt = ["PyJWT>=2.8"]
40
40
  dev = [
@@ -49,7 +49,7 @@ dev = [
49
49
  "django",
50
50
  "asgiref",
51
51
  "PyJWT",
52
- "muxws[starlette,websockets]>=0.4.1",
52
+ "muxws[starlette,websockets]>=0.4.2",
53
53
  "ruff",
54
54
  "uvicorn",
55
55
  ]
@@ -1 +0,0 @@
1
- __version__ = "0.11.2"
@@ -1,103 +0,0 @@
1
- """
2
- `origin_allowed`, the `Origin` check of a WebSocket endpoint.
3
-
4
- A browser sends the cookies of the endpoint's site with a WebSocket handshake whatever page opened
5
- it, and a WebSocket has no CSRF protection of its own: an endpoint whose session is a cookie answers
6
- a handshake from another site's page with that session unless it checks `Origin` before `accept()`.
7
- """
8
-
9
- from collections.abc import Collection
10
- from typing import TYPE_CHECKING
11
- from urllib.parse import urlsplit
12
-
13
- if TYPE_CHECKING:
14
- from starlette.requests import HTTPConnection
15
-
16
- _DEFAULT_PORTS = {"http": 80, "https": 443}
17
-
18
-
19
- def _parse_origin(value: str) -> tuple[str, str, int] | None:
20
- """`(scheme, host, port)` of a serialized origin, `scheme://host[:port]` with an optional
21
- trailing `/`, scheme and host lower-cased and the port explicit; None for anything else."""
22
- text = value.strip()
23
- if text.endswith("/"):
24
- text = text[:-1]
25
- try:
26
- parts = urlsplit(text)
27
- port = parts.port
28
- except ValueError:
29
- return None
30
- scheme = parts.scheme.lower()
31
- if scheme not in _DEFAULT_PORTS or not parts.hostname:
32
- return None
33
- if parts.path or parts.query or parts.fragment or parts.username is not None or parts.password is not None:
34
- return None
35
- if "@" in parts.netloc:
36
- return None
37
- return scheme, parts.hostname.lower(), port if port is not None else _DEFAULT_PORTS[scheme]
38
-
39
-
40
- def _parse_host(value: str, default_port: int) -> tuple[str, int] | None:
41
- """`(host, port)` of a `Host` header value, the port `default_port` when it names none."""
42
- try:
43
- parts = urlsplit(f"//{value.strip()}")
44
- port = parts.port
45
- except ValueError:
46
- return None
47
- if not parts.hostname or parts.path or parts.query or parts.fragment or "@" in parts.netloc:
48
- return None
49
- return parts.hostname.lower(), port if port is not None else default_port
50
-
51
-
52
- def origin_allowed(connection: "HTTPConnection", allowed_origins: Collection[str] | None = None) -> bool:
53
- """
54
- Whether the `Origin` of `connection` (a `WebSocket` or a `Request`) may open the endpoint.
55
-
56
- - No `Origin` header (a client that is not a browser): True. More than one: False.
57
- - `Origin: null`, and an origin that is not `scheme://host[:port]` with the scheme `http` or
58
- `https` (a path, a query, userinfo, another scheme): False.
59
- - Origins compare as `(scheme, host, port)`, scheme and host lower-cased, the port the scheme's
60
- default (80, 443) when the origin names none, a trailing `/` removed.
61
- - With `allowed_origins`: True when the origin equals one of its entries. An entry that is not
62
- an origin raises `ValueError`.
63
- - Without it: True when the origin's `(host, port)` equals that of the request's host, the first
64
- item of `X-Forwarded-Host` when the request carries one, else `Host`; a host without a port
65
- takes the default port of the origin's scheme. The scheme is not compared, so a TLS proxy in
66
- front of the application passes. No host at all: False. A proxy that rewrites `Host` without
67
- setting `X-Forwarded-Host` needs `allowed_origins`.
68
-
69
- The endpoint calls it before `accept()` and closes a refused handshake, which the server answers
70
- with HTTP 403:
71
-
72
- if not origin_allowed(websocket):
73
- await websocket.close(code=1008)
74
- return
75
- peer = await accept(websocket)
76
- """
77
- allowed = None
78
- if allowed_origins is not None:
79
- allowed = set()
80
- for entry in allowed_origins:
81
- parsed = _parse_origin(entry)
82
- if parsed is None:
83
- raise ValueError(f"{entry!r} in allowed_origins is not an origin: scheme://host[:port]")
84
- allowed.add(parsed)
85
-
86
- values = connection.headers.getlist("origin")
87
- if not values:
88
- return True
89
- if len(values) > 1:
90
- return False
91
- origin = _parse_origin(values[0])
92
- if origin is None:
93
- return False
94
- if allowed is not None:
95
- return origin in allowed
96
-
97
- scheme, host, port = origin
98
- forwarded = connection.headers.get("x-forwarded-host")
99
- request_host = forwarded.split(",")[0] if forwarded else connection.headers.get("host")
100
- if not request_host or not request_host.strip():
101
- return False
102
- parsed_host = _parse_host(request_host, _DEFAULT_PORTS[scheme])
103
- return parsed_host == (host, port)