latch-asgi 1.0.0__tar.gz → 1.0.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: latch-asgi
3
- Version: 1.0.0
3
+ Version: 1.0.1
4
4
  Summary: ASGI python server
5
5
  Author-Email: Max Smolin <max@latch.bio>
6
6
  License: CC0-1.0
@@ -1,5 +1,5 @@
1
1
  from dataclasses import dataclass, field
2
- from typing import Generic, Self, TypeVar
2
+ from typing import ClassVar, Generic, Self, TypeVar, cast
3
3
 
4
4
  from hypercorn.typing import WWWScope
5
5
  from latch_o11y.o11y import (
@@ -8,12 +8,11 @@ from latch_o11y.o11y import (
8
8
  dict_to_attrs,
9
9
  trace_app_function,
10
10
  )
11
- from opentelemetry.util.types import AttributeValue
11
+ from opentelemetry import context
12
+ from opentelemetry.trace.span import Span
12
13
 
13
14
  from ..asgi_iface import WWWReceiveCallable, WWWSendCallable
14
15
  from ..auth import Authorization, get_signer_sub
15
- from ..framework.common import otel_header_whitelist
16
- from ..framework.http import current_http_request_span
17
16
 
18
17
  Scope = TypeVar("Scope", bound=WWWScope)
19
18
  SendCallable = TypeVar("SendCallable", bound=WWWSendCallable)
@@ -31,6 +30,8 @@ class Context(Generic[Scope, ReceiveCallable, SendCallable]):
31
30
  _header_cache: dict[bytes, bytes] = field(default_factory=dict, init=False)
32
31
  _db_response_idx: int = field(default=0, init=False)
33
32
 
33
+ _request_span_key: ClassVar[str]
34
+
34
35
  def __post_init__(self: Self) -> None:
35
36
  with app_tracer.start_as_current_span("find Authentication header"):
36
37
  auth_header = self.header_str("authorization")
@@ -39,7 +40,7 @@ class Context(Generic[Scope, ReceiveCallable, SendCallable]):
39
40
  self.auth = get_signer_sub(auth_header)
40
41
 
41
42
  if self.auth.oauth_sub is not None:
42
- current_http_request_span().set_attribute("enduser.id", self.auth.oauth_sub)
43
+ self.current_request_span().set_attribute("enduser.id", self.auth.oauth_sub)
43
44
 
44
45
  def header(self: Self, x: str | bytes) -> bytes | None:
45
46
  if isinstance(x, str):
@@ -62,8 +63,11 @@ class Context(Generic[Scope, ReceiveCallable, SendCallable]):
62
63
 
63
64
  return res.decode("latin-1")
64
65
 
66
+ def current_request_span(self: Self) -> Span:
67
+ return cast(Span, context.get_value(self._request_span_key))
68
+
65
69
  def add_request_span_attrs(self: Self, data: AttributesDict, prefix: str) -> None:
66
- current_http_request_span().set_attributes(dict_to_attrs(data, prefix))
70
+ self.current_request_span().set_attributes(dict_to_attrs(data, prefix))
67
71
 
68
72
  @trace_app_function
69
73
  def add_db_response(self: Self, data: AttributesDict) -> None:
@@ -6,7 +6,7 @@ from hypercorn.typing import HTTPScope
6
6
  from latch_o11y.o11y import trace_app_function
7
7
 
8
8
  from ..asgi_iface import HTTPReceiveCallable, HTTPSendCallable
9
- from ..framework.http import HTTPMethod, receive_class_ext
9
+ from ..framework.http import HTTPMethod, http_request_span_key, receive_class_ext
10
10
  from . import common
11
11
 
12
12
  T = TypeVar("T")
@@ -14,6 +14,8 @@ T = TypeVar("T")
14
14
 
15
15
  @dataclass
16
16
  class Context(common.Context[HTTPScope, HTTPReceiveCallable, HTTPSendCallable]):
17
+ _request_span_key = http_request_span_key
18
+
17
19
  @trace_app_function
18
20
  async def receive_request_payload(self: Self, cls: type[T]) -> T:
19
21
  json, res = await receive_class_ext(self.receive, cls)
@@ -3,16 +3,16 @@ from dataclasses import dataclass
3
3
  from typing import Any, Self, TypeAlias, TypeVar
4
4
 
5
5
  from hypercorn.typing import WebsocketScope
6
- from latch_o11y.o11y import AttributesDict, dict_to_attrs, trace_app_function
6
+ from latch_o11y.o11y import dict_to_attrs, trace_app_function
7
7
  from opentelemetry.trace import get_current_span
8
8
 
9
9
  from ..asgi_iface import WebsocketReceiveCallable, WebsocketSendCallable
10
10
  from ..framework.common import Headers
11
11
  from ..framework.websocket import (
12
12
  accept_connection,
13
- current_websocket_session_span,
14
13
  receive_class_ext,
15
14
  send_websocket_auto,
15
+ websocket_session_span_key,
16
16
  )
17
17
  from . import common
18
18
 
@@ -23,8 +23,7 @@ T = TypeVar("T")
23
23
  class Context(
24
24
  common.Context[WebsocketScope, WebsocketReceiveCallable, WebsocketSendCallable]
25
25
  ):
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))
26
+ _request_span_key = websocket_session_span_key
28
27
 
