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.
- {latch_asgi-1.0.0 → latch_asgi-1.0.1}/PKG-INFO +1 -1
- {latch_asgi-1.0.0 → latch_asgi-1.0.1}/latch_asgi/context/common.py +10 -6
- {latch_asgi-1.0.0 → latch_asgi-1.0.1}/latch_asgi/context/http.py +3 -1
- {latch_asgi-1.0.0 → latch_asgi-1.0.1}/latch_asgi/context/websocket.py +3 -4
- {latch_asgi-1.0.0 → latch_asgi-1.0.1}/latch_asgi/framework/http.py +1 -1
- {latch_asgi-1.0.0 → latch_asgi-1.0.1}/latch_asgi/framework/websocket.py +4 -5
- {latch_asgi-1.0.0 → latch_asgi-1.0.1}/latch_asgi/server.py +13 -12
- {latch_asgi-1.0.0 → latch_asgi-1.0.1}/pyproject.toml +1 -1
- {latch_asgi-1.0.0 → latch_asgi-1.0.1}/COPYING +0 -0
- {latch_asgi-1.0.0 → latch_asgi-1.0.1}/README.md +0 -0
- {latch_asgi-1.0.0 → latch_asgi-1.0.1}/latch_asgi/__init__.py +0 -0
- {latch_asgi-1.0.0 → latch_asgi-1.0.1}/latch_asgi/asgi_iface.py +0 -0
- {latch_asgi-1.0.0 → latch_asgi-1.0.1}/latch_asgi/auth.py +0 -0
- {latch_asgi-1.0.0 → latch_asgi-1.0.1}/latch_asgi/config.py +0 -0
- {latch_asgi-1.0.0 → latch_asgi-1.0.1}/latch_asgi/context/__init__.py +0 -0
- {latch_asgi-1.0.0 → latch_asgi-1.0.1}/latch_asgi/datadog_propagator.py +0 -0
- {latch_asgi-1.0.0 → latch_asgi-1.0.1}/latch_asgi/framework/__init__.py +0 -0
- {latch_asgi-1.0.0 → latch_asgi-1.0.1}/latch_asgi/framework/common.py +0 -0
- {latch_asgi-1.0.0 → latch_asgi-1.0.1}/latch_asgi/py.typed +0 -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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,
|
|
190
|
+
s: Span, send: WebsocketSendCallable, /, *, status: WebsocketStatus, reason: str
|
|
192
191
|
) -> None:
|
|
193
|
-
s.set_attributes({"status": status.name, "reason":
|
|
192
|
+
s.set_attributes({"status": status.name, "reason": reason})
|
|
194
193
|
|
|
195
194
|
await send(
|
|
196
|
-
WebsocketCloseEvent(type="websocket.close", code=status.value, reason=
|
|
195
|
+
WebsocketCloseEvent(type="websocket.close", code=status.value, reason=reason)
|
|
197
196
|
)
|
|
198
197
|
|
|
199
|
-
current_websocket_session_span().set_attribute("websocket.close_reason",
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|