latch-asgi 0.2.0__py3-none-any.whl → 1.0.0__py3-none-any.whl

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.
latch_asgi/asgi_iface.py CHANGED
@@ -1,196 +1,47 @@
1
- import dataclasses
2
- from dataclasses import dataclass
3
- from typing import (
4
- Awaitable,
5
- Callable,
6
- Iterable,
7
- Literal,
8
- TypeAlias,
9
- get_args,
10
- get_origin,
1
+ from collections.abc import Awaitable, Callable
2
+ from typing import TypeAlias
3
+
4
+ from hypercorn.typing import (
5
+ HTTPDisconnectEvent,
6
+ HTTPRequestEvent,
7
+ HTTPResponseBodyEvent,
8
+ HTTPResponseStartEvent,
9
+ HTTPServerPushEvent,
10
+ LifespanShutdownCompleteEvent,
11
+ LifespanShutdownEvent,
12
+ LifespanShutdownFailedEvent,
13
+ LifespanStartupCompleteEvent,
14
+ LifespanStartupEvent,
15
+ LifespanStartupFailedEvent,
16
+ WebsocketAcceptEvent,
17
+ WebsocketCloseEvent,
18
+ WebsocketConnectEvent,
19
+ WebsocketDisconnectEvent,
20
+ WebsocketReceiveEvent,
21
+ WebsocketResponseBodyEvent,
22
+ WebsocketResponseStartEvent,
23
+ WebsocketSendEvent,
11
24
  )
12
25
 
13
- import hypercorn.typing as htyping
14
- from latch_data_validation.data_validation import validate
15
-
16
-
17
- def type_str(x: type) -> str:
18
- for f in dataclasses.fields(x):
19
- if f.name != "type":
20
- continue
21
-
22
- o = get_origin(f.type)
23
- if o is not Literal:
24
- raise ValueError("'type' field type is not a Literal")
25
-
26
- res = get_args(f.type)[0]
27
- if not isinstance(res, str):
28
- raise ValueError("'type' field Literal is not a string")
29
-
30
- return res
31
-
32
- raise ValueError("'type' field not found")
33
-
34
-
35
- @dataclass(frozen=True)
36
- class ASGIVersions:
37
- spec_version: str
38
- version: Literal["2.0"] | Literal["3.0"]
39
-
40
- def as_dict(self):
41
- return validate(dataclasses.asdict(self), htyping.ASGIVersions)
42
-
43
-
44
26
  # >>> Lifespan
45
- @dataclass(frozen=True)
46
- class LifespanScope:
47
- type: Literal["lifespan"]
48
- asgi: ASGIVersions
49
-
50
- def as_dict(self):
51
- return validate(dataclasses.asdict(self), htyping.LifespanScope)
52
-
53
-
54
- @dataclass(frozen=True)
55
- class LifespanStartupEvent:
56
- type: Literal["lifespan.startup"]
57
-
58
- def as_dict(self):
59
- return validate(dataclasses.asdict(self), htyping.LifespanStartupEvent)
60
-
61
-
62
- @dataclass(frozen=True)
63
- class LifespanShutdownEvent:
64
- type: Literal["lifespan.shutdown"]
65
-
66
- def as_dict(self):
67
- return validate(dataclasses.asdict(self), htyping.LifespanShutdownEvent)
68
-
69
-
70
27
  LifespanReceiveEvent: TypeAlias = LifespanStartupEvent | LifespanShutdownEvent
71
28
  LifespanReceiveCallable: TypeAlias = Callable[[], Awaitable[LifespanReceiveEvent]]
72
29
 
73
-
74
- @dataclass(frozen=True)
75
- class LifespanStartupCompleteEvent:
76
- type: Literal["lifespan.startup.complete"]
77
-
78
- def as_dict(self):
79
- return validate(dataclasses.asdict(self), htyping.LifespanStartupCompleteEvent)
80
-
81
-
82
- @dataclass(frozen=True)
83
- class LifespanStartupFailedEvent:
84
- type: Literal["lifespan.startup.failed"]
85
- message: str
86
-
87
- def as_dict(self):
88
- return validate(dataclasses.asdict(self), htyping.LifespanStartupFailedEvent)
89
-
90
-
91
- LifespanStartupSendEvent: TypeAlias = (
92
- LifespanStartupCompleteEvent | LifespanStartupFailedEvent
93
- )
94
-
95
-
96
- @dataclass(frozen=True)
97
- class LifespanShutdownCompleteEvent:
98
- type: Literal["lifespan.shutdown.complete"]
99
-
100
- def as_dict(self):
101
- return validate(dataclasses.asdict(self), htyping.LifespanShutdownCompleteEvent)
102
-
103
-
104
- @dataclass(frozen=True)
105
- class LifespanShutdownFailedEvent:
106
- type: Literal["lifespan.shutdown.failed"]
107
- message: str
108
-
109
- def as_dict(self):
110
- return validate(dataclasses.asdict(self), htyping.LifespanShutdownFailedEvent)
111
-
112
-
113
30
  LifespanShutdownSendEvent: TypeAlias = (
114
31
  LifespanShutdownCompleteEvent | LifespanShutdownFailedEvent
115
32
  )
33
+ LifespanStartupSendEvent: TypeAlias = (
34
+ LifespanStartupCompleteEvent | LifespanStartupFailedEvent
35
+ )
116
36
 
117
37
  LifespanSendEvent: TypeAlias = LifespanStartupSendEvent | LifespanShutdownSendEvent
118
38
  LifespanSendCallable: TypeAlias = Callable[[LifespanSendEvent], Awaitable[None]]
119
39
 
120
-
121
40
  # >>> HTTP
122
- @dataclass(frozen=True)
123
- class HTTPScope:
124
- type: Literal["http"]
125
- asgi: ASGIVersions
126
- http_version: str
127
- method: str
128
- scheme: str
129
- path: str
130
- raw_path: bytes
131
- query_string: bytes
132
- root_path: str
133
- headers: Iterable[tuple[bytes, bytes]]
134
- client: tuple[str, int] | None
135
- server: tuple[str, int | None] | None
136
- extensions: dict[str, dict]
137
-
138
- def as_dict(self):
139
- return validate(dataclasses.asdict(self), htyping.HTTPScope)
140
-
141
-
142
- @dataclass(frozen=True)
143
- class HTTPRequestEvent:
144
- type: Literal["http.request"]
145
- body: bytes
146
- more_body: bool
147
-
148
- def as_dict(self):
149
- return validate(dataclasses.asdict(self), htyping.HTTPRequestEvent)
150
-
151
-
152
- @dataclass(frozen=True)
153
- class HTTPDisconnectEvent:
154
- type: Literal["http.disconnect"]
155
-
156
- def as_dict(self):
157
- return validate(dataclasses.asdict(self), htyping.HTTPDisconnectEvent)
158
-
159
41
 
160
42
  HTTPReceiveEvent: TypeAlias = HTTPRequestEvent | HTTPDisconnectEvent
161
43
  HTTPReceiveCallable: TypeAlias = Callable[[], Awaitable[HTTPReceiveEvent]]
162
44
 
163
-
164
- @dataclass(frozen=True)
165
- class HTTPResponseStartEvent:
166
- type: Literal["http.response.start"]
167
- status: int
168
- headers: Iterable[tuple[bytes, bytes]]
169
-
170
- def as_dict(self):
171
- return validate(dataclasses.asdict(self), htyping.HTTPResponseStartEvent)
172
-
173
-
174
- @dataclass(frozen=True)
175
- class HTTPResponseBodyEvent:
176
- type: Literal["http.response.body"]
177
- body: bytes
178
- more_body: bool
179
-
180
- def as_dict(self):
181
- return validate(dataclasses.asdict(self), htyping.HTTPResponseBodyEvent)
182
-
183
-
184
- @dataclass(frozen=True)
185
- class HTTPServerPushEvent:
186
- type: Literal["http.response.push"]
187
- path: str
188
- headers: Iterable[tuple[bytes, bytes]]
189
-
190
- def as_dict(self):
191
- return validate(dataclasses.asdict(self), htyping.HTTPServerPushEvent)
192
-
193
-
194
45
  HTTPSendEvent: TypeAlias = (
195
46
  HTTPResponseStartEvent
196
47
  | HTTPResponseBodyEvent
@@ -199,104 +50,12 @@ HTTPSendEvent: TypeAlias = (
199
50
  )
200
51
  HTTPSendCallable: TypeAlias = Callable[[HTTPSendEvent], Awaitable[None]]
201
52
 
202
-
203
53
  # >>> Websocket
204
- @dataclass(frozen=True)
205
- class WebsocketScope:
206
- type: Literal["websocket"]
207
- asgi: ASGIVersions
208
- http_version: str
209
- scheme: str
210
- path: str
211
- raw_path: bytes
212
- query_string: bytes
213
- root_path: str
214
- headers: Iterable[tuple[bytes, bytes]]
215
- client: tuple[str, int] | None
216
- server: tuple[str, int | None] | None
217
- subprotocols: Iterable[str]
218
- extensions: dict[str, object]
219
-
220
- def as_dict(self):
221
- return validate(dataclasses.asdict(self), htyping.WebsocketScope)
222
-
223
-
224
- @dataclass(frozen=True)
225
- class WebsocketConnectEvent:
226
- type: Literal["websocket.connect"]
227
-
228
- def as_dict(self):
229
- return validate(dataclasses.asdict(self), htyping.WebsocketConnectEvent)
230
-
231
-
232
- @dataclass(frozen=True)
233
- class WebsocketAcceptEvent:
234
- type: Literal["websocket.accept"]
235
- subprotocol: str | None
236
- headers: Iterable[tuple[bytes, bytes]]
237
-
238
- def as_dict(self):
239
- return validate(dataclasses.asdict(self), htyping.WebsocketAcceptEvent)
240
-
241
-
242
- @dataclass(frozen=True)
243
- class WebsocketReceiveEvent:
244
- type: Literal["websocket.receive"]
245
- bytes: bytes | None
246
- text: str | None
247
-
248
- def as_dict(self):
249
- return validate(dataclasses.asdict(self), htyping.WebsocketReceiveEvent)
250
-
251
-
252
- @dataclass(frozen=True)
253
- class WebsocketSendEvent:
254
- type: Literal["websocket.send"]
255
- bytes: bytes | None
256
- text: str | None
257
-
258
- def as_dict(self):
259
- return validate(dataclasses.asdict(self), htyping.WebsocketSendEvent)
260
-
261
-
262
- @dataclass(frozen=True)
263
- class WebsocketResponseStartEvent:
264
- type: Literal["websocket.http.response.start"]
265
- status: int
266
- headers: Iterable[tuple[bytes, bytes]]
267
-
268
- def as_dict(self):
269
- return validate(dataclasses.asdict(self), htyping.WebsocketResponseStartEvent)
270
-
271
-
272
- @dataclass(frozen=True)
273
- class WebsocketResponseBodyEvent:
274
- type: Literal["websocket.http.response.body"]
275
- body: bytes
276
- more_body: bool
277
-
278
- def as_dict(self):
279
- return validate(dataclasses.asdict(self), htyping.WebsocketResponseBodyEvent)
280
-
281
-
282
- @dataclass(frozen=True)
283
- class WebsocketDisconnectEvent:
284
- type: Literal["websocket.disconnect"]
285
- code: int
286
-
287
- def as_dict(self):
288
- return validate(dataclasses.asdict(self), htyping.WebsocketDisconnectEvent)
289
-
290
-
291
- @dataclass(frozen=True)
292
- class WebsocketCloseEvent:
293
- type: Literal["websocket.close"]
294
- code: int
295
- reason: str | None
296
-
297
- def as_dict(self):
298
- return validate(dataclasses.asdict(self), htyping.WebsocketCloseEvent)
299
54
 
55
+ WebsocketReceiveEventT: TypeAlias = (
56
+ WebsocketConnectEvent | WebsocketReceiveEvent | WebsocketDisconnectEvent
57
+ )
58
+ WebsocketReceiveCallable: TypeAlias = Callable[[], Awaitable[WebsocketReceiveEventT]]
300
59
 
301
60
  WebsocketSendEventT: TypeAlias = (
302
61
  WebsocketAcceptEvent
@@ -307,22 +66,7 @@ WebsocketSendEventT: TypeAlias = (
307
66
  )
308
67
  WebsocketSendCallable: TypeAlias = Callable[[WebsocketSendEventT], Awaitable[None]]
309
68
 
69
+ # >>> WWW
310
70
 
311
- WebsocketReceiveEventT: TypeAlias = (
312
- WebsocketConnectEvent | WebsocketReceiveEvent | WebsocketDisconnectEvent
313
- )
314
- WebsocketReceiveCallable: TypeAlias = Callable[[], Awaitable[WebsocketReceiveEventT]]
315
-
316
-
317
- WWWScope: TypeAlias = HTTPScope | WebsocketScope
318
- Scope: TypeAlias = HTTPScope | WebsocketScope | LifespanScope
319
-
320
- WWWSendCallable: TypeAlias = HTTPSendCallable | WebsocketSendCallable
321
- SendCallable: TypeAlias = (
322
- HTTPSendCallable | WebsocketSendCallable | LifespanSendCallable
323
- )
324
-
325
- WWWReceiveCallable: TypeAlias = HTTPReceiveCallable | WebsocketReceiveCallable
326
- ReceiveCallable: TypeAlias = (
327
- HTTPReceiveCallable | WebsocketReceiveCallable | LifespanReceiveCallable
328
- )
71
+ WWWReceiveCallable = HTTPReceiveCallable | WebsocketReceiveCallable
72
+ WWWSendCallable = HTTPSendCallable | WebsocketSendCallable
latch_asgi/auth.py CHANGED
@@ -3,7 +3,7 @@
3
3
  import re
4
4
  from dataclasses import dataclass
5
5
  from http import HTTPStatus
6
- from typing import Literal
6
+ from typing import Literal, Self
7
7
 
8
8
  import jwt
9
9
  from jwt import PyJWKClient
@@ -37,14 +37,10 @@ class _HTTPUnauthorized(HTTPErrorResponse):
37
37
  """WARNING: HTTPForbidden is the correct error to use in virtually all cases"""
38
38
 
39
39
  def __init__(
40
- self,
40
+ self: Self,
41
41
  error_description: str,
42
- error: (
43
- Literal["invalid_request"]
44
- | Literal["invalid_token"]
45
- | Literal["insufficient_scope"]
46
- ),
47
- ):
42
+ error: (Literal["invalid_request", "invalid_token", "insufficient_scope"]),
43
+ ) -> None:
48
44
  escaped_description = error_description.replace('"', '\\"')
49
45
  super().__init__(
50
46
  HTTPStatus.UNAUTHORIZED,
@@ -63,7 +59,7 @@ class Authorization:
63
59
  execution_token: str | None = None
64
60
  sdk_token: str | None = None
65
61
 
66
- def unauthorized_if_none(self):
62
+ def unauthorized_if_none(self: Self) -> None:
67
63
  if self.oauth_sub is not None:
68
64
  return
69
65
  if self.execution_token is not None:
@@ -71,10 +67,7 @@ class Authorization:
71
67
  if self.sdk_token is not None:
72
68
  return
73
69
 
74
- raise _HTTPUnauthorized(
75
- "Authenticaton required",
76
- error="invalid_request",
77
- )
70
+ raise _HTTPUnauthorized("Authenticaton required", error="invalid_request")
78
71
 
79
72
 
80
73
  @trace_app_function
@@ -121,8 +114,7 @@ def get_signer_sub(auth_header: str) -> Authorization:
121
114
  jwt_key = jwk_client.get_signing_key_from_jwt(oauth_token).key
122
115
  except jwt.exceptions.InvalidTokenError as e:
123
116
  raise _HTTPUnauthorized(
124
- error_description="JWT decoding failed",
125
- error="invalid_token",
117
+ error_description="JWT decoding failed", error="invalid_token"
126
118
  ) from e
127
119
  except jwt.exceptions.PyJWKClientError:
128
120
  # fixme(maximsmol): gut this abomination
latch_asgi/config.py CHANGED
@@ -1,4 +1,5 @@
1
1
  from dataclasses import dataclass
2
+
2
3
  from latch_config.config import read_config
3
4
 
4
5
 
@@ -8,4 +9,5 @@ class AuthConfig:
8
9
  self_signed_jwk: str
9
10
  allow_spoofing: bool = False
10
11
 
12
+
11
13
  config = read_config(AuthConfig, "auth_")
@@ -1,61 +1,72 @@
1
1
  from dataclasses import dataclass, field
2
- from typing import Generic, TypeVar
3
-
4
- from latch_o11y.o11y import AttributesDict, app_tracer, trace_app_function
5
-
6
- from ..asgi_iface import WWWReceiveCallable, WWWScope, WWWSendCallable
2
+ from typing import Generic, Self, TypeVar
3
+
4
+ from hypercorn.typing import WWWScope
5
+ from latch_o11y.o11y import (
6
+ AttributesDict,
7
+ app_tracer,
8
+ dict_to_attrs,
9
+ trace_app_function,
10
+ )
11
+ from opentelemetry.util.types import AttributeValue
12
+
13
+ from ..asgi_iface import WWWReceiveCallable, WWWSendCallable
7
14
  from ..auth import Authorization, get_signer_sub
15
+ from ..framework.common import otel_header_whitelist
16
+ from ..framework.http import current_http_request_span
8
17
 
9
- # todo(ayush): this sucks
10
18
  Scope = TypeVar("Scope", bound=WWWScope)
11
- Send = TypeVar("Send", bound=WWWSendCallable)
12
- Receive = TypeVar("Receive", bound=WWWReceiveCallable)
19
+ SendCallable = TypeVar("SendCallable", bound=WWWSendCallable)
20
+ ReceiveCallable = TypeVar("ReceiveCallable", bound=WWWReceiveCallable)
13
21
 
14
22
 
15
23
  @dataclass
16
- class Context(Generic[Scope, Receive, Send]):
24
+ class Context(Generic[Scope, ReceiveCallable, SendCallable]):
17
25
  scope: Scope
18
- receive: Receive
19
- send: Send
26
+ receive: ReceiveCallable
27
+ send: SendCallable
20
28
 
21
29
  auth: Authorization = field(default_factory=Authorization, init=False)
22
30
 
23
31
  _header_cache: dict[bytes, bytes] = field(default_factory=dict, init=False)
24
32
  _db_response_idx: int = field(default=0, init=False)
25
33
 
26
- def __post_init__(self):
34
+ def __post_init__(self: Self) -> None:
27
35
  with app_tracer.start_as_current_span("find Authentication header"):
28
36
  auth_header = self.header_str("authorization")
29
37
 
30
38
  if auth_header is not None:
31
39
  self.auth = get_signer_sub(auth_header)
32
40
 
33
- def header(self, x: str | bytes):
41
+ if self.auth.oauth_sub is not None:
42
+ current_http_request_span().set_attribute("enduser.id", self.auth.oauth_sub)
43
+
44
+ def header(self: Self, x: str | bytes) -> bytes | None:
34
45
  if isinstance(x, str):
35
- x = x.encode("utf-8")
46
+ x = x.encode("latin-1")
36
47
 
37
48
  if x in self._header_cache:
38
49
  return self._header_cache[x]
39
50
 
40
- for k, v in self.scope.headers:
51
+ for k, v in self.scope["headers"]:
41
52
  self._header_cache[k] = v
42
53
  if k == x:
43
54
  return v
44
55
 
45
56
  return None
46
57
 
47
- def header_str(self, x: str | bytes):
58
+ def header_str(self: Self, x: str | bytes) -> str | None:
48
59
  res = self.header(x)
49
60
  if res is None:
50
61
  return None
51
62
 
52
63
  return res.decode("latin-1")
53
64
 
54
- def add_request_span_attrs(self, data: AttributesDict, prefix: str):
55
- raise NotImplementedError()
65
+ def add_request_span_attrs(self: Self, data: AttributesDict, prefix: str) -> None:
66
+ current_http_request_span().set_attributes(dict_to_attrs(data, prefix))
56
67
 
57
68
  @trace_app_function
58
- def add_db_response(self, data: AttributesDict):
69
+ def add_db_response(self: Self, data: AttributesDict) -> None:
59
70
  # todo(maximsmol): datadog has shit support for events
60
71
  # current_http_request_span().add_event(
61
72
  # f"database response {self._db_response_idx}", dict_to_attrs(data, "data")
@@ -1,10 +1,12 @@
1
+ from collections.abc import Awaitable, Callable
1
2
  from dataclasses import dataclass
2
- from typing import Any, Awaitable, Callable, TypeAlias, TypeVar
3
+ from typing import Any, Self, TypeAlias, TypeVar
3
4
 
4
- from latch_o11y.o11y import AttributesDict, dict_to_attrs, trace_app_function
5
+ from hypercorn.typing import HTTPScope
6
+ from latch_o11y.o11y import trace_app_function
5
7
 
6
- from ..asgi_iface import HTTPReceiveCallable, HTTPScope, HTTPSendCallable
7
- from ..framework.http import HTTPMethod, current_http_request_span, receive_class_ext
8
+ from ..asgi_iface import HTTPReceiveCallable, HTTPSendCallable
9
+ from ..framework.http import HTTPMethod, receive_class_ext
8
10
  from . import common
9
11
 
10
12
  T = TypeVar("T")
@@ -12,31 +14,15 @@ T = TypeVar("T")
12
14
 
13
15
  @dataclass
14
16
  class Context(common.Context[HTTPScope, HTTPReceiveCallable, HTTPSendCallable]):
15
- def __post_init__(self):
16
- super().__post_init__()
17
-
18
- if self.auth.oauth_sub is not None:
19
- current_http_request_span().set_attribute("enduser.id", self.auth.oauth_sub)
20
-
21
- def add_request_span_attrs(self, data: AttributesDict, prefix: str):
22
- current_http_request_span().set_attributes(dict_to_attrs(data, prefix))
23
-
24
17
  @trace_app_function
25
- async def receive_request_payload(self, cls: type[T]) -> T:
18
+ async def receive_request_payload(self: Self, cls: type[T]) -> T:
26
19
  json, res = await receive_class_ext(self.receive, cls)
27
20
 
28
- # todo(maximsmol): datadog has shit support for events
29
- # current_http_request_span().add_event(
30
- # "request payload", dict_to_attrs(json, "data")
31
- # )
32
- self.add_request_span_attrs(json, "http.request_payload")
21
+ self.add_request_span_attrs(json, "http.request.body.data")
33
22
 
34
23
  return res
35
24
 
36
25
 
37
26
  HandlerResult: TypeAlias = Any | None
38
- Handler: TypeAlias = Callable[
39
- [Context],
40
- Awaitable[HandlerResult],
41
- ]
27
+ Handler: TypeAlias = Callable[[Context], Awaitable[HandlerResult]]
42
28
  Route: TypeAlias = Handler | tuple[list[HTTPMethod], Handler]
@@ -1,10 +1,19 @@
1
+ from collections.abc import Awaitable, Callable
1
2
  from dataclasses import dataclass
2
- from typing import Awaitable, Callable, TypeAlias, TypeVar
3
-
4
- from latch_o11y.o11y import AttributesDict, dict_to_attrs
5
-
6
- from ..asgi_iface import WebsocketReceiveCallable, WebsocketScope, WebsocketSendCallable
7
- from ..framework.websocket import WebsocketStatus, current_websocket_request_span
3
+ from typing import Any, Self, TypeAlias, TypeVar
4
+
5
+ from hypercorn.typing import WebsocketScope
6
+ from latch_o11y.o11y import AttributesDict, dict_to_attrs, trace_app_function
7
+ from opentelemetry.trace import get_current_span
8
+
9
+ from ..asgi_iface import WebsocketReceiveCallable, WebsocketSendCallable
10
+ from ..framework.common import Headers
11
+ from ..framework.websocket import (
12
+ accept_connection,
13
+ current_websocket_session_span,
14
+ receive_class_ext,
15
+ send_websocket_auto,
16
+ )
8
17
  from . import common
9
18
 
10
19
  T = TypeVar("T")
@@ -14,21 +23,28 @@ T = TypeVar("T")
14
23
  class Context(
15
24
  common.Context[WebsocketScope, WebsocketReceiveCallable, WebsocketSendCallable]
16
25
  ):
17
- def __post_init__(self):
18
- super().__post_init__()
26
+ def add_session_span_attrs(self: Self, data: AttributesDict, prefix: str) -> None:
27
+ current_websocket_session_span().set_attributes(dict_to_attrs(data, prefix))
28
+
29
+ @trace_app_function
30
+ async def accept_connection(
31
+ self: Self, *, subprotocol: str | None = None, headers: Headers | None = None
32
+ ) -> None:
33
+ await accept_connection(self.send, subprotocol=subprotocol, headers=headers)
34
+
35
+ @trace_app_function
36
+ async def receive_message(self: Self, cls: type[T]) -> T:
37
+ json, res = await receive_class_ext(self.receive, cls)
38
+
39
+ get_current_span().set_attributes(dict_to_attrs(json, "payload"))
19
40
 
20
- if self.auth.oauth_sub is not None:
21
- current_websocket_request_span().set_attribute(
22
- "enduser.id", self.auth.oauth_sub
23
- )
41
+ return res
24
42
 
25
- def add_request_span_attrs(self, data: AttributesDict, prefix: str):
26
- current_websocket_request_span().set_attributes(dict_to_attrs(data, prefix))
43
+ @trace_app_function
44
+ async def send_message(self: Self, data: Any) -> None:
45
+ await send_websocket_auto(self.send, data)
27
46
 
28
47
 
29
- HandlerResult = str | tuple[WebsocketStatus, str]
30
- Handler: TypeAlias = Callable[
31
- [Context],
32
- Awaitable[HandlerResult],
33
- ]
48
+ HandlerResult = str
49
+ Handler: TypeAlias = Callable[[Context], Awaitable[HandlerResult]]
34
50
  Route: TypeAlias = Handler