serverwamp 1.2.0__tar.gz → 2.0.0__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 (29) hide show
  1. {serverwamp-1.2.0 → serverwamp-2.0.0}/PKG-INFO +18 -12
  2. {serverwamp-1.2.0 → serverwamp-2.0.0}/README.md +11 -3
  3. serverwamp-2.0.0/docs/changelog.rst +264 -0
  4. {serverwamp-1.2.0 → serverwamp-2.0.0}/pyproject.toml +29 -12
  5. serverwamp-2.0.0/serverwamp/__init__.py +17 -0
  6. {serverwamp-1.2.0 → serverwamp-2.0.0}/serverwamp/adapters/aiohttp.py +59 -36
  7. {serverwamp-1.2.0 → serverwamp-2.0.0}/serverwamp/adapters/anyio.py +7 -10
  8. {serverwamp-1.2.0 → serverwamp-2.0.0}/serverwamp/adapters/asgi.py +96 -56
  9. {serverwamp-1.2.0 → serverwamp-2.0.0}/serverwamp/adapters/async_base.py +3 -7
  10. serverwamp-2.0.0/serverwamp/adapters/asyncio.py +57 -0
  11. {serverwamp-1.2.0 → serverwamp-2.0.0}/serverwamp/adapters/trio.py +4 -4
  12. serverwamp-2.0.0/serverwamp/application.py +888 -0
  13. {serverwamp-1.2.0 → serverwamp-2.0.0}/serverwamp/connection.py +19 -4
  14. {serverwamp-1.2.0 → serverwamp-2.0.0}/serverwamp/events.py +16 -6
  15. {serverwamp-1.2.0 → serverwamp-2.0.0}/serverwamp/helpers.py +19 -2
  16. {serverwamp-1.2.0 → serverwamp-2.0.0}/serverwamp/json.py +21 -5
  17. {serverwamp-1.2.0 → serverwamp-2.0.0}/serverwamp/protocol.py +107 -70
  18. {serverwamp-1.2.0 → serverwamp-2.0.0}/serverwamp/routing.py +7 -7
  19. {serverwamp-1.2.0 → serverwamp-2.0.0}/serverwamp/rpc.py +109 -54
  20. {serverwamp-1.2.0 → serverwamp-2.0.0}/serverwamp/session.py +92 -26
  21. serverwamp-1.2.0/CHANGELOG.md +0 -89
  22. serverwamp-1.2.0/serverwamp/__init__.py +0 -15
  23. serverwamp-1.2.0/serverwamp/adapters/asyncio.py +0 -52
  24. serverwamp-1.2.0/serverwamp/application.py +0 -667
  25. serverwamp-1.2.0/serverwamp/context.py +0 -145
  26. {serverwamp-1.2.0 → serverwamp-2.0.0}/.gitignore +0 -0
  27. {serverwamp-1.2.0 → serverwamp-2.0.0}/LICENSE.md +0 -0
  28. {serverwamp-1.2.0 → serverwamp-2.0.0}/serverwamp/adapters/__init__.py +0 -0
  29. {serverwamp-1.2.0 → serverwamp-2.0.0}/serverwamp/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: serverwamp
3
- Version: 1.2.0
3
+ Version: 2.0.0
4
4
  Summary: Components that add Web Application Messaging Protocol features to WebSocket servers.
5
5
  Project-URL: Homepage, https://github.com/JustinTArthur/serverwamp
6
6
  Project-URL: Documentation, https://serverwamp.readthedocs.io/
@@ -14,11 +14,6 @@ Classifier: Framework :: Trio
14
14
  Classifier: Intended Audience :: Developers
15
15
  Classifier: Programming Language :: Python
16
16
  Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3.6
18
- Classifier: Programming Language :: Python :: 3.7
19
- Classifier: Programming Language :: Python :: 3.8
20
- Classifier: Programming Language :: Python :: 3.9
21
- Classifier: Programming Language :: Python :: 3.10
22
17
  Classifier: Programming Language :: Python :: 3.11
23
18
  Classifier: Programming Language :: Python :: 3.12
24
19
  Classifier: Programming Language :: Python :: 3.13
@@ -26,11 +21,14 @@ Classifier: Programming Language :: Python :: 3.14
26
21
  Classifier: Programming Language :: Python :: 3.15
