-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathpyproject.toml
More file actions
302 lines (281 loc) · 15.7 KB
/
Copy pathpyproject.toml
File metadata and controls
302 lines (281 loc) · 15.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
[project]
name = "serverless-app"
version = "3.0.0"
description = "Production-grade AWS CDK reference architecture for Lambda + Powertools serverless applications, with multi-rule-pack cdk-nag, WAF, CloudTrail data events, CMK encryption end-to-end, and supply-chain hygiene."
requires-python = ">=3.13"
readme = "README.md"
license = "Apache-2.0"
# No runtime dependencies at the project level — see [dependency-groups] below.
# The project splits into two resolution environments because CDK and Powertools
# require incompatible `attrs` versions (CDK pulls attrs<26 via jsii, Powertools
# pulls attrs>=26). [tool.uv.conflicts] below declares the `lambda` and `cdk`
# groups as mutually exclusive so uv locks both resolutions in one uv.lock.
dependencies = []
[project.urls]
Repository = "https://github.com/timpugh/lambda-powertools-reference"
Issues = "https://github.com/timpugh/lambda-powertools-reference/issues"
# =============================================================================
# Dependency groups (PEP 735) — resolved together by `uv lock`, installed
# selectively with `uv sync --group <name>`. The `lambda` and `cdk` groups
# are declared as conflicts in [tool.uv] and must never be installed into
# the same venv.
# =============================================================================
[dependency-groups]
# Lambda runtime — ships in the deployment bundle AND is installed locally
# for unit tests that import `lambda/app.py`.
lambda = [
"aws-lambda-powertools[all]==3.29.0",
"aws-xray-sdk==2.15.0",
"boto3==1.43.40",
]
# CDK infrastructure — used by `app.py`, `infrastructure/`, and the CDK stack
# assertion tests. Conflicts with `lambda` at the `attrs` pin (CDK requires
# attrs<26 via jsii; Powertools requires attrs>=26).
cdk = [
"aws-cdk-lib==2.261.0",
"constructs==10.6.0",
"aws-cdk-aws-lambda-python-alpha==2.261.0a0",
"cdk-monitoring-constructs==10.1.0",
"cdk-nag==3.0.1",
]
# Test runner — environment-agnostic; pairs with either `lambda` (unit tests,
# integration tests) or `cdk` (stack assertion tests).
test = [
"pytest==9.0.3",
"pytest-env==1.6.0",
"pytest-cov==7.1.0",
"pytest-xdist==3.8.0",
"pytest-mock==3.15.1",
"pytest-html==4.2.0",
"pytest-timeout==2.4.0",
"pytest-randomly==4.1.0",
"requests==2.33.1",
]
# Linting / static analysis / supply-chain checks — environment-agnostic.
# pydantic is pinned here (same version the lambda group resolves) so the
# pydantic.mypy plugin works in BOTH venvs: .venv-lambda already gets pydantic
# via Powertools, but .venv (CDK side) has no other reason to carry it, and
# mypy errors out at startup if a configured plugin is not importable.
# pydantic has no `attrs` dependency, so it does not touch the lambda/cdk
# resolution conflict.
lint = [
"radon==6.0.1",
"xenon==0.9.3",
"mypy==2.1.0",
"ruff==0.15.20",
"pylint==4.0.6",
"bandit==1.9.4",
"pip-audit==2.10.1",
"pre-commit==4.6.0",
"boto3-stubs==1.43.39",
"pydantic==2.13.4",
]
# Documentation build — used only by `make docs`. The Zensical static site
# generator is pre-1.0, so versions are pinned exactly.
docs = [
"zensical==0.0.46",
"mkdocstrings==1.0.4",
"mkdocstrings-python==2.0.5",
]
# =============================================================================
# uv configuration
# =============================================================================
[tool.uv]
# The `lambda` and `cdk` groups resolve `attrs` to incompatible versions.
# Declaring them as conflicts lets uv produce one uv.lock that contains both
# resolutions, installable independently via `uv sync --only-group lambda`
# or `uv sync --group cdk`.
conflicts = [
[{ group = "lambda" }, { group = "cdk" }],
]
# =============================================================================
# Ruff — fast Python linter and formatter (replaces flake8, isort, and black)
# =============================================================================
[tool.ruff]
target-version = "py313"
line-length = 120
exclude = [".venv", "cdk.out", "docs", "__pycache__"]
[tool.ruff.lint]
# Allows _-prefixed variables (e.g. _, _unused) to be unused without warnings
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes — unused imports, undefined names
"I", # isort — import ordering
"C", # flake8-comprehensions — inefficient list/dict/set comprehensions
"B", # flake8-bugbear — likely bugs and design issues
"S", # flake8-bandit — security checks
"UP", # pyupgrade — modernize syntax for the target Python version
"SIM", # flake8-simplify — suggest simpler code patterns
"RUF", # ruff-specific rules
"T20", # flake8-print — catches print() (use Logger instead in Lambda)
"PT", # flake8-pytest-style — enforces pytest conventions
"N", # pep8-naming — snake_case functions, PascalCase classes, SCREAMING_SNAKE constants
"RET", # flake8-return — unnecessary else after return, missing/redundant return values
]
ignore = [
"S101", # allow assert in tests
"E203", # whitespace before ':' (formatter handles this)
"E501", # line-too-long (formatter enforces line-length instead)
"W191", # indentation contains tabs
]
# Controls ruff format (the formatter, equivalent to black)
[tool.ruff.format]
quote-style = "double" # enforce double quotes throughout
indent-style = "space" # spaces, not tabs
line-ending = "auto" # detect line endings per file (LF on Unix, CRLF on Windows)
# Controls how imports are grouped and sorted (replaces isort)
[tool.ruff.lint.isort]
# Ensures these packages sort into the third-party group rather than first-party
known-third-party = ["aws_lambda_powertools", "boto3"]
[tool.ruff.lint.per-file-ignores]
# E402: module-level import not at top of file — __init__.py sometimes needs setup code first
"__init__.py" = ["E402"]
# S101: assert statements — valid in pytest; S106: hardcoded password — false positive on fixtures
"tests/**/*.py" = ["S101", "S106"]
# E402: imports follow pytest.importorskip() which must come first to skip the module when aws_cdk is absent
"tests/cdk/*.py" = ["E402"]
# S603/S607: shells out to the pinned `npx cdk` with a fixed argv (no shell, no
# untrusted input). Mirrored by `# nosec` for the separate bandit hook.
"scripts/cdk_pr_diff.py" = ["S603", "S607"]
# T201: this is a CLI gate script — its printed report IS the product (the
# violation list operators act on from the build log).
"scripts/check_validation_report.py" = ["T201"]
# =============================================================================
# Mypy — static type checker
# All settings tighten type safety beyond mypy's permissive defaults.
# =============================================================================
[tool.mypy]
python_version = "3.13"
warn_return_any = true # warn when a typed function returns Any (often masks missing types)
warn_unused_configs = true # warn if a mypy config section is unused or misspelled
warn_unused_ignores = true # warn if a # type: ignore comment is no longer needed (prevents rot)
disallow_untyped_defs = true # every function must have complete type annotations
check_untyped_defs = true # type-check function bodies even if the function lacks annotations
no_implicit_optional = true # f(x: str = None) does not implicitly mean Optional[str]
ignore_missing_imports = true # suppress errors for third-party packages without type stubs
show_error_codes = true # print [error-code] next to each error (required for type: ignore[code])
# pydantic plugin: type-checks model constructor calls against field types and
# catches misspelled field names. pydantic is pinned in BOTH venvs (lambda group
# via Powertools; lint group explicitly) so this plugin loads in the CDK-side
# pre-commit hook and in the .venv-lambda `make typecheck` run alike.
plugins = ["pydantic.mypy"]
[tool.pydantic-mypy]
init_forbid_extra = true # flag unknown keyword args to model constructors
init_typed = true # type-check constructor args instead of accepting Any
warn_required_dynamic_aliases = true # warn when a required field uses a dynamic alias mypy can't verify
# Tests are deliberately outside the mypy gate: the pre-commit hook excludes
# tests/ and `make typecheck` only passes infrastructure/, lambda/, and scripts/.
# Encoding that policy here (not just in hook excludes) keeps the editor's
# mypy extension — which type-checks any opened file against this config —
# in agreement with the CLI gates, instead of flagging strict-mode errors
# (missing annotations, requests stubs) the project never enforces on tests.
[[tool.mypy.overrides]]
module = "tests.*"
ignore_errors = true
# =============================================================================
# Pylint — design and complexity checks that complement ruff's style checks
# Disabled rules are handled by ruff or produce false positives in this project.
# =============================================================================
[tool.pylint."messages control"]
disable = [
"C0114", # missing-module-docstring (not enforced in this project)
"C0115", # missing-class-docstring
"C0116", # missing-function-docstring
"C0301", # line-too-long (ruff handles this)
"C0303", # trailing-whitespace (ruff handles this)
"C0304", # missing-final-newline (ruff handles this)
"C0305", # trailing-newlines (ruff handles this)
"C0411", # wrong-import-order (ruff isort handles this)
"E0401", # import-error (mypy handles this more accurately)
"E1120", # no-value-for-argument (false positive on Powertools decorators)
"R0903", # too-few-public-methods (acceptable for CDK constructs and data classes)
"W0611", # unused-import (ruff F401 handles this)
"W0612", # unused-variable (ruff F841 handles this)
"W0613", # unused-argument (unavoidable with some decorator signatures)
"W0718", # broad-exception-caught (intentional for non-critical fallback paths)
]
[tool.pylint.format]
max-line-length = 120 # must match [tool.ruff] line-length above
max-module-lines = 1600 # CDK frontend stack legitimately exceeds default 1000 — many resources, each with its own nag suppressions, plus the audit-pass additions (KMS confused-deputy guards, CloudTrail bucket Deny, async DLQ wiring, RUM log-group cleanup CR), the hardening additions (custom HSTS + CSP ResponseHeadersPolicy), the review-pass additions (exact-API-host CSP, auto-delete-provider warning, extracted helper docstrings), and the 2026-06 review fixes (CloudTrail data-event scoping, BucketDeployment DLQ, auto-delete log-group race ordering, invalidation rollback caveat)
# Structural complexity thresholds — pylint fails if any function or class exceeds these.
# Complexity is also enforced via the xenon pre-commit hook.
[tool.pylint.design]
max-args = 8 # max parameters per function
max-locals = 32 # max local variables per function (CDK stacks legitimately have many — frontend __init__ wires ~31 after the audit-pass additions)
max-returns = 6 # max return statements per function
max-branches = 12 # max branches (if/for/while/try) per function
max-statements = 55 # max statements per function body — frontend __init__ runs ~52 after the audit-pass additions
max-attributes = 10 # max instance attributes per class
# =============================================================================
# Pytest — test runner configuration
# =============================================================================
[tool.pytest.ini_options]
testpaths = ["tests"] # only collect tests from tests/ (avoids scanning the whole project)
timeout = 30 # fail any test that runs longer than 30 seconds (pytest-timeout)
# NOTE: addopts below hardcodes lambda/ coverage with a 100% gate, which only makes
# sense for the unit suite. The cdk and integration suites measure no lambda/ code,
# so `make test-cdk` / `make test-integration` (and the CI cdk-check job) pass
# --override-ini="addopts=" to drop these flags. Running `pytest tests/cdk` directly
# WITHOUT that override will fail the coverage gate — use the make targets.
# The VS Code Testing panel passes the same override (python.testing.pytestArgs in
# .vscode/settings.json and tests/unit/.vscode/settings.json): test DISCOVERY is a
# collect-only run that executes nothing, so without the override it reports
# "coverage 0% / FAIL" noise and rewrites report.html+htmlcov on every refresh.
# The 100% gate is enforced where the full unit suite actually runs: `make test`
# and the CI test job.
addopts = [
"-ra", # print a short summary of all non-passed tests at the end
"--cov=lambda", # measure coverage for the lambda/ source directory
"--cov-branch", # track branch coverage (not just line coverage)
"--cov-report=term-missing", # show uncovered line numbers in the terminal output
"--cov-report=html", # also generate an HTML coverage report
"--cov-fail-under=100", # fail the run if total coverage drops below 100%
"--no-cov-on-fail", # skip coverage reporting when tests fail (avoids misleading output)
"-n", "auto", # run tests in parallel across all CPU cores (pytest-xdist)
"--html=report.html", # generate an HTML test results report (pytest-html)
"--self-contained-html", # embed all assets into the HTML file (no external dependencies)
]
log_cli = true # stream log output in real time during the test run
log_cli_level = "WARNING" # only show WARNING and above (suppress DEBUG/INFO noise)
env = [
# Required because importing lambda/app.py constructs boto3 clients
# (SSM / AppConfig / DynamoDB) at module load, and a boto3 client needs a
# region. Declaring it here keeps the unit suite hermetic — no ambient AWS
# config required — so every path that runs it (make test, make coverage,
# make coverage-badge, and CI, including the docs job's badge step) gets a
# region without setting one per-invocation. Unit tests mock all AWS calls,
# so the value is never used against a real endpoint.
"AWS_DEFAULT_REGION=us-east-1",
"AWS_BACKEND_STACK_NAME=ServerlessAppBackend-us-east-1",
"AWS_FRONTEND_STACK_NAME=ServerlessAppFrontend-us-east-1",
"POWERTOOLS_IDEMPOTENCY_DISABLED=true",
"POWERTOOLS_SERVICE_NAME=serverless-app",
"POWERTOOLS_METRICS_NAMESPACE=ServerlessApp",
"POWERTOOLS_LOG_LEVEL=INFO",
"IDEMPOTENCY_TABLE_NAME=test-idempotency",
"GREETING_PARAM_NAME=/test/greeting",
"APPCONFIG_APP_NAME=test-app",
"APPCONFIG_ENV_NAME=ServerlessAppBackend-test-env",
"APPCONFIG_PROFILE_NAME=ServerlessAppBackend-test-features",
]
# =============================================================================
# Coverage — controls pytest-cov and standalone `coverage` runs
# =============================================================================
[tool.coverage.run]
source = ["lambda"] # measure coverage for the lambda/ directory only
omit = ["tests/**"] # exclude test files themselves from coverage measurement
branch = true # track branch coverage in addition to line coverage
[tool.coverage.report]
exclude_lines = [
"pragma: no cover", # manual opt-out on a per-line basis
"if TYPE_CHECKING:", # type-only imports are never executed at runtime
# Conventional defensive/unreachable patterns, excluded so the 100% gate stays
# meaningful as code is added without needing a per-line pragma on each one.
# (A bare `...` ellipsis is deliberately NOT excluded: it would also match the
# Pydantic Field(...) required-field marker, which is real executed code.)
"raise NotImplementedError",
"if __name__ == .__main__.:",
"@(abc\\.)?abstractmethod",
]