turbo-lambda 0.7.1__tar.gz → 0.9.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.
- {turbo_lambda-0.7.1 → turbo_lambda-0.9.0}/PKG-INFO +3 -2
- {turbo_lambda-0.7.1 → turbo_lambda-0.9.0}/pyproject.toml +18 -7
- turbo_lambda-0.9.0/src/turbo_lambda/constants.py +1 -0
- {turbo_lambda-0.7.1 → turbo_lambda-0.9.0}/src/turbo_lambda/decorators.py +5 -3
- {turbo_lambda-0.7.1 → turbo_lambda-0.9.0}/src/turbo_lambda/errors.py +4 -3
- {turbo_lambda-0.7.1 → turbo_lambda-0.9.0}/src/turbo_lambda/log.py +23 -7
- turbo_lambda-0.9.0/src/turbo_lambda/psycopg.py +107 -0
- {turbo_lambda-0.7.1 → turbo_lambda-0.9.0}/src/turbo_lambda/scripts/update_turbo_lambda_layer.py +4 -1
- {turbo_lambda-0.7.1 → turbo_lambda-0.9.0}/README.md +0 -0
- {turbo_lambda-0.7.1 → turbo_lambda-0.9.0}/src/turbo_lambda/__init__.py +0 -0
- {turbo_lambda-0.7.1 → turbo_lambda-0.9.0}/src/turbo_lambda/py.typed +0 -0
- {turbo_lambda-0.7.1 → turbo_lambda-0.9.0}/src/turbo_lambda/schemas.py +0 -0
- {turbo_lambda-0.7.1 → turbo_lambda-0.9.0}/src/turbo_lambda/scripts/__init__.py +0 -0
- {turbo_lambda-0.7.1 → turbo_lambda-0.9.0}/src/turbo_lambda/version.py +0 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: turbo-lambda
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Turbo Lambda Description
|
|
5
5
|
Author: Sam Mosleh
|
|
6
6
|
Author-email: Sam Mosleh <sam.mosleh.d@gmail.com>
|
|
7
7
|
Requires-Dist: opentelemetry-api>=1.27.0
|
|
8
8
|
Requires-Dist: pydantic-settings>=2.11.0
|
|
9
|
-
Requires-
|
|
9
|
+
Requires-Dist: psycopg[binary]>=3.3.4
|
|
10
|
+
Requires-Python: >=3.14
|
|
10
11
|
Description-Content-Type: text/markdown
|
|
11
12
|
|
|
12
13
|
# turbo-lambda
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "turbo-lambda"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.9.0"
|
|
4
4
|
description = "Turbo Lambda Description"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{ name = "Sam Mosleh", email = "sam.mosleh.d@gmail.com" }]
|
|
7
|
-
requires-python = ">=3.
|
|
7
|
+
requires-python = ">=3.14"
|
|
8
8
|
dependencies = [
|
|
9
9
|
"opentelemetry-api>=1.27.0",
|
|
10
10
|
"pydantic-settings>=2.11.0",
|
|
11
|
+
"psycopg[binary]>=3.3.4",
|
|
11
12
|
]
|
|
12
13
|
|
|
13
14
|
[dependency-groups]
|
|
@@ -22,7 +23,7 @@ dev = [
|
|
|
22
23
|
update-turbo-lambda-layer = "turbo_lambda.scripts.update_turbo_lambda_layer:main"
|
|
23
24
|
|
|
24
25
|
[build-system]
|
|
25
|
-
requires = ["uv_build>=0.
|
|
26
|
+
requires = ["uv_build>=0.10.2,<0.11.0"]
|
|
26
27
|
build-backend = "uv_build"
|
|
27
28
|
|
|
28
29
|
[tool.mypy]
|
|
@@ -39,18 +40,20 @@ filterwarnings = ["error"]
|
|
|
39
40
|
[tool.coverage.run]
|
|
40
41
|
branch = true
|
|
41
42
|
parallel = true
|
|
42
|
-
relative_files = true
|
|
43
43
|
source = ["src", "tests"]
|
|
44
44
|
|
|
45
45
|
[tool.coverage.report]
|
|
46
46
|
show_missing = true
|
|
47
|
-
# skip_covered = true
|
|
48
47
|
partial_branches = [
|
|
49
48
|
"# pragma: no cover\\b",
|
|
50
49
|
"# pragma: (nt|posix|cygwin|darwin|linux|msys|win32|cpython|pypy) (no )?cover\\b",
|
|
51
50
|
"# pragma: (>=?|<=?|==|!=)\\d+\\.\\d+ cover\\b",
|
|
52
51
|
]
|
|
53
|
-
exclude_also = [
|
|
52
|
+
exclude_also = [
|
|
53
|
+
'case _:\n\s*assert_never\(.*\)',
|
|
54
|
+
'if __name__ == .__main__.:',
|
|
55
|
+
'raise$',
|
|
56
|
+
]
|
|
54
57
|
|
|
55
58
|
[tool.ruff.lint]
|
|
56
59
|
select = [
|
|
@@ -59,11 +62,16 @@ select = [
|
|
|
59
62
|
"F", # pyflakes
|
|
60
63
|
"PL", # pylint
|
|
61
64
|
"I", # isort
|
|
62
|
-
"
|
|
65
|
+
"C4", # flake8-comprehensions
|
|
63
66
|
"B", # flake8-bugbear
|
|
64
67
|
"Q", # flake8-quotes
|
|
65
68
|
"T20", # flake8-print
|
|
66
69
|
"S", # flake8-bandit
|
|
70
|
+
"PIE", # flake8-pie
|
|
71
|
+
"RET", # flake8-return
|
|
72
|
+
"SIM", # flake8-simplify
|
|
73
|
+
"TC", # flake8-type-checking
|
|
74
|
+
"C90", # mccabe
|
|
67
75
|
"N", # pep8-naming
|
|
68
76
|
"UP", # pyupgrade
|
|
69
77
|
"RUF", # ruff
|
|
@@ -72,6 +80,9 @@ ignore = [
|
|
|
72
80
|
"E501", # line too long, handled by formatter
|
|
73
81
|
]
|
|
74
82
|
|
|
83
|
+
[tool.ruff.lint.flake8-type-checking]
|
|
84
|
+
runtime-evaluated-base-classes = ["pydantic.BaseModel"]
|
|
85
|
+
|
|
75
86
|
[tool.ruff.lint.per-file-ignores]
|
|
76
87
|
"tests/*" = ["S101", "S311"]
|
|
77
88
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
MAX_DYNAMODB_BATCH_WRITE_ITEM = 25
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
import logging
|
|
3
|
-
from collections.abc import Callable
|
|
4
3
|
from concurrent.futures import ThreadPoolExecutor
|
|
5
4
|
from contextlib import AbstractContextManager, ContextDecorator
|
|
6
5
|
from contextlib import suppress as contextlib_suppress
|
|
7
6
|
from functools import wraps
|
|
8
|
-
from
|
|
9
|
-
from typing import Any, Protocol, overload
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Protocol, overload
|
|
10
8
|
|
|
11
9
|
import pydantic
|
|
12
10
|
from opentelemetry.trace import format_span_id, format_trace_id, get_current_span
|
|
@@ -18,6 +16,10 @@ from turbo_lambda.errors import (
|
|
|
18
16
|
)
|
|
19
17
|
from turbo_lambda.log import log_after_call, logger, logger_bind
|
|
20
18
|
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from collections.abc import Callable
|
|
21
|
+
from types import TracebackType
|
|
22
|
+
|
|
21
23
|
|
|
22
24
|
class LambdaHandlerT[ResponseT](Protocol):
|
|
23
25
|
def __call__(
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import annotationlib
|
|
2
|
+
import base64
|
|
1
3
|
import contextlib
|
|
2
4
|
import contextvars
|
|
3
5
|
import datetime
|
|
@@ -5,15 +7,16 @@ import inspect
|
|
|
5
7
|
import logging
|
|
6
8
|
import time
|
|
7
9
|
import uuid
|
|
8
|
-
from collections.abc import Callable, Generator, Iterable
|
|
9
10
|
from enum import Enum
|
|
10
11
|
from functools import wraps
|
|
11
|
-
from typing import Any, overload
|
|
12
|
+
from typing import TYPE_CHECKING, Any, overload
|
|
12
13
|
|
|
13
14
|
import pydantic
|
|
14
15
|
|
|
15
16
|
from turbo_lambda.schemas import IS_LAMBDA
|
|
16
17
|
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from collections.abc import Callable, Generator, Iterable
|
|
17
20
|
LOGGING_CTX: contextvars.ContextVar[dict[str, Any]] = contextvars.ContextVar(
|
|
18
21
|
"LOGGING_CTX"
|
|
19
22
|
)
|
|
@@ -44,6 +47,8 @@ def _json_custom_default(value: Any) -> Any:
|
|
|
44
47
|
return str(value)
|
|
45
48
|
case set():
|
|
46
49
|
return list(value)
|
|
50
|
+
case bytes():
|
|
51
|
+
return base64.b64encode(value).decode()
|
|
47
52
|
case _:
|
|
48
53
|
raise TypeError(value.__class__.__name__)
|
|
49
54
|
|
|
@@ -57,6 +62,10 @@ def _setup_logger() -> None: # pragma: no cover
|
|
|
57
62
|
lambda_runtime_log_utils._json_encoder.default = _json_custom_default
|
|
58
63
|
|
|
59
64
|
|
|
65
|
+
def _default_result_extractor[T](res: T) -> dict[str, T]:
|
|
66
|
+
return {"result": res}
|
|
67
|
+
|
|
68
|
+
|
|
60
69
|
@overload
|
|
61
70
|
def log_after_call[**P, T](func: Callable[P, T]) -> Callable[P, T]: ...
|
|
62
71
|
|
|
@@ -68,7 +77,7 @@ def log_after_call[**P, T](
|
|
|
68
77
|
log_message: str = "call",
|
|
69
78
|
log_exceptions: bool = False,
|
|
70
79
|
excluded_fields: Iterable[str] = ("self",),
|
|
71
|
-
result_extractor:
|
|
80
|
+
result_extractor: bool = False,
|
|
72
81
|
) -> Callable[[Callable[P, T]], Callable[P, T]]: ...
|
|
73
82
|
|
|
74
83
|
|
|
@@ -89,10 +98,17 @@ def log_after_call[**P, T]( # noqa: PLR0913
|
|
|
89
98
|
log_message: str = "call",
|
|
90
99
|
log_exceptions: bool = False,
|
|
91
100
|
excluded_fields: Iterable[str] = ("self",),
|
|
92
|
-
result_extractor: Callable[[T], dict[str, Any]] |
|
|
101
|
+
result_extractor: Callable[[T], dict[str, Any]] | bool = False,
|
|
93
102
|
) -> Callable[P, T] | Callable[[Callable[P, T]], Callable[P, T]]:
|
|
94
103
|
def decorator(func: Callable[P, T]) -> Callable[P, T]:
|
|
95
|
-
sig = inspect.signature(func)
|
|
104
|
+
sig = inspect.signature(func, annotation_format=annotationlib.Format.STRING)
|
|
105
|
+
result_extractor_func = (
|
|
106
|
+
result_extractor
|
|
107
|
+
if callable(result_extractor)
|
|
108
|
+
else _default_result_extractor
|
|
109
|
+
if result_extractor
|
|
110
|
+
else None
|
|
111
|
+
)
|
|
96
112
|
|
|
97
113
|
@wraps(func)
|
|
98
114
|
def wrapper(*f_args: P.args, **f_kwargs: P.kwargs) -> T:
|
|
@@ -110,8 +126,8 @@ def log_after_call[**P, T]( # noqa: PLR0913
|
|
|
110
126
|
st = time.monotonic()
|
|
111
127
|
try:
|
|
112
128
|
result = func(*f_args, **f_kwargs)
|
|
113
|
-
if
|
|
114
|
-
extra.update(
|
|
129
|
+
if result_extractor_func:
|
|
130
|
+
extra.update(result_extractor_func(result))
|
|
115
131
|
return result
|
|
116
132
|
except Exception as e:
|
|
117
133
|
if log_exceptions:
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, Any
|
|
2
|
+
|
|
3
|
+
import psycopg
|
|
4
|
+
from psycopg import pq, sql
|
|
5
|
+
from psycopg.raw_cursor import RawCursorMixin
|
|
6
|
+
from psycopg.rows import Row
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from collections.abc import Iterable
|
|
10
|
+
from typing import Self
|
|
11
|
+
|
|
12
|
+
from psycopg.abc import Params, Query
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class UnoptimizedQueryError(Exception):
|
|
16
|
+
"""Raised when a statement is planned using a sequential scan.
|
|
17
|
+
|
|
18
|
+
The detection is invisible to the interactor: every statement is planned
|
|
19
|
+
with ``EXPLAIN (FORMAT JSON)`` under the hood, so callers cannot tell that
|
|
20
|
+
the plan was inspected.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, query: str, plan: dict[str, Any]) -> None:
|
|
24
|
+
self.query = query
|
|
25
|
+
self.plan = plan
|
|
26
|
+
super().__init__(f"Unoptimized query uses a sequential scan: {query!r}")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_EXPLAINABLE_COMMAND_TAGS = frozenset({"SELECT", "INSERT", "UPDATE", "DELETE"})
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _plan_uses_seq_scan(node: dict[str, Any]) -> bool:
|
|
33
|
+
if "Seq Scan" in str(node.get("Node Type", "")):
|
|
34
|
+
return True
|
|
35
|
+
return any(_plan_uses_seq_scan(child) for child in node.get("Plans", []))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _reject_seq_scan(query: str, payload: list[dict[str, Any]]) -> None:
|
|
39
|
+
root_plan = payload[0]["Plan"]
|
|
40
|
+
if _plan_uses_seq_scan(root_plan):
|
|
41
|
+
raise UnoptimizedQueryError(query, root_plan)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SeqScanDetectingCursor(psycopg.Cursor[Row]):
|
|
45
|
+
"""Raw cursor that rejects any statement planned with a sequential scan.
|
|
46
|
+
|
|
47
|
+
After running each explainable statement it transparently re-plans it with
|
|
48
|
+
``EXPLAIN (FORMAT JSON)`` under ``SET LOCAL enable_seqscan = off``. A
|
|
49
|
+
sequential scan in that plan means no index can serve the query, even when
|
|
50
|
+
the session allows the planner to prefer sequential scans for optimization.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def execute(
|
|
54
|
+
self,
|
|
55
|
+
query: Query,
|
|
56
|
+
params: Params | None = None,
|
|
57
|
+
*,
|
|
58
|
+
prepare: bool | None = None,
|
|
59
|
+
binary: bool | None = None,
|
|
60
|
+
) -> Self:
|
|
61
|
+
result = super().execute(
|
|
62
|
+
query, # type: ignore
|
|
63
|
+
params,
|
|
64
|
+
prepare=prepare,
|
|
65
|
+
binary=binary,
|
|
66
|
+
)
|
|
67
|
+
if self._status_command() in _EXPLAINABLE_COMMAND_TAGS:
|
|
68
|
+
self._explain_no_seq_scan(query, params)
|
|
69
|
+
return result
|
|
70
|
+
|
|
71
|
+
def executemany(
|
|
72
|
+
self,
|
|
73
|
+
query: Query,
|
|
74
|
+
params_seq: Iterable[Params],
|
|
75
|
+
*,
|
|
76
|
+
returning: bool = False,
|
|
77
|
+
) -> None:
|
|
78
|
+
params_list = list(params_seq)
|
|
79
|
+
super().executemany(query, params_list, returning=returning)
|
|
80
|
+
if self._status_command() in _EXPLAINABLE_COMMAND_TAGS:
|
|
81
|
+
for params in params_list:
|
|
82
|
+
self._explain_no_seq_scan(query, params)
|
|
83
|
+
|
|
84
|
+
def _status_command(self) -> str | None:
|
|
85
|
+
if self.connection.info.pipeline_status != pq.PipelineStatus.OFF:
|
|
86
|
+
self.connection._pipeline.sync() # type: ignore[union-attr]
|
|
87
|
+
return self.statusmessage.split(maxsplit=1)[0] if self.statusmessage else None
|
|
88
|
+
|
|
89
|
+
def _explain_no_seq_scan(self, query: Query, params: Params | None) -> None:
|
|
90
|
+
explain_query = sql.SQL("EXPLAIN (FORMAT JSON) {}").format(
|
|
91
|
+
sql.SQL(query) if isinstance(query, str) else query
|
|
92
|
+
)
|
|
93
|
+
with psycopg.RawCursor(self.connection) as plan_cursor:
|
|
94
|
+
plan_cursor.execute(explain_query, params)
|
|
95
|
+
row = next(plan_cursor)
|
|
96
|
+
query_label = (
|
|
97
|
+
query
|
|
98
|
+
if isinstance(query, str)
|
|
99
|
+
else explain_query.as_string(self.connection)
|
|
100
|
+
)
|
|
101
|
+
_reject_seq_scan(query_label, row["QUERY PLAN"])
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class SeqScanDetectingRawCursor(
|
|
105
|
+
RawCursorMixin[psycopg.Connection[Any], Row], SeqScanDetectingCursor[Row]
|
|
106
|
+
):
|
|
107
|
+
__module__ = "psycopg"
|
{turbo_lambda-0.7.1 → turbo_lambda-0.9.0}/src/turbo_lambda/scripts/update_turbo_lambda_layer.py
RENAMED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import re
|
|
3
|
-
from
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
5
|
from turbo_lambda.version import __version__
|
|
6
6
|
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
|
|
7
10
|
|
|
8
11
|
def main(version: str = __version__, argv: Sequence[str] | None = None) -> int:
|
|
9
12
|
parser = argparse.ArgumentParser(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|