27
22
  Classifier: Topic :: Internet
28
23
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
29
- Requires-Python: >=3.6
30
- Requires-Dist: dataclasses~=0.6; python_version < '3.7'
24
+ Requires-Python: >=3.11
31
25
  Requires-Dist: msgpack<2,>=1.0.0
26
+ Provides-Extra: anyio
27
+ Requires-Dist: anyio>=4.0; extra == 'anyio'
32
28
  Provides-Extra: docs
33
- Requires-Dist: sphinx<10,>=9.1; extra == 'docs'
29
+ Requires-Dist: sphinx<10,>=5.3; extra == 'docs'
30
+ Provides-Extra: trio
31
+ Requires-Dist: trio>=0.25.0; extra == 'trio'
34
32
  Description-Content-Type: text/markdown
35
33
 
36
34
  # serverwamp
@@ -70,7 +68,6 @@ async def delete_doc(document_id):
70
68
  @docs_events.route('docs.changes')
71
69
  async def subscribe_to_doc_changes(session, changes_subscribers):
72
70
  changes_subscribers.add(session)
73
- session.send_event(args=('Thanks for subscribing!',))
74
71
  yield
75
72
  changes_subscribers.remove(session)
76
73
 
@@ -79,8 +76,12 @@ async def server_context(app):
79
76
 
80
77
  async def watch_for_docs_changes():
81
78
  async for event in my_db.watch_changes():
82
- for session in app['changes_subscribers']:
83
- await session.send_event(f'Document changed: {event}')
79
+ # A copy, as sessions can unsubscribe while events are sent.
80
+ for session in list(app['changes_subscribers']):
81
+ await session.send_event(
82
+ 'docs.changes',
83
+ args=(f'Document changed: {event}',)
84
+ )
84
85
 
85
86
  watcher = asyncio.create_task(watch_for_docs_changes())
86
87
  try:
@@ -113,3 +114,8 @@ Unit tests can be run with:
113
114
 
114
115
  pip install -e . --group ci
115
116
  pytest
117
+
118
+ Linting and type checking can be run with:
119
+
120
+ ruff check serverwamp tests examples
121
+ mypy serverwamp
@@ -35,7 +35,6 @@ async def delete_doc(document_id):
35
35
  @docs_events.route('docs.changes')
36
36
  async def subscribe_to_doc_changes(session, changes_subscribers):
37
37
  changes_subscribers.add(session)
38
- session.send_event(args=('Thanks for subscribing!',))
39
38
  yield
40
39
  changes_subscribers.remove(session)
41
40
 
@@ -44,8 +43,12 @@ async def server_context(app):
44
43
 
45
44
  async def watch_for_docs_changes():
46
45
  async for event in my_db.watch_changes():
47
- for session in app['changes_subscribers']:
48
- await session.send_event(f'Document changed: {event}')
46
+ # A copy, as sessions can unsubscribe while events are sent.
47
+ for session in list(app['changes_subscribers']):
48
+ await session.send_event(
49
+ 'docs.changes',
50
+ args=(f'Document changed: {event}',)
51
+ )
49
52
 
50
53
  watcher = asyncio.create_task(watch_for_docs_changes())
51
54
  try:
@@ -78,3 +81,8 @@ Unit tests can be run with:
78
81
 
79
82
  pip install -e . --group ci
80
83
  pytest