29
28
  @trace_app_function
30
29
  async def accept_connection(
@@ -3,7 +3,7 @@ from typing import Any, Literal, Self, TypeAlias, TypeVar, cast
3
3
 
4
4
  import orjson
5
5
  from latch_data_validation.data_validation import DataValidationError, validate
6
- from latch_o11y.o11y import trace_function, trace_function_with_span
6
+ from latch_o11y.o11y import trace_function
7
7
  from opentelemetry import context
8
8
  from opentelemetry.trace.span import Span
9
9
 
@@ -6,7 +6,6 @@ from latch_data_validation.data_validation import DataValidationError, validate
6
6
  from latch_o11y.o11y import trace_function, trace_function_with_span
7
7
  from opentelemetry import context
8
8
  from opentelemetry.trace.span import Span
9
- from opentelemetry.util.types import AttributeValue
10
9
 
11
10
  from ..asgi_iface import (
12
11
  WebsocketAcceptEvent,
@@ -188,15 +187,15 @@ async def accept_connection(
188
187
 
189
188
  @trace_function_with_span(tracer)
190
189
  async def close_connection(
191
- s: Span, send: WebsocketSendCallable, /, *, status: WebsocketStatus, data: str
190
+ s: Span, send: WebsocketSendCallable, /, *, status: WebsocketStatus, reason: str
192
191
  ) -> None:
193
- s.set_attributes({"status": status.name, "reason": data})
192
+ s.set_attributes({"status": status.name, "reason": reason})
194
193
 
195
194
  await send(
196
- WebsocketCloseEvent(type="websocket.close", code=status.value, reason=data)
195
+ WebsocketCloseEvent(type="websocket.close", code=status.value, reason=reason)
197
196
  )
198
197
 
199
- current_websocket_session_span().set_attribute("websocket.close_reason", data)
198
+ current_websocket_session_span().set_attribute("websocket.close_reason", reason)
200
199
 
201
200
 
202
201
  # >>> Receive
@@ -48,6 +48,7 @@ from .framework.http import (
48
48
  send_http_data,
49
49
  )
50
50
  from .framework.websocket import (
51
+ WebsocketConnectionClosedError,
51
52
  WebsocketErrorResponse,
52
53
  WebsocketInternalServerError,
53
54
  WebsocketStatus,
@@ -214,12 +215,13 @@ class LatchASGIServer:
214
215
  # todo(maximsmol): better error message
215
216
  await log.info("Not found")
216
217
  await close_connection(
217
- send, status=WebsocketStatus.policy_violation, data="Not found"
218
+ send, status=WebsocketStatus.policy_violation, reason="Not found"
218
219
  )
219
220
  return
220
221
 
221
222
  s.set_attribute("http.route", scope["path"])
222
223
 
224
+ close_reason: str | None = None
223
225
  try:
224
226
  try:
225
227
  msg = await receive()
@@ -229,24 +231,30 @@ class LatchASGIServer:
229
231
  )
230
232
 
231
233
  ctx = websocket.Context(scope, receive, send)
232
- data = await handler(ctx)
234
+ close_reason = await handler(ctx)
233
235
  except WebsocketErrorResponse:
234
236
  raise
237
+ except WebsocketConnectionClosedError as e:
238
+ s.set_attribute("websocket.close.code", e.code.name)
235
239
  except Exception as e:
236
240
  # todo(maximsmol): better error message
237
241
  raise WebsocketInternalServerError("Internal Error") from e
238
242
  except WebsocketErrorResponse as e:
239
243
  await close_connection(
240
- send, status=e.status, data=orjson.dumps({"error": e.data}).decode()
244
+ send, status=e.status, reason=orjson.dumps({"error": e.data}).decode()
241
245
  )
242
246
 
243
247
  if e.status != WebsocketStatus.server_error:
244
248
  return
245
249
 
246
- # await log.exception() # fixme(maximsmol)
247
250
  raise
248
251
  else:
249
- await close_connection(send, status=WebsocketStatus.normal, data=data)
252
+ if close_reason is None:
253
+ close_reason = "Session complete"
254
+
255
+ await close_connection(
256
+ send, status=WebsocketStatus.normal, reason=close_reason
257
+ )
250
258
 
251
259
  async def scope_http(
252
260
  self: Self,
@@ -286,12 +294,6 @@ class LatchASGIServer:
286
294
  )
287
295
  return
288
296
 
289
- if handler is None:
290
- # todo(maximsmol): better error message
291
- await log.info("Not found")
292
- await send_http_data(send, HTTPStatus.NOT_FOUND, "Not found")
293
- return
294
-
295
297
  try:
296
298
  try:
297
299
  ctx = http.Context(scope, receive, send)
@@ -311,7 +313,6 @@ class LatchASGIServer:
311
313
  if e.status != HTTPStatus.INTERNAL_SERVER_ERROR:
312
314
  return
313
315
 
314
- # await log.exception() # fixme(maximsmol)
315
316
  raise
316
317
 
317
318
  async def raw_app(
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "latch-asgi"
3
- version = "1.0.0"
3
+ version = "1.0.1"
4
4
  description = "ASGI python server"
5
5
  authors = [
6
6
  { name = "Max Smolin", email = "max@latch.bio" },
File without changes
File without changes