84
+
85
+ Linting and type checking can be run with:
86
+
87
+ ruff check serverwamp tests examples
88
+ mypy serverwamp
@@ -0,0 +1,264 @@
1
+ Changelog
2
+ =========
3
+
4
+ .. _serverwamp-changelog-2.0.0:
5
+
6
+ 2.0.0
7
+ -----
8
+
9
+ * Rename session state handlers to session lifespan handlers
10
+ (``add_session_lifespan_handler()``, ``SessionLifespanHandler``,
11
+ ``Realm.session_lifespan_handlers`` and
12
+ ``authed_session_lifespan_handlers``). The old names are deprecated.
13
+ * Fix session lifespan and subscription handlers getting arguments in the wrong
14
+ parameters when one isn't ``session``, a default arg, or a subscription
15
+ handler's ``topic_uri``. Such a parameter now keeps its default value, or the
16
+ handler fails with a ``TypeError`` naming it.
17
+ * Answer subscription handler errors with ``wamp.error.runtime_error``, and end
18
+ sessions whose session lifespan handlers fail to start with that reason,
19
+ instead of failing the connection without a reply. Errors are logged, and no
20
+ longer stop other teardown.
21
+ * Send events for a subscription only after its SUBSCRIBED. Events sent while
22
+ its handler runs, such as a first event from the handler, are held until
23
+ then.
24
+ * Add ``serverwamp.SubscribeError`` for subscription handlers to raise to
25
+ answer a SUBSCRIBE with an error, such as ``wamp.error.not_authorized``.
26
+ * Require Python 3.11 or newer. ``serverwamp.context``, a backport of
27
+ ``contextlib.asynccontextmanager`` for older Pythons, is removed.
28
+ * Build the asyncio support on ``asyncio.TaskGroup``. As with Trio and AnyIO,
29
+ task group failures, including unhandled errors while handling a connection,
30
+ are raised as an ``ExceptionGroup`` instead of the first error alone, and a
31
+ failing task interrupts the code that started the group.
32
+ * Support current AnyIO and Trio releases, now requiring AnyIO 4 or newer and
33
+ Trio 0.25 or newer. They can be installed with serverwamp's ``anyio`` and
34
+ ``trio`` extras.
35
+ * Abort sessions for protocol errors with ``wamp.error.protocol_violation``,
36
+ per WAMP spec, instead of ``wamp.error.protocol_error``.
37
+ * Handle these client mistakes instead of failing in the connection handler:
38
+
39
+ * WebSocket messages that aren't valid JSON or MessagePack, WAMP messages
40
+ that are too short or have fields of the wrong type, and HELLO after the
41
+ session is established abort the session with a protocol violation. Message
42
+ types must be integers, so ``1.0`` and ``true`` are no longer accepted as
43
+ HELLO.
44
+ * UNSUBSCRIBE for a subscription the session doesn't have is answered with
45
+ ``wamp.error.no_such_subscription``.
46
+ * SUBSCRIBE for a topic without a route is answered with
47
+ ``wamp.error.invalid_uri``.
48
+ * HELLO with a null ``authmethods`` that no transport authenticator
49
+ identifies is aborted with ``wamp.error.authentication_failed``.
50
+
51
+ * Fix GOODBYE details and reason being read from the wrong fields.
52
+ ``WAMPGoodbyeRequest`` no longer has a ``request_id``.
53
+ * Give GOODBYE messages a reason instead of none. Replies to a client's GOODBYE
54
+ and ``WAMPSession.close()`` without a reason use
55
+ ``wamp.close.goodbye_and_out``, and ``Application.close_sessions()`` uses
56
+ ``wamp.close.system_shutdown``.
57
+ * End sessions once GOODBYE has been exchanged, instead of when the client
58
+ disconnects. Subscription and session lifespan handler teardown run then, and
59
+ the client can start a new session on the same connection with HELLO, as the
60
+ WAMP spec allows.
61
+
62
+ * After the session sends GOODBYE, messages from the client other than its
63
+ GOODBYE in reply are ignored, per WAMP spec.
64
+ * Sending for a session that has ended raises
65
+ ``serverwamp.ConnectionClosed``, including from call handlers still running
66
+ and from ``send_event()``.
67
+ * ``Application.handle_session()`` returns whether the session ended with
68
+ GOODBYE.
69
+
70
+ * Fix prefix and regex routes handling URIs they don't match. A call or
71
+ subscription for a URI without an exact route went to the first prefix or
72
+ regex route added.
73
+
74
+ * Applications relying on this should add a catch-all route such as
75
+ ``prefix_route('')`` after their other prefix and regex routes.
76
+
77
+ * Answer SUBSCRIBE for a topic the session is already subscribed to with the
78
+ existing subscription, without running the subscription handler again.
79
+ * Fix RPCs that finish after their client disconnected logging errors,
80
+ cancelling the session's other calls, and keeping the session in memory.
81
+
82
+ * Sends to a disconnected client raise ``serverwamp.ConnectionClosed``, a
83
+ ``ConnectionError``, instead of the transport's own exception.
84
+ * ``WAMPSession.close()`` and ``abort()`` succeed when the client already
85
+ left.
86
+ * Unhandled exceptions from a session's tasks are logged. With Trio or AnyIO,
87
+ they no longer end the session.
88
+
89
+ * Run progressive RPC handlers' ``finally`` blocks as soon as their call is
90
+ handled, instead of at garbage collection.
91
+ * Wait for tasks started with ``WAMPSession.spawn_task()`` while a session
92
+ ends, instead of cancelling them.
93
+ * Fix sessions never being cleaned up with Trio. Teardown after ``yield`` in
94
+ subscription and session lifespan handlers now runs, and ended sessions are
95
+ no longer kept in memory.
96
+ * Fix ASGI apps failing on anything other than a WebSocket. HTTP requests are
97
+ answered with 426 Upgrade Required without an error, and lifespan startup and
98
+ shutdown are acknowledged, so an app from ``asgi_application()`` can be
99
+ served directly.
100
+ * Rename ``serverwamp.adapters.asgi.ASGIWebSocketConection`` to
101
+ ``ASGIWebSocketConnection``.
102
+ * Reject WebSocket connections that offer none of the supported WAMP
103
+ subprotocols, with 400 Bad Request with aiohttp and 403 Forbidden from ASGI
104
+ servers. aiohttp upgraded them and then failed in the request handler, and
105
+ ASGI accepted them with ``wamp.2.msgpack.batched``, which the client hadn't
106
+ offered.
107
+ * Fix how RPC handlers' parameters are filled:
108
+
109
+ * Handlers given to ``set_rpc_handler()`` failed on every call unless their
110
+ parameter was named ``rpc_request``.
111
+ * Route handlers with keyword-only parameters failed on every call. ``*args``
112
+ and ``**kwargs`` now get the call's leftover arguments.
113
+ * Callers could override default args. Parameters named for a default arg now
114
+ always get its value.
115
+
116
+ * Answer RPC arguments that can't be converted to their parameter's annotated
117
+ type with ``wamp.error.invalid_argument``, per WAMP spec, instead of
118
+ ``wamp.error.runtime_error``. Only class annotations are converted to, so
119
+ parameters annotated like ``int | None`` get arguments as they are instead of
120
+ failing, and parameters' own default values are no longer converted.
121
+
122
+ .. _serverwamp-changelog-1.2.0:
123
+
124
+ 1.2.0
125
+ -----
126
+
127
+ * Fix the default realm never being used. Sessions requesting a realm that
128
+ wasn't added with ``add_realm`` now join the default realm as documented
129
+ instead of being aborted with ``wamp.error.no_such_realm``.
130
+
131
+ * To reject realms that weren't added, use
132
+ ``Application(allow_default_realm=False)``. Configuring the default realm
133
+ through the application (e.g. ``add_rpc_routes``) then raises a
134
+ ``RuntimeError`` instead of an ``AttributeError``.
135
+
136
+ * Fix single RPC return values (e.g. ``return 'hello'``) being sent as the
137
+ result arguments themselves instead of as a single-element argument list.
138
+
139
+ * Returning ``None`` still sends a result without arguments. Return ``None,``
140
+ to send ``None`` as the single argument.
141
+
142
+ * Support Python 3.14 and 3.15, and allow msgpack 1.x releases newer than 1.0.
143
+ * Package with pyproject.toml and hatchling.
144
+ * Fix aiohttp logging request handler errors when the server ended a
145
+ connection.
146
+ * Fix ``wamp.2.json.batched`` sessions being aborted with a protocol error, and
147
+ batched JSON messages containing line breaks being dropped.
148
+ * Fix ASGI ``wamp.2.json`` sessions failing on the first message sent to the
149
+ client.
150
+ * Fix ``Application(synchronize_requests=True)`` failing every session on its
151
+ first request after the welcome.
152
+ * Fix messages batched in the same WebSocket message as a client's HELLO being
153
+ dropped.
154
+ * Declare public names in ``__all__`` so strict type checkers accept imports
155
+ such as ``serverwamp.Application``.
156
+
157
+ .. _serverwamp-changelog-1.1.2:
158
+
159
+ 1.1.2
160
+ -----
161
+
162
+ * Fix standard library json support.
163
+
164
+ .. _serverwamp-changelog-1.1.1:
165
+
166
+ 1.1.1
167
+ -----
168
+
169
+ * Support session close goodbye message.
170
+
171
+ .. _serverwamp-changelog-1.0.7:
172
+
173
+ 1.0.7
174
+ -----
175
+
176
+ * Fix using deprecated collections.Sequence.
177
+ * WAMP-compliant msgpack batch processing
178
+
179
+ * Fixes Autobahn|Python out-of-box compatibility.
180
+
181
+ * Allow unauthenticated sessions if no authenticators configured.
182
+
183
+ * ``session.identity`` is ``None`` in this case.
184
+
185
+ .. _serverwamp-changelog-1.0.6:
186
+
187
+ 1.0.6
188
+ -----
189
+
190
+ * Server now responds with error when unsubscribing from non-subscribed topic.
191
+ * Temporarily synchronize subscribe/unsubscribe messages.
192
+
193
+ * Synchronization will be made configurable by request type in a future
194
+ version.
195
+
196
+ * Fix for exception raised in trying to process protocol messages from a
197
+ canceled connection.
198
+
199
+ .. _serverwamp-changelog-1.0.5:
200
+
201
+ 1.0.5
202
+ -----
203
+
204
+ * Fix error in SCRAM nonce generation
205
+ * Fix lack of response to clients' unsubscribe requests
206
+ * Fix for exception raised in trying to process protocol messages from a
207
+ canceled connection.
208
+
209
+ .. _serverwamp-changelog-1.0.4:
210
+
211
+ 1.0.4
212
+ -----
213
+
214
+ * aiohttp sessions now cancellable by server shutdown
215
+ * Session cleanup operations specifically shielded from cancellation
216
+ * Added Application.close_sessions method (undocumented)
217
+
218
+ .. _serverwamp-changelog-1.0.3:
219
+
220
+ 1.0.3
221
+ -----
222
+
223
+ * Fix for plain topic routes raising exception during registration. `Issue #4
224
+ <https://github.com/JustinTArthur/serverwamp/issues/4>`_
225
+ * Exceptions in user-supplied RPC code no longer crash the app, but instead
226
+ return a wamp.error.runtime_error to the caller like Crossbar.io's toolchain.
227
+
228
+ .. _serverwamp-changelog-1.0.2:
229
+
230
+ 1.0.2
231
+ -----
232
+
233
+ * Add "http_headers_raw" transport_info item for aiohttp and ASGI connections.
234
+
235
+ .. _serverwamp-changelog-1.0.1:
236
+
237
+ 1.0.1
238
+ -----
239
+
240
+ * Fix stopasynciteration exception on session close with outstanding
241
+ subscriptions.
242
+
243
+ .. _serverwamp-changelog-1.0.0:
244
+
245
+ 1.0.0
246
+ -----
247
+
248
+ Brand new API with…
249
+
250
+ * Support for multiple realms w/ separate handlers
251
+ * Context-manager-friendly subscription handlers
252
+ * Session state handlers.
253
+ * Routes configuration for topics similar to RPC.
254
+ * Slightly more structured concurrency on the asyncio side.
255
+ * Transport authenticators, ticket authenticators, and CRA auth.
256
+
257
+ Released 2020-05-07 alongside new documentation.
258
+
259
+ .. _serverwamp-changelog-0.2.3:
260
+
261
+ 0.2.3
262
+ -----
263
+
264
+ Transport auth, RPC and subscriptions now working.
@@ -4,11 +4,11 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "serverwamp"
7
- version = "1.2.0"
7
+ version = "2.0.0"
8
8
  description = "Components that add Web Application Messaging Protocol features to WebSocket servers."
9
9
  readme = "README.md"
10
10
  license = "MIT"
11
- requires-python = ">=3.6"
11
+ requires-python = ">=3.11"
12
12
  authors = [
13
13
  { name = "Justin Turner Arthur", email = "justinarthur@gmail.com" },
14
14
  ]
@@ -19,11 +19,6 @@ keywords = [
19
19
  classifiers = [
20
20
  "Programming Language :: Python",
21
21
  "Programming Language :: Python :: 3",
22
- "Programming Language :: Python :: 3.6",
23
- "Programming Language :: Python :: 3.7",
24
- "Programming Language :: Python :: 3.8",
25
- "Programming Language :: Python :: 3.9",
26
- "Programming Language :: Python :: 3.10",
27
22
  "Programming Language :: Python :: 3.11",
28
23
  "Programming Language :: Python :: 3.12",
29
24
  "Programming Language :: Python :: 3.13",
@@ -37,13 +32,18 @@ classifiers = [
37
32
  "Framework :: Trio",
38
33
  ]
39
34
  dependencies = [
40
- "dataclasses~=0.6;python_version<'3.7'",
41
35
  "msgpack>=1.0.0,<2",
42
36
  ]
43
37
 
44
38
  [project.optional-dependencies]
39
+ anyio = [
40
+ "anyio>=4.0",
41
+ ]
42
+ trio = [
43
+ "trio>=0.25.0",
44
+ ]
45
45
  docs = [
46
- "Sphinx>=9.1,<10",
46
+ "Sphinx>=5.3,<10",
47
47
  ]
48
48
 
49
49
  [project.urls]
@@ -53,15 +53,32 @@ Documentation = "https://serverwamp.readthedocs.io/"
53
53
  [dependency-groups]
54
54
  ci = [
55
55
  "aiohttp>=3.0.0",
56
+ "anyio>=4.0",
57
+ "msgpack-types>=0.8.0",
56
58
  "mypy>=0.770",
57
- "pytest>=5.4.1",
58
- "trio>=0.12.0",
59
+ "pytest>=8.0",
60
+ "ruff>=0.17.1,<0.18",
61
+ "trio>=0.25.0",
59
62
  ]
60
63
 
64
+ [tool.ruff]
65
+ line-length = 99
66
+
67
+ [tool.ruff.lint]
68
+ # Ruff's default rules, as of 0.17, are far broader than these.
69
+ select = ["E4", "E7", "E9", "F", "E501", "I", "Q", "UP"]
70
+
71
+ [tool.ruff.lint.flake8-quotes]
72
+ inline-quotes = "single"
73
+
74
+ [tool.ruff.lint.isort]
75
+ # Helper modules the tests import from their own directory.
76
+ known-local-folder = ["memory_connection", "stream_connection"]
77
+
61
78
  [tool.hatch.build.targets.sdist]
62
79
  include = [
63
80
  "/serverwamp",
64
- "/CHANGELOG.md",
81
+ "/docs/changelog.rst",
65
82
  "/LICENSE.md",
66
83
  "/README.md",
67
84
  ]
@@ -0,0 +1,17 @@
1
+ from serverwamp.application import Application, Realm
2
+ from serverwamp.connection import ConnectionClosed
3
+ from serverwamp.events import SubscribeError, TopicRouteSet
4
+ from serverwamp.rpc import RPCErrorResult, RPCProgressReport, RPCRequest, RPCResult, RPCRouteSet
5
+
6
+ __all__ = (
7
+ 'Application',
8
+ 'ConnectionClosed',
9
+ 'Realm',
10
+ 'RPCErrorResult',
11
+ 'RPCProgressReport',
12
+ 'RPCRequest',
13
+ 'RPCResult',
14
+ 'RPCRouteSet',
15
+ 'SubscribeError',
16
+ 'TopicRouteSet',
17
+ )
@@ -1,16 +1,15 @@
1
1
  from abc import ABCMeta
2
+ from collections.abc import Mapping
2
3
  from ssl import SSLObject
3
- from typing import Optional
4
4
 
5
5
  import aiohttp
6
6
  import msgpack
7
7
  from aiohttp import WSMsgType, web
8
8
 
9
- from serverwamp.connection import Connection
10
- from serverwamp.helpers import objects_from_msgpack_batch, pack_uint32_be
11
- from serverwamp.json import JSON_BATCH_SPLITTER
12
- from serverwamp.json import deserialize as deserialize_json
13
- from serverwamp.json import objects_from_json_batch
9
+ from serverwamp.connection import Connection, ConnectionClosed
10
+ from serverwamp.helpers import object_from_msgpack, objects_from_msgpack_batch, pack_uint32_be
11
+ from serverwamp.json import JSON_BATCH_SPLITTER, objects_from_json_batch
12
+ from serverwamp.json import deserialize_msg as deserialize_json
14
13
  from serverwamp.json import serialize as serialize_json
15
14
 
16
15
  SUPPORTED_WS_PROTOCOLS = (
@@ -23,9 +22,15 @@ SUPPORTED_WS_PROTOCOLS = (
23
22
 
24
23
  async def connection_for_aiohttp_request(request: aiohttp.web.Request):
25
24
  ws = web.WebSocketResponse(protocols=SUPPORTED_WS_PROTOCOLS)
25
+ ws_protocol = ws.can_prepare(request).protocol
26
+ if ws_protocol is None:
27
+ # WAMP must be negotiated as a WebSocket subprotocol.
28
+ raise web.HTTPBadRequest(
29
+ text='A supported WAMP WebSocket subprotocol is required.'
30
+ )
26
31
  await ws.prepare(request)
27
32
 
28
- construct_connection = ws_protocol_connection_classes[ws.ws_protocol]
33
+ construct_connection = ws_protocol_connection_classes[ws_protocol]
29
34
  connection = construct_connection(ws, request)
30
35
  return connection
31
36
 
@@ -51,7 +56,7 @@ class AiohttpWebSocketConnection(Connection, metaclass=ABCMeta):
51
56
  self.transport_info['peer_certificate'] = (
52
57
  request.transport.get_extra_info('peercert')
53
58
  )
54
- ssl_obj: Optional[SSLObject] = (
59
+ ssl_obj: SSLObject | None = (
55
60
  request.transport.get_extra_info('ssl_object')
56
61
  )
57
62
  if ssl_obj:
@@ -75,10 +80,11 @@ class AiohttpJSONWebSocketConnection(AiohttpWebSocketConnection):
75
80
  yield deserialize_json(ws_msg.data)
76
81
 
77
82
  async def send_msg(self, msg):
78
- await self._ws.send_json(
79
- msg,
80
- compress=self._compress_outbound
81
- )
83
+ try:
84
+ await self._ws.send_json(msg, compress=self._compress_outbound)
85
+ except ConnectionResetError:
86
+ # aiohttp raises this when writing to a closing transport.
87
+ raise ConnectionClosed() from None
82
88
 
83
89
 
84
90
  class AiohttpBatchedJSONWebSocketConnection(AiohttpWebSocketConnection):
@@ -89,32 +95,41 @@ class AiohttpBatchedJSONWebSocketConnection(AiohttpWebSocketConnection):
89
95
  yield msg
90
96
 
91
97
  async def send_msg(self, msg):
92
- await self._ws.send_str(
93
- serialize_json(msg) + JSON_BATCH_SPLITTER,
94
- compress=self._compress_outbound
95
- )
98
+ try:
99
+ await self._ws.send_str(
100
+ serialize_json(msg) + JSON_BATCH_SPLITTER,
101
+ compress=self._compress_outbound
102
+ )
103
+ except ConnectionResetError:
104
+ raise ConnectionClosed() from None
96
105
 
97
106
  async def send_msgs(self, msgs):
98
- await self._ws.send_str(
99
- JSON_BATCH_SPLITTER.join(
100
- [serialize_json(msg) for msg in msgs]
101
- ) + JSON_BATCH_SPLITTER,
102
- compress=self._compress_outbound
103
- )
107
+ try:
108
+ await self._ws.send_str(
109
+ JSON_BATCH_SPLITTER.join(
110
+ [serialize_json(msg) for msg in msgs]
111
+ ) + JSON_BATCH_SPLITTER,
112
+ compress=self._compress_outbound
113
+ )
114
+ except ConnectionResetError:
115
+ raise ConnectionClosed() from None
104
116
 
105
117
 
106
118
  class AiohttpMsgPackWebSocketConnection(AiohttpWebSocketConnection):
107
119
  async def iterate_msgs(self):
108
120
  async for ws_msg in self._ws:
109
121
  if ws_msg.type == WSMsgType.BINARY:
110
- yield msgpack.unpackb(ws_msg.data, use_list=False)
122
+ yield object_from_msgpack(ws_msg.data)
111
123
 
112
124
  async def send_msg(self, msg):
113
125
  msg_bytes = msgpack.packb(msg)
114
- await self._ws.send_bytes(
115
- msg_bytes,
116
- compress=self._compress_outbound
117
- )
126
+ try:
127
+ await self._ws.send_bytes(
128
+ msg_bytes,
129
+ compress=self._compress_outbound
130
+ )
131
+ except ConnectionResetError:
132
+ raise ConnectionClosed() from None
118
133
 
119
134
 
120
135
  class AiohttpBatchedMsgPackWebSocketConnection(AiohttpWebSocketConnection):
@@ -129,10 +144,13 @@ class AiohttpBatchedMsgPackWebSocketConnection(AiohttpWebSocketConnection):
129
144
  async def send_msg(self, msg):
130
145
  msg_bytes = msgpack.packb(msg)
131
146
  msg_len_bytes = pack_uint32_be(len(msg_bytes))
132
- await self._ws.send_bytes(
133
- msg_len_bytes + msg_bytes,
134
- compress=self._compress_outbound
135
- )
147
+ try:
148
+ await self._ws.send_bytes(
149
+ msg_len_bytes + msg_bytes,
150
+ compress=self._compress_outbound
151
+ )
152
+ except ConnectionResetError:
153
+ raise ConnectionClosed() from None
136
154
 
137
155
  async def send_msgs(self, msgs):
138
156
  batch_bytes = bytearray()
@@ -141,13 +159,18 @@ class AiohttpBatchedMsgPackWebSocketConnection(AiohttpWebSocketConnection):
141
159
  msg_len_bytes = pack_uint32_be(len(msg_bytes))
142
160
  batch_bytes += msg_len_bytes
143
161
  batch_bytes += msg_bytes
144
- await self._ws.send_bytes(
145
- batch_bytes,
146
- compress=self._compress_outbound
147
- )
162
+ try:
163
+ await self._ws.send_bytes(
164
+ batch_bytes,
165
+ compress=self._compress_outbound
166
+ )
167
+ except ConnectionResetError:
168
+ raise ConnectionClosed() from None
148
169
 
149
170
 
150
- ws_protocol_connection_classes = {
171
+ ws_protocol_connection_classes: Mapping[
172
+ str, type[AiohttpWebSocketConnection]
173
+ ] = {
151
174
  'wamp.2.msgpack': AiohttpMsgPackWebSocketConnection,
152
175
  'wamp.2.msgpack.batched': AiohttpBatchedMsgPackWebSocketConnection,
153
176
  'wamp.2.json': AiohttpJSONWebSocketConnection,
@@ -1,9 +1,10 @@
1
+ from contextlib import asynccontextmanager
1
2
  from functools import partial
2
3
 
3
4
  import anyio
5
+ import anyio.abc
4
6
 
5
7
  from serverwamp.adapters.async_base import AsyncSupport, AsyncTaskGroup
6
- from serverwamp.context import asynccontextmanager
7
8
 
8
9
 
9
10
  class AnyioAsyncSupport(AsyncSupport):
@@ -15,30 +16,26 @@ class AnyioAsyncSupport(AsyncSupport):
15
16
  yield task_group
16
17
 
17
18
  @classmethod
18
- @asynccontextmanager
19
19
  async def shield(
20
20
  cls,
21
21
  callback,
22
22
  *callback_args,
23
23
  **callback_kwargs
24
24
  ):
25
- async with anyio.open_cancel_scope(shield=True):
26
- await callback(*callback_args, **callback_kwargs)
25
+ with anyio.CancelScope(shield=True):
26
+ return await callback(*callback_args, **callback_kwargs)
27
27
 
28
28
 
29
29
  class AnyioTaskGroup(AsyncTaskGroup):
30
30
  """In anyio's case, this is just a light wrapper around its own TaskGroup.
31
31
  """
32
- def __init__(self, anyio_task_group: anyio.TaskGroup):
32
+ def __init__(self, anyio_task_group: anyio.abc.TaskGroup):
33
33
  self._anyio_task_group = anyio_task_group
34
34
 
35
35
  async def spawn(self, callback, *callback_args, **callback_kwargs):
36
36
  if callback_kwargs:
37
37
  callback = partial(callback, **callback_kwargs)
38
-
39
- await self._anyio_task_group.spawn(
40
- callback, *callback_args, **callback_kwargs
41
- )
38
+ self._anyio_task_group.start_soon(callback, *callback_args)
42
39
 
43
40
  async def cancel(self):
44
- await self._anyio_task_group.cancel_scope.cancel()
41
+ self._anyio_task_group.cancel_scope.cancel()