From 5f67545c75d7438061e3551477799f7b3ff466b2 Mon Sep 17 00:00:00 2001 From: ks6088ts Date: Mon, 9 Nov 2026 08:13:32 +0900 Subject: [PATCH 1/7] build: default to Python 3.14, extend lint rules, and pin uv range Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .python-version | 1 + pyproject.toml | 68 +++++++++-- uv.lock | 319 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 381 insertions(+), 7 deletions(-) create mode 100644 .python-version diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..6324d40 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.14 diff --git a/pyproject.toml b/pyproject.toml index f1acf21..658f485 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,23 +1,27 @@ [project] name = "template-python" version = "0.0.13" -description = "A minimal, typed Python package and CLI template" +description = "A minimal, typed Python CLI template with a production-grade toolchain" readme = "README.md" requires-python = ">=3.12" license = "MIT" license-files = ["LICENSE"] -keywords = ["python", "template", "typer", "uv"] +authors = [{ name = "ks6088ts" }] +keywords = ["cli", "python", "template", "typer", "uv"] classifiers = [ "Development Status :: 3 - Alpha", "Environment :: Console", - "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3.15", "Typing :: Typed", ] dependencies = [ + "pydantic>=2.12,<3", "pydantic-settings>=2.15,<3", "typer>=0.27,<1", ] @@ -42,7 +46,9 @@ test = [ ] dev = [ { include-group = "test" }, + "actionlint-py>=1.7.12,<2", "mypy>=2.4,<3", + "pip-audit>=2.10,<3", "prek>=0.5.4,<0.6", "ruff>=0.17.1,<0.18", "zizmor>=1.30.1,<2", @@ -55,27 +61,75 @@ notebook = [ requires = ["uv_build>=0.13,<0.14"] build-backend = "uv_build" +[tool.uv] +required-version = ">=0.13.1,<0.14" + [tool.ruff] line-length = 120 target-version = "py312" [tool.ruff.lint] -select = ["B", "E", "F", "I", "PT", "RUF", "SIM", "UP"] +select = [ + "A", # flake8-builtins + "ARG", # flake8-unused-arguments + "B", # flake8-bugbear + "BLE", # flake8-blind-except + "C4", # flake8-comprehensions + "DTZ", # flake8-datetimez + "E", # pycodestyle errors + "ERA", # eradicate (commented-out code) + "F", # pyflakes + "FBT", # flake8-boolean-trap + "FURB", # refurb + "G", # flake8-logging-format + "I", # isort + "LOG", # flake8-logging + "N", # pep8-naming + "PERF", # perflint + "PIE", # flake8-pie + "PL", # pylint + "PT", # flake8-pytest-style + "PTH", # flake8-use-pathlib + "RET", # flake8-return + "RUF", # ruff-specific + "S", # flake8-bandit (security) + "SIM", # flake8-simplify + "T20", # flake8-print + "TRY", # tryceratops + "UP", # pyupgrade + "W", # pycodestyle warnings +] + +[tool.ruff.lint.per-file-ignores] +# Typer declares boolean flags as keyword parameters with defaults. +"src/template_python/cli.py" = ["FBT001", "FBT002"] +"tests/**" = ["PLR2004", "S101"] -[tool.pytest.ini_options] -addopts = "-ra --cov=src/template_python" +[tool.pytest] +minversion = "9.0" +addopts = ["-ra", "--cov", "--cov-report=term-missing"] +strict = true testpaths = ["tests"] +filterwarnings = ["error"] [tool.coverage.run] branch = true +source_pkgs = ["template_python"] [tool.coverage.report] fail_under = 95 show_missing = true +partial_also = ['if __name__ == "__main__":'] [tool.mypy] files = ["src", "tests"] python_version = "3.12" strict = true -show_error_codes = true +warn_unreachable = true +enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] plugins = ["pydantic.mypy"] + +[tool.pydantic-mypy] +init_forbid_extra = true +init_typed = true +warn_required_dynamic_aliases = true diff --git a/uv.lock b/uv.lock index a19d80c..0922cef 100644 --- a/uv.lock +++ b/uv.lock @@ -7,6 +7,12 @@ resolution-markers = [ "python_full_version < '3.14'", ] +[[package]] +name = "actionlint-py" +version = "1.7.12.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/75/36ee89db39bb3620d1e46fe1f6ec77d6000d30ff45345699e51b4c850e83/actionlint_py-1.7.12.25.tar.gz", hash = "sha256:83f83be4cd317c79b15cbb7022787513a6994f00bfb2dd8da02fa4e62b4f640d", size = 12438, upload-time = "2026-09-22T16:24:42.477Z" } + [[package]] name = "annotated-doc" version = "0.0.5" @@ -259,6 +265,33 @@ css = [ { name = "tinycss2" }, ] +[[package]] +name = "boolean-py" +version = "5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/cf/85379f13b76f3a69bca86b60237978af17d6aa0bc5998978c3b8cf05abb2/boolean_py-5.0.tar.gz", hash = "sha256:60cbc4bad079753721d32649545505362c754e121570ada4658b852a3a318d95", size = 37047, upload-time = "2025-04-03T10:39:49.734Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/ca/78d423b324b8d77900030fa59c4aa9054261ef0925631cd2501dd015b7b7/boolean_py-5.0-py3-none-any.whl", hash = "sha256:ef28a70bd43115208441b53a045d1549e2f0ec6e3d08a9d142cbc41c1938e8d9", size = 26577, upload-time = "2025-04-03T10:39:48.449Z" }, +] + +[[package]] +name = "cachecontrol" +version = "0.14.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "msgpack" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2d/f6/c972b32d80760fb79d6b9eeb0b3010a46b89c0b23cf6329417ff7886cd22/cachecontrol-0.14.4.tar.gz", hash = "sha256:e6220afafa4c22a47dd0badb319f84475d79108100d04e26e8542ef7d3ab05a1", size = 16150, upload-time = "2025-11-14T04:32:13.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/79/c45f2d53efe6ada1110cf6f9fca095e4ff47a0454444aefdde6ac4789179/cachecontrol-0.14.4-py3-none-any.whl", hash = "sha256:b7ac014ff72ee199b5f8af1de29d60239954f223e948196fa3d84adaffc71d2b", size = 22247, upload-time = "2025-11-14T04:32:11.733Z" }, +] + +[package.optional-dependencies] +filecache = [ + { name = "filelock" }, +] + [[package]] name = "certifi" version = "2026.10.23" @@ -622,6 +655,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/da/91/1805cc144497d76a436f66a21fa57492bee7ed1257826fb992f03b9b2b6f/coverage-7.17.2-py3-none-any.whl", hash = "sha256:e674027ceb6bfd959f1bb035000fd20fa8b717dc6765adaaf8bca347822c8c7d", size = 219280, upload-time = "2026-11-04T19:52:32.726Z" }, ] +[[package]] +name = "cyclonedx-python-lib" +version = "11.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "license-expression" }, + { name = "packageurl-python" }, + { name = "py-serializable" }, + { name = "sortedcontainers" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/40/6509e6cfd7f2f3255501690f46375fdd224949e21fd1e96f4f4c8a9041b1/cyclonedx_python_lib-11.12.0.tar.gz", hash = "sha256:16767c4039de90c04e9f03348f8f0ed4b8ff842eaa7eefcad3a95685f970dacf", size = 1445378, upload-time = "2026-08-13T07:52:18.675Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/f0/b2cb999244f2f4194df63ecff6939228178fa63a33be809c412684ca8db7/cyclonedx_python_lib-11.12.0-py3-none-any.whl", hash = "sha256:0e807521a921a5c3cb8ce1153f8a61d29eedfe76a46aac2796b7c6b573391a54", size = 529453, upload-time = "2026-08-13T07:52:16.836Z" }, +] + [[package]] name = "debugpy" version = "1.8.22" @@ -674,6 +723,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d7/0d/6caab33488dc9d26440c24221fa610c7c8b83c30715ab45d31f0e004308e/fastjsonschema-2.22.3-py3-none-any.whl", hash = "sha256:f670b443830b75aca3f00b75949efa20f4b2a80302ec7a21505c28dd54cffd43", size = 28442, upload-time = "2026-11-01T21:11:29.306Z" }, ] +[[package]] +name = "filelock" +version = "4.2.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/1d/d730c664fcb4f1d264a722722a3134cf0881854f73c1fd58abf87572f232/filelock-4.2.9.tar.gz", hash = "sha256:96530b7c538d4ea08493ab067247aab7948c54d8cd622cce711c97cccc5d9896", size = 663116, upload-time = "2026-11-04T18:37:10.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/05/76184fe08339723d643ab47887a8e4de8f2921ca4e008617ab7490af7857/filelock-4.2.9-py3-none-any.whl", hash = "sha256:a379c50a729e5b68c8519a4d26489f755d9769960a236ecd838190e2a78db2e9", size = 164734, upload-time = "2026-11-04T18:37:08.273Z" }, +] + [[package]] name = "fqdn" version = "1.6.0" @@ -1177,6 +1235,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/69/7a5d10ac409c4da0355e054a14371871da9b5557fcc42772cd00181c6cce/librt-0.16.0-cp315-cp315t-win_arm64.whl", hash = "sha256:8ceafb70f2a4f0826f11031942e59c0728fd98da112dc346d4352bde1e486866", size = 116391, upload-time = "2026-09-29T00:55:07.484Z" }, ] +[[package]] +name = "license-expression" +version = "30.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "boolean-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/71/d89bb0e71b1415453980fd32315f2a037aad9f7f70f695c7cec7035feb13/license_expression-30.4.4.tar.gz", hash = "sha256:73448f0aacd8d0808895bdc4b2c8e01a8d67646e4188f887375398c761f340fd", size = 186402, upload-time = "2025-07-22T11:13:32.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/40/791891d4c0c4dab4c5e187c17261cedc26285fd41541577f900470a45a4d/license_expression-30.4.4-py3-none-any.whl", hash = "sha256:421788fdcadb41f049d2dc934ce666626265aeccefddd25e162a26f23bcbf8a4", size = 120615, upload-time = "2025-07-22T11:13:31.217Z" }, +] + [[package]] name = "markdown" version = "3.11.3" @@ -1431,6 +1501,83 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6a/0b/43ff4afb6b438d47718b1959a22075ed95d8460d8c47381878b37a40de63/mkdocs_static_i18n-1.3.1-py3-none-any.whl", hash = "sha256:4036e24795a150c9c4d4b001ed24a43aec01335f76188dbe5a5d8fb4a27eba65", size = 21853, upload-time = "2026-02-20T10:42:40.551Z" }, ] +[[package]] +name = "msgpack" +version = "1.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/52/1ec7ead2c7528bf3d1db73e886fa4df10f9e6f0eb7854a448b7090cfcef7/msgpack-1.2.4.tar.gz", hash = "sha256:475c44e12f2a5de766a8448783f149c9b33f3f49b88953c6cd739a77d93c732c", size = 197764, upload-time = "2026-10-26T17:55:48.593Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/fd/45f6cd13cefbb659a8e0128ea64d51b349571163fec3c9c20f99792dc42a/msgpack-1.2.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:90cba4996b18ec1201fcaabd01ec48ff4b9755416bcd52105ded2f89fe145c49", size = 91903, upload-time = "2026-10-26T17:54:03.097Z" }, + { url = "https://files.pythonhosted.org/packages/7f/89/169c2a0b1ffeafe68a4f13d8d96b6ed1cd8d40d8262b3a6690851da7eda1/msgpack-1.2.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:112933726919f4239646cca9572e2c2fa5626358c670da8440aac09403100a48", size = 90573, upload-time = "2026-10-26T17:54:04.383Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/272a384f2a001cfced1baab0a8abdd912d6281432670f4609d1aab718d7b/msgpack-1.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ca1e78106675f2aed07dc37b842ce19be70010ab22c5c5c04688f0840d48539", size = 461057, upload-time = "2026-10-26T17:54:05.821Z" }, + { url = "https://files.pythonhosted.org/packages/1e/d9/f59bd841fc2fd2b74dc50ce85860a6a25f58e588dd48946aeaf2862cf990/msgpack-1.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c4d5bbdbea8f5669b3a734d6c226772804fb120848f99cbe63a0ddee63b049c9", size = 475652, upload-time = "2026-10-26T17:54:07.414Z" }, + { url = "https://files.pythonhosted.org/packages/00/03/afd000e1b1e28fd535dc8c7bc21ebb88923bc1964a2429fe660fb313ace3/msgpack-1.2.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d9ff7f3aaffd84010f1c501b05406a497584f5cab2f732eb9d89bf960e12f6f", size = 427082, upload-time = "2026-10-26T17:54:09.1Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7a/e2890b272883c496faad777324ddb70e9cc4a6c63731962a4a39a6a40f44/msgpack-1.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aa10dc04c03408f31948ac1896a942141fd8d7a954b3612edac79f19663e4076", size = 451956, upload-time = "2026-10-26T17:54:10.476Z" }, + { url = "https://files.pythonhosted.org/packages/c2/46/0c1b240b8a580af3e72139c33b018025440a1a42c2579e82e39b1195ee36/msgpack-1.2.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:472a25463678f2795143a7cb42e88a1b59cc2375e9881917e69a9323bde42114", size = 423833, upload-time = "2026-10-26T17:54:12.195Z" }, + { url = "https://files.pythonhosted.org/packages/80/8e/851415b1a9264e6abf64d8a9617d856769d59e247fb9470106f133aedbae/msgpack-1.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cbb8a677da3e845713925e9a203b8a120b1e9da9989836890ba4a4a187902701", size = 470951, upload-time = "2026-10-26T17:54:13.764Z" }, + { url = "https://files.pythonhosted.org/packages/c9/74/d75bf61aab9cb88b84f7d3a8eca8dd92e19a742b77ded300a1177e88d031/msgpack-1.2.4-cp312-cp312-win32.whl", hash = "sha256:2b0b7dff5beddc10f16ecea6b28c6b98014d1135d7015bd83adff5037f35848d", size = 68786, upload-time = "2026-10-26T17:54:15.049Z" }, + { url = "https://files.pythonhosted.org/packages/81/c5/dd0e78eb04f9a0773a810d25ef2cd8f16bb941487567d0c63f814881133c/msgpack-1.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:6e38b123681df22373d545520f54bbeddc4706d9235a1161061f12bd6648497d", size = 77272, upload-time = "2026-10-26T17:54:16.491Z" }, + { url = "https://files.pythonhosted.org/packages/65/e7/b38f63ae82777c0077daa56c4b09af39d29dcb8927ab2ff59268094069c5/msgpack-1.2.4-cp312-cp312-win_arm64.whl", hash = "sha256:6795c588b210b2f13d62c8fcc23bf779b509b839ec5c197a7ab4f5191459e126", size = 71884, upload-time = "2026-10-26T17:54:17.995Z" }, + { url = "https://files.pythonhosted.org/packages/2d/26/16eaad5fe6e199334fa5bd66070778665717c86e9a273525dc706ddb06a7/msgpack-1.2.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fb811ac3d463a75013b234d2aae56b9f0bdb0105caf4dd2ee65305b6a8a6078d", size = 92057, upload-time = "2026-10-26T17:54:19.616Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b8/9660be6d120820bc05106f291e824c73d35f4ab77d927788ae67ea80889c/msgpack-1.2.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:36258fd0909e1bf26e94604439108846432abe4ffa9a23c9dc2f6f1c60f94b60", size = 90542, upload-time = "2026-10-26T17:54:20.965Z" }, + { url = "https://files.pythonhosted.org/packages/6c/10/cd55a69fec29df26884b178dc4b1803442ec82abf929604aac619881917a/msgpack-1.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9c5ba0f053a7e98f013f083c9d1b2dffb26cad8c897c533d8286e44ed24c669", size = 455798, upload-time = "2026-10-26T17:54:22.434Z" }, + { url = "https://files.pythonhosted.org/packages/c9/94/562e0733593a780c70b0b18994f30c205149f7f9f4efbe3e9b00b88612c5/msgpack-1.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fabd415c41e62006c6641fcdf5b3e866f581c4e11c58328c0d0f69584b9577d", size = 469262, upload-time = "2026-10-26T17:54:24.043Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/76d1a5c597e04e1d360b05573d150bdb4502f5a014e6e9cf089c9537c156/msgpack-1.2.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7433d340de45003db3ed909a642c9469b598d175530f7a707cbaba64287d27ce", size = 420789, upload-time = "2026-10-26T17:54:25.587Z" }, + { url = "https://files.pythonhosted.org/packages/8a/c6/b72b8d8022718380db459ced8ffbabc7570ad2d22c7f2fa659c872cb6734/msgpack-1.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ccd6e4af5347eb9828147320449bcd30e3321f6397d4583ff16047485dd8be3d", size = 447068, upload-time = "2026-10-26T17:54:27.721Z" }, + { url = "https://files.pythonhosted.org/packages/cb/dc/f378ef1def4aa135e70325c09e22457f3203ae2a390a192e892267906f33/msgpack-1.2.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:23d1459a1d0b4c9f6629df072d592eb74424b3ed3656b69af507bc9023cc027f", size = 418896, upload-time = "2026-10-26T17:54:29.325Z" }, + { url = "https://files.pythonhosted.org/packages/62/93/a7e507378f43e91415cb6fda47e0ded0c9de1618a1f4ac4f2887912a92dd/msgpack-1.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9e73761fe9fc55a4e9d2df8e8f59b0979231af4dbf80b8bf90cd64c74173d779", size = 465764, upload-time = "2026-10-26T17:54:30.719Z" }, + { url = "https://files.pythonhosted.org/packages/60/76/7f39f49cbcdaa4aafe64b4941b2640772dbdc1cac3a9332f4c9ba6994da9/msgpack-1.2.4-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:d088d1f552e129859db830692268747aba0a1ee3e6e0fe6344dbb3aab1df9f6c", size = 53615, upload-time = "2026-10-26T17:54:32.213Z" }, + { url = "https://files.pythonhosted.org/packages/67/9b/85b93350dbd7b52b358882f7f17a88a03664a7a59a359406e6315c162634/msgpack-1.2.4-cp313-cp313-win32.whl", hash = "sha256:892d4f17d7962bfd2c2928b0a47542bf2948f074a7c04721b73d4138b70481e4", size = 68724, upload-time = "2026-10-26T17:54:33.484Z" }, + { url = "https://files.pythonhosted.org/packages/02/4a/a7e2778ce09a943c17b6f91c7acb2f9488357657c01f2cccb278297621c4/msgpack-1.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:318d4e634d2b320be10decd9968ad1ae0be1d80cba23b5528c37b429eeb0e3e0", size = 77039, upload-time = "2026-10-26T17:54:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/64/16/9ff7495a9853e9d7de74965fdadce15017e7213ce564bb5dafc5403c9108/msgpack-1.2.4-cp313-cp313-win_arm64.whl", hash = "sha256:bec3e34749b90cfcf116149aad11295c720ff4a74a7d0a35abdb968a396c1524", size = 71873, upload-time = "2026-10-26T17:54:36.19Z" }, + { url = "https://files.pythonhosted.org/packages/86/34/747f9ea2b428088201c73fcb3a885cebcbedcd71def0f1f3c36896294b27/msgpack-1.2.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:895e9369185df4a874768849405c30cba7990caceb3fcb1692ebe62bb050f55e", size = 92449, upload-time = "2026-10-26T17:54:37.503Z" }, + { url = "https://files.pythonhosted.org/packages/83/7e/52d208cb1561f869762c17be0e2c004e4d34f2842d239a450492d6246d62/msgpack-1.2.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c97f5f17407e1761f23f1944be33cbf1d5ce67017db32544dcd5e16e0b1b4a3b", size = 91202, upload-time = "2026-10-26T17:54:38.797Z" }, + { url = "https://files.pythonhosted.org/packages/2e/58/5516cabb60da83506024015c669622a0277979aef51eb611308cebaf4cc9/msgpack-1.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2444a4e04f924630d059ae6eae8f91fdfcd28ed9fd17b21bacf4e74550a091f7", size = 455185, upload-time = "2026-10-26T17:54:40.213Z" }, + { url = "https://files.pythonhosted.org/packages/39/8f/bf975b5d2111e4954a0f8567fc6b60bab22c8766dd4fa509af1334c91a4f/msgpack-1.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d65b791ce4283e02b6267742ad540899155e1b32887b9cbdda47f38f5a076b5", size = 465078, upload-time = "2026-10-26T17:54:41.971Z" }, + { url = "https://files.pythonhosted.org/packages/32/2f/c74baeafc53d1c1fadfbeb774444d0e6354778d12b40b827e437a7a35856/msgpack-1.2.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5e4c67dbac05dce90a3fa1d845137617383c415e6b319062baac9734540706ae", size = 420144, upload-time = "2026-10-26T17:54:44.994Z" }, + { url = "https://files.pythonhosted.org/packages/b0/f2/bdf2ce9e284821397dc5549101824c7607ee6ba6e0635390d168a77ac49c/msgpack-1.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:781e856db4f2cb398e4992e2a74305295d3ebf694eec26fa0f4bc766eb9fa0a0", size = 446479, upload-time = "2026-10-26T17:54:46.484Z" }, + { url = "https://files.pythonhosted.org/packages/76/8d/b4572d24bb75d98bcbbd031a934a3a1d03494ea88a100cd51ab16c4c7d57/msgpack-1.2.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cfc1cc237792f9a5664002b66acb961027d4184528ec9d10e0e42330f7972383", size = 418780, upload-time = "2026-10-26T17:54:48.04Z" }, + { url = "https://files.pythonhosted.org/packages/55/8e/4493bef4422730cfd3420b84094652fabd27ef8aadc8b443a8995b524d08/msgpack-1.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f60de6ae13d4d2a12a483f15c5e47cb6fa2731aa32d92fb31178e2017d6574a8", size = 462009, upload-time = "2026-10-26T17:54:49.433Z" }, + { url = "https://files.pythonhosted.org/packages/41/29/efdc3d1d09e4e0e45955b0aad559b4c1f5d908767c9fb1021990c52182a5/msgpack-1.2.4-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:06a0657a4110d563255006b7cb7147432f67827144e429c2cdb1c967548d1817", size = 53714, upload-time = "2026-10-26T17:54:50.753Z" }, + { url = "https://files.pythonhosted.org/packages/df/0b/08794c65e0a752acf20aa44e193e61dfb552b48fe6c27ba6023c146f9e77/msgpack-1.2.4-cp314-cp314-win32.whl", hash = "sha256:727a65b35db9f5ef516fc0229c4b05cab983092b1ae274a8a769b2d517fb0499", size = 70726, upload-time = "2026-10-26T17:54:52.397Z" }, + { url = "https://files.pythonhosted.org/packages/b5/da/e2cc63ec61e7ac05595b391cba01c10cb4657f4e517da1512e4a75d2d12e/msgpack-1.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:80f5d4fda1ba373d95933f98e6c1d18247feffac839fda3930bdd85e6561bb5f", size = 78346, upload-time = "2026-10-26T17:54:54.662Z" }, + { url = "https://files.pythonhosted.org/packages/93/e4/1b0eb49887009dc2c530171d659cf2aa37202b5f79c0b784e63777554b37/msgpack-1.2.4-cp314-cp314-win_arm64.whl", hash = "sha256:e5f67f0286f2ba2c6dbb4998404ed880112ca204ceda729cf239879cd89d3acf", size = 74152, upload-time = "2026-10-26T17:54:55.915Z" }, + { url = "https://files.pythonhosted.org/packages/d0/4a/6defc8fd0270c4ba675e6625f4f76ce936fd0c48733395fd7ba912cd423e/msgpack-1.2.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:45506bfb902cde24a0d33fb0581f58497ddb2682686dd1f6aa07e7b769df7ca4", size = 94083, upload-time = "2026-10-26T17:54:57.108Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e8/59f7e076739048864710a4de3403ae1c1b43ed5077b796cd9e8cbce9b3f6/msgpack-1.2.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dc9dc681958e69f46b9f37def3e7fd6cc5dcfaa5943fb00bfe9c223a1a1052f6", size = 95037, upload-time = "2026-10-26T17:54:58.489Z" }, + { url = "https://files.pythonhosted.org/packages/2c/dd/dbae250b23b831cd0571eb664089f80de3249a39e98c6ef55f9a0efe928d/msgpack-1.2.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0bef691e2a95119cc6bf3ded29325dbd89c4b6f8ff233070431948ed70902421", size = 472722, upload-time = "2026-10-26T17:54:59.924Z" }, + { url = "https://files.pythonhosted.org/packages/a9/e2/383ece1c1b27c1ed322055fce8ab8972bc6d529ede881592065ce0c461bf/msgpack-1.2.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:55e6106d2d085af42b2de72efeb38ed255241a24b32295ca889707c1d679c442", size = 468726, upload-time = "2026-10-26T17:55:01.492Z" }, + { url = "https://files.pythonhosted.org/packages/37/2f/5410a8815ae74a824ac87543c612e40cb6674851b70a6ad858bb32813203/msgpack-1.2.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:996dfc4774c240c8e6adf1166ae102141cfbed33c8856fde821bd6c0bd35ee41", size = 420488, upload-time = "2026-10-26T17:55:02.848Z" }, + { url = "https://files.pythonhosted.org/packages/39/8e/16fdf02b4f91dff9c1480433c1bf80756c2fedc0b74fc6a231f02dfed007/msgpack-1.2.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:efa1357fb0c7201e833de28638be371ee127476f7ccff9e7a94211b0afc33ddf", size = 455541, upload-time = "2026-10-26T17:55:04.564Z" }, + { url = "https://files.pythonhosted.org/packages/c4/76/2454ef54d780a167667552d02e7bc977a3d419943bcbdf6a96a4da6786ad/msgpack-1.2.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b9270b63409e70c8c6deb5faedc81fc27c6bd0196b38084a8773f68286abf034", size = 419514, upload-time = "2026-10-26T17:55:06.257Z" }, + { url = "https://files.pythonhosted.org/packages/ac/8b/2af9bf378def33c24875f250421b0d0b1b06f72a1c2c3f964749fc4cdee1/msgpack-1.2.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2b597bb01aff61d65e305c2cf2589c93c86609929d42d575bfc359f3c54ea8e1", size = 461910, upload-time = "2026-10-26T17:55:08.05Z" }, + { url = "https://files.pythonhosted.org/packages/0e/46/a133ac6c4cef4d3939383c00cc19baa16ee85fd70af0c5990e7a9ca8767f/msgpack-1.2.4-cp314-cp314t-win32.whl", hash = "sha256:ad5bc540cbc34db236f8535721a9a872d12a9d88d1322470f1fc7c5e9dadfdb2", size = 76441, upload-time = "2026-10-26T17:55:09.711Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f5/8d22ec454c188f1795107a3fd9f68ada485076ebf394a4aaaa5fbb6093e7/msgpack-1.2.4-cp314-cp314t-win_amd64.whl", hash = "sha256:287e52c2ebaa94f8eca045628374859f51584e77bc07086e6835a9bb2204c774", size = 84321, upload-time = "2026-10-26T17:55:10.947Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/1390514aee47d14ca3fe6ea0cf236fddc55021867ace30daf8761deba36f/msgpack-1.2.4-cp314-cp314t-win_arm64.whl", hash = "sha256:f3f19fe5decd348b35b003ae5f32d3fa90877413a0183da676f748a3a7ab9270", size = 78589, upload-time = "2026-10-26T17:55:12.425Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d8/0465d475c1a7063af1a1fb4f8c69e0dd661c4dd172055c9757a343a07d5d/msgpack-1.2.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:765584b93ed6f0b561f0c56e9735f24945c0682f857c9b3ee3b4caad9c32f66a", size = 92449, upload-time = "2026-10-26T17:55:13.665Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d0/5220a8a53579dda39f3526394ba5b7eafc28736085f2626deda7fcd22cd1/msgpack-1.2.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:0221748c30ace75fde0dec8c470abe65fb5374cd4f37adb9db6aca7a3c362c0a", size = 91209, upload-time = "2026-10-26T17:55:14.913Z" }, + { url = "https://files.pythonhosted.org/packages/b2/98/f151742eba413503074a10d8f636bf03dd4f3f7cba29689767a35d19bd1c/msgpack-1.2.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5917972ce1bf5f85f761031e00dab6fe0ad9f880cb3d2d43ed74b330c850400", size = 455713, upload-time = "2026-10-26T17:55:16.406Z" }, + { url = "https://files.pythonhosted.org/packages/b0/54/890510a2c9841aff63258e085d15d4e140c7eef559d4da81ef2d2d867436/msgpack-1.2.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7ae8cf6012372cecb43016132d1b87ccc2ebb8ae5ab5078687399e82423386a", size = 466177, upload-time = "2026-10-26T17:55:18.351Z" }, + { url = "https://files.pythonhosted.org/packages/3d/41/a6f20d7c2c380fb072b95ffc5b865a1d62eaf49d77fbee44466c45a1a8de/msgpack-1.2.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e78c66cf97ac5675037f705ca433ad98ac9177f9523e7fdbb34e486ae29044c8", size = 424398, upload-time = "2026-10-26T17:55:19.979Z" }, + { url = "https://files.pythonhosted.org/packages/89/86/fa9559961f2b0d498e6cefa226298ddee2b896cb223449c211702bc34071/msgpack-1.2.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:59887281c3c163a7afff7dffaaad2f78cd2b513b7e1a7843f1ee8e7cd6a94231", size = 445686, upload-time = "2026-10-26T17:55:21.775Z" }, + { url = "https://files.pythonhosted.org/packages/39/23/5a9d94036cdd77d4310bbb7cfaf3ccd1f087c4a5d5a4befec4361c5fc0dc/msgpack-1.2.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:9f46e70c8d22e96856967ab9d80c51dcf5bc63133e4da2d66fab64c0d4119b82", size = 422096, upload-time = "2026-10-26T17:55:24Z" }, + { url = "https://files.pythonhosted.org/packages/6e/4b/86a5384112509a94ff6e20028aba6845138743effb7c6617e959ad1256ab/msgpack-1.2.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:0a1fd6c31952155b7cd01061288c70303e6554ae855244dfb38a897fd94f8749", size = 463407, upload-time = "2026-10-26T17:55:26.146Z" }, + { url = "https://files.pythonhosted.org/packages/f4/cf/14db289e0ad531e22ce0f03284865f81d14fbc80bf3f79d3b1714bcf76cf/msgpack-1.2.4-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:b1f08ec826417e3c8dc3edadb50f38debba033af6cb849229f0eac72f80f2342", size = 53743, upload-time = "2026-10-26T17:55:27.497Z" }, + { url = "https://files.pythonhosted.org/packages/46/72/a76fc90b51503c0bdacbc7fc823aa6fa62d40596ac47f9ea8492d6f95a34/msgpack-1.2.4-cp315-cp315-win32.whl", hash = "sha256:e8ba23c838739fe929e868095171e71f2f38d6ce0fcf5840b576fb82648e58e6", size = 70780, upload-time = "2026-10-26T17:55:28.705Z" }, + { url = "https://files.pythonhosted.org/packages/58/a7/fa9d4a1b5dfead83ea6e03b4007041a5da988c25eb941d42612e0211d3c0/msgpack-1.2.4-cp315-cp315-win_amd64.whl", hash = "sha256:67310606cf2528a6dc3ece1298dcd5cc6c57add148b16d22c5111138aa84820c", size = 78288, upload-time = "2026-10-26T17:55:29.928Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f3/8163d7938a8fe003952fe2719ddb31da71998559bbe290c55ffe622b886b/msgpack-1.2.4-cp315-cp315-win_arm64.whl", hash = "sha256:ce76e7a7c11459157887a8dadf07c107ff74eca98cf914f0de6ee7ab4b47104d", size = 74235, upload-time = "2026-10-26T17:55:31.112Z" }, + { url = "https://files.pythonhosted.org/packages/b4/fa/37c3219b250d81bee788c0dd78ae2fc673db2da1c21ae51f7982d85698f7/msgpack-1.2.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:fb9ddbf23b8c6e89752af399fcf4daed60055e239cda44c6acfaa76d19331e8c", size = 93793, upload-time = "2026-10-26T17:55:32.304Z" }, + { url = "https://files.pythonhosted.org/packages/83/9f/b8afcd20d32cf4be1fa3c327e997656d6d446e667452b0fcc66b91710e82/msgpack-1.2.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:e283cf563166c21762131b88e3870ee2ec57757b08a8736929757b5a67541061", size = 94746, upload-time = "2026-10-26T17:55:33.624Z" }, + { url = "https://files.pythonhosted.org/packages/e6/4f/03348d381fcbbc2685356833408cc7e9d2af0fe0775926935a2889650f64/msgpack-1.2.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40e91bf8325f492e8eb3c3aa6de06b20593c225336bb52b585c913d7c1da025b", size = 468480, upload-time = "2026-10-26T17:55:34.991Z" }, + { url = "https://files.pythonhosted.org/packages/16/46/e54af8d9a51fb562e068e69a0d75a51e9c2fedec49f911f888515f5477ab/msgpack-1.2.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cfa86432bf329d6b976f5a00ad4c9064fd51736e4967a52526750236345cc2da", size = 470064, upload-time = "2026-10-26T17:55:36.615Z" }, + { url = "https://files.pythonhosted.org/packages/74/f5/8f9a6b3af1d803e832b11017c8dad44d761fefdb91dba04ee58cb51838da/msgpack-1.2.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:42e0a7dc26df739149478585d4264338b4d330f24b3e3b57d5aa89e48866e8ab", size = 421267, upload-time = "2026-10-26T17:55:38.076Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8f/7302b0d18eadbbfef9be1dbbdc49905f7f3b847200c3e47068d093976be4/msgpack-1.2.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:8e680367090a58c966052d61418cc943111863538fd82471d61fca5271d911e1", size = 451244, upload-time = "2026-10-26T17:55:39.639Z" }, + { url = "https://files.pythonhosted.org/packages/36/c6/05717fb56b510b5e974446f2cb8e9b26fa4347fafc0c5927745b176939b3/msgpack-1.2.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:a6c9926f8871b261622fe6b599db72c69be5bca8fdc0976886f9a531a301dee9", size = 420273, upload-time = "2026-10-26T17:55:41.488Z" }, + { url = "https://files.pythonhosted.org/packages/05/d9/a19077d664c37319097e16ba8de14e6b3a2edcc8ee78a2069c0808e38960/msgpack-1.2.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:c50bac325dbf1a9c80e10da6dbd1100d7082f6b12e153084501e18e86cd3a037", size = 460421, upload-time = "2026-10-26T17:55:43.223Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bd/0e2033a7435f8d2311be37363fedc88bdfa9fd95c364ee8ac3216eb63e36/msgpack-1.2.4-cp315-cp315t-win32.whl", hash = "sha256:34051de19ae532ba47fcab5b0abd08c25c7a64ff763ad819f18764d45f3c2c81", size = 76256, upload-time = "2026-10-26T17:55:44.588Z" }, + { url = "https://files.pythonhosted.org/packages/b3/3f/aa7265f25dafb6af7567a1a7d0a95c5566803db2226c5e084dc5850cfd96/msgpack-1.2.4-cp315-cp315t-win_amd64.whl", hash = "sha256:6ad2f0808099d4281208ff2c756efa958a55a7c8eb21fe1c7052b95c584f02f1", size = 83939, upload-time = "2026-10-26T17:55:45.773Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f2/c53904c4bc3da828fb75678bf489ece808a277affa73f8a7a91dfb7c5851/msgpack-1.2.4-cp315-cp315t-win_arm64.whl", hash = "sha256:e38d854f1c60e94f46d998765201126338bde3fe1b029be0d849094bcbef72c2", size = 77995, upload-time = "2026-10-26T17:55:47.016Z" }, +] + [[package]] name = "mypy" version = "2.4.1" @@ -1570,6 +1717,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef", size = 13307, upload-time = "2024-02-14T23:35:16.286Z" }, ] +[[package]] +name = "packageurl-python" +version = "0.17.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/d6/3b5a4e3cfaef7a53869a26ceb034d1ff5e5c27c814ce77260a96d50ab7bb/packageurl_python-0.17.6.tar.gz", hash = "sha256:1252ce3a102372ca6f86eb968e16f9014c4ba511c5c37d95a7f023e2ca6e5c25", size = 50618, upload-time = "2025-11-24T15:20:17.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/2f/c7277b7615a93f51b5fbc1eacfc1b75e8103370e786fd8ce2abf6e5c04ab/packageurl_python-0.17.6-py3-none-any.whl", hash = "sha256:31a85c2717bc41dd818f3c62908685ff9eebcb68588213745b14a6ee9e7df7c9", size = 36776, upload-time = "2025-11-24T15:20:16.962Z" }, +] + [[package]] name = "packaging" version = "26.3" @@ -1627,6 +1783,61 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, ] +[[package]] +name = "pip" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/c7/f8d9e2e0e7c1ed8a22a10eeb3b5bf7860bc07d8446c7a978a882c5c8214b/pip-26.3.tar.gz", hash = "sha256:6c36ae7da8695be67e3cd9f7d87919667ced53f57ce2ede75d89b5912ed2bc4c", size = 1934348, upload-time = "2026-11-02T20:34:27.738Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/25/ee8c38ae72d66b844ce8eb9711ba905dba21ca1a8a0c960057c41cd23337/pip-26.3-py3-none-any.whl", hash = "sha256:f3b60b20ffaadd4415ed7957212a38ac63fa4c5fa375bd0f4a797777940fad13", size = 1950951, upload-time = "2026-11-02T20:34:25.728Z" }, +] + +[[package]] +name = "pip-api" +version = "0.0.35" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pip" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/08/47dfc28aa16ef0dbc38dd7c6e4fe24d124c3ed3a722a4a3e5021584a588c/pip_api-0.0.35.tar.gz", hash = "sha256:14dea05d7b3b68f4c7582213e3ae8932bbba8e1f754bf06b95263d5fa6ab4363", size = 123884, upload-time = "2026-09-08T19:35:12.876Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/2e/d424c5b5877be862dd9a2c8b7e9ddb13382bb25e132e6dd50cc09c0a7dde/pip_api-0.0.35-py3-none-any.whl", hash = "sha256:66990d0da25378555cbb538db79384bab92d4617c6350dfd7a2126ba2a3a9894", size = 123943, upload-time = "2026-09-08T19:35:11.537Z" }, +] + +[[package]] +name = "pip-audit" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachecontrol", extra = ["filecache"] }, + { name = "cyclonedx-python-lib" }, + { name = "packaging" }, + { name = "pip-api" }, + { name = "pip-requirements-parser" }, + { name = "platformdirs" }, + { name = "requests" }, + { name = "rich" }, + { name = "tomli" }, + { name = "tomli-w" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/a4/f21d5f0a0edabcbce31560b73c7c5a6f72ae87af4236fd1069c8f59a353d/pip_audit-2.10.1.tar.gz", hash = "sha256:1eb4565d19ebe5d48996f4b770b4d2b32887e12cb12cfa637f1a064011b55ffc", size = 54275, upload-time = "2026-06-10T22:17:01.744Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/a7/b0c504148114047bd1bc9d97447453c6850ca176bb2f3c0038835994e8b7/pip_audit-2.10.1-py3-none-any.whl", hash = "sha256:99ef3f600a317c1945f1e89e227ef26e1c2d618429b8bd3fa6f4f7c440c4611a", size = 62023, upload-time = "2026-06-10T22:17:00.309Z" }, +] + +[[package]] +name = "pip-requirements-parser" +version = "32.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "pyparsing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/2a/63b574101850e7f7b306ddbdb02cb294380d37948140eecd468fae392b54/pip-requirements-parser-32.0.1.tar.gz", hash = "sha256:b4fa3a7a0be38243123cf9d1f3518da10c51bdb165a2b2985566247f9155a7d3", size = 209359, upload-time = "2022-12-21T15:25:22.732Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/d0/d04f1d1e064ac901439699ee097f58688caadea42498ec9c4b4ad2ef84ab/pip_requirements_parser-32.0.1-py3-none-any.whl", hash = "sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526", size = 35648, upload-time = "2022-12-21T15:25:21.046Z" }, +] + [[package]] name = "platformdirs" version = "4.14.5" @@ -1729,6 +1940,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6d/18/83376915176eb058cb86470eb7396388a13350b09a8f233a79303bbcbc5b/pure_eval-0.2.4-py3-none-any.whl", hash = "sha256:96cae060a313cfaad51bb761278bfb0e62dc0248d9315a81173752dc546cd37a", size = 11893, upload-time = "2026-09-10T21:41:21.532Z" }, ] +[[package]] +name = "py-serializable" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "defusedxml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/8c/cad9596066de835b304be48b0eefa30dae1834afae6e670805831cc05ea2/py_serializable-2.2.0.tar.gz", hash = "sha256:8e4fa02e8de819c0488ccdb69c2d3109274adcf224c02357fd8729cb12727aab", size = 52608, upload-time = "2026-11-06T13:43:39.943Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/56/94f6a9ac81193713e6be0d41c62d6b916b53c814c05d8b3a0d90976db0d6/py_serializable-2.2.0-py3-none-any.whl", hash = "sha256:2cce775abc5aac12115f9ff572a145c7276d04a770090334bd4e51ed9c782415", size = 23101, upload-time = "2026-11-06T13:43:38.593Z" }, +] + [[package]] name = "pycparser" version = "3.12" @@ -1899,6 +2122,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f6/47/e96d2b3439b9fab6dc085747f25aee0cbae2bae16743e86ca60907bd0d59/pymdown_extensions-13.0-py3-none-any.whl", hash = "sha256:c68f6e69b14604fb55227735bd0c4c4d3d1d9299a2d1314352190c48aa0d1040", size = 280470, upload-time = "2026-11-08T14:20:27.756Z" }, ] +[[package]] +name = "pyparsing" +version = "3.3.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/11/b213bebff182584360cb8d17c72c1677fec5c5c228de439e63bcf8ab1c8f/pyparsing-3.3.3.tar.gz", hash = "sha256:928ae7e20211f3b6f3915a72f06a0cfd29ab9d24279dd6346b6b1a7146397d36", size = 1050487, upload-time = "2026-09-20T20:59:05.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/bb/d215ee7c73b61497b28a5503f9f53523f294fcc936762b7caf90e0c1c2b5/pyparsing-3.3.3-py3-none-any.whl", hash = "sha256:ece8c00a69cf01b45d0b1dedabb469c90d8caf996d4fda40f147627a122849a4", size = 126420, upload-time = "2026-09-20T20:59:04.025Z" }, +] + [[package]] name = "pytest" version = "9.2.0" @@ -2326,6 +2558,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + [[package]] name = "soupsieve" version = "4.0.1" @@ -2354,13 +2595,16 @@ name = "template-python" version = "0.0.13" source = { editable = "." } dependencies = [ + { name = "pydantic" }, { name = "pydantic-settings" }, { name = "typer" }, ] [package.dev-dependencies] dev = [ + { name = "actionlint-py" }, { name = "mypy" }, + { name = "pip-audit" }, { name = "prek" }, { name = "pytest" }, { name = "pytest-cov" }, @@ -2382,13 +2626,16 @@ test = [ [package.metadata] requires-dist = [ + { name = "pydantic", specifier = ">=2.12,<3" }, { name = "pydantic-settings", specifier = ">=2.15,<3" }, { name = "typer", specifier = ">=0.27,<1" }, ] [package.metadata.requires-dev] dev = [ + { name = "actionlint-py", specifier = ">=1.7.12,<2" }, { name = "mypy", specifier = ">=2.4,<3" }, + { name = "pip-audit", specifier = ">=2.10,<3" }, { name = "prek", specifier = ">=0.5.4,<0.6" }, { name = "pytest", specifier = ">=9.1,<10" }, { name = "pytest-cov", specifier = ">=7.1,<8" }, @@ -2432,6 +2679,78 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl", hash = "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661", size = 28404, upload-time = "2025-11-23T10:29:08.676Z" }, ] +[[package]] +name = "tomli" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/78/9ad63712633ed3ab5cc1a648d863d7e7da371e9425e209555a0fe711b695/tomli-2.5.0.tar.gz", hash = "sha256:264507556cd8b8c8e7c6ee037cdf443a463f03f4c958e57195e3d369711b8ff6", size = 17662, upload-time = "2026-10-07T12:23:37.892Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/ed/7baa86f87493646a594de388c7c1c40a39dd0461f7e9c0359cbeefc91fe8/tomli-2.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f4a40d03fb9f63424f0979855bdeaf44dd7696b8d59501822c10ed30ba532df", size = 164854, upload-time = "2026-10-07T12:22:27.444Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b1/44c0341f2224397855723c7a8a39f718ea6fcbcc3dacc66e5aeca0f334e3/tomli-2.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9ebf8d19b17bd0daeb7b7dec81a946a439b753942fd0210d6e96c532249eea6b", size = 164074, upload-time = "2026-10-07T12:22:28.679Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e2d5b7d3fba47adedb23de616c16d428ea076c79a3d8e1d95d649ffe197e/tomli-2.5.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf0b5e8e0f68ebb494356e577c06c139161efd8d3b9050f93b39b7c26cc54ff0", size = 274274, upload-time = "2026-10-07T12:22:29.804Z" }, + { url = "https://files.pythonhosted.org/packages/43/90/6090e706ff27a6f89f4a40578e3324b95c3cd8c4150868aabf33a8f414c3/tomli-2.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cf74416bdc94ae458b14e37286c1073081850ac8459a00d0c5efef5d44294c6", size = 286435, upload-time = "2026-10-07T12:22:31.297Z" }, + { url = "https://files.pythonhosted.org/packages/0a/9e/a2c40768df16c408f22430afb0a73e9d7e5f79c950884954649d1146b74d/tomli-2.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:61ea1ebe1e55a34ea8199cc8dbff398d35027b82271c8ac4802fd3a1fd5b1bcc", size = 278119, upload-time = "2026-10-07T12:22:32.601Z" }, + { url = "https://files.pythonhosted.org/packages/12/25/3c0cb485b98e9cfac495629b1c93c87ccf0b72fbe9d2689fd8fe62c6d5a3/tomli-2.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed53f7e89bb04f6d9e8e7799112360b0c4d5cbff067de0814c98c37c39b920f7", size = 286177, upload-time = "2026-10-07T12:22:33.745Z" }, + { url = "https://files.pythonhosted.org/packages/77/8b/0144c65f0e37e51c18d04ae15c21b19431c165002d0131fe9aa8b0b8b1e8/tomli-2.5.0-cp312-cp312-win32.whl", hash = "sha256:e7ad033e27a516a233bea839cdb77b80146facb3b4f40bf02cd0cac165cdd5c2", size = 102760, upload-time = "2026-10-07T12:22:34.887Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/5d6d8f42fc9a05fce69354e00ff256484192f5f2fc9a2165718fa0de61ec/tomli-2.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:bd05de8c1698f8413dd7d869492693a0bf2211543b787ac78cd5e7536af1a6d7", size = 112722, upload-time = "2026-10-07T12:22:36.162Z" }, + { url = "https://files.pythonhosted.org/packages/30/65/df18032218db0fb9b769fb23c8039a051f15c811993995ea04c350273a32/tomli-2.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:069435bd5480429b98c5e5afb02ab21c219b6f0064680671c6dc0d46817346ea", size = 109534, upload-time = "2026-10-07T12:22:37.296Z" }, + { url = "https://files.pythonhosted.org/packages/42/e5/51736d70da209350969e15aca5c5ab6e2ce1ea87a0a892a6c13aec172a86/tomli-2.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:943276cf269e0071948d9ff697159c1735e623c1151d88abb09b74659ef0cbea", size = 163328, upload-time = "2026-10-07T12:22:38.373Z" }, + { url = "https://files.pythonhosted.org/packages/ec/55/086f80dab4ab497602644274e6dea7ec5dd0b4e262e443a8ad3bb7edee2d/tomli-2.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:463b16086865b97facd8d0b3fb4cb7c544e3f58d2a69dc3113d6db9653fdb043", size = 162246, upload-time = "2026-10-07T12:22:39.673Z" }, + { url = "https://files.pythonhosted.org/packages/aa/eb/3ecc94459f3635c92321f4e7bde571323fdb2267c50e19e3188a281eae3b/tomli-2.5.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1245a6638fc4bb0a60af38a7d45413db34a13842027c77597c712c998c62fdf0", size = 272655, upload-time = "2026-10-07T12:22:41.08Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d7/494fd1f0c37a621f1ad9975c2efadb523e8101f144ed6edb2e7fe64738f2/tomli-2.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d8bac3d603c97e6854424e5b2b5b741bdbde387e09f162fb0446812b4a8362b", size = 283595, upload-time = "2026-10-07T12:22:42.222Z" }, + { url = "https://files.pythonhosted.org/packages/70/51/bb8d62b1317e6640866f6949b2d5855e5300f2c99d46de1cd245570bba65/tomli-2.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:21e4cae4114aba25aa0d4f85cdf486d290fb35c0954d7bba536248da64d43066", size = 276253, upload-time = "2026-10-07T12:22:43.625Z" }, + { url = "https://files.pythonhosted.org/packages/66/f4/f46bd7f0763cd47de2db697dca9257c6a4adfd1a93b018cc75c8190ed5a8/tomli-2.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bbaefc84548d754be821bba7c4141c4787dda182f9e77f2f87b71213529efa7b", size = 283582, upload-time = "2026-10-07T12:22:44.983Z" }, + { url = "https://files.pythonhosted.org/packages/ac/03/70f2bcb2923a6db37818d917e124270a7f4cfd38ea576f5aa753a91c0ef5/tomli-2.5.0-cp313-cp313-win32.whl", hash = "sha256:abdbf6313b8d9efe157edeb7ab6eae4de064b1300ad31abf73755154b30abe68", size = 102628, upload-time = "2026-10-07T12:22:46.508Z" }, + { url = "https://files.pythonhosted.org/packages/dc/98/d52024bb5b0ff68b4f0d276d867f634c84a67319a7e9f6b7708a37742333/tomli-2.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd4dc129784e0c5335bd4e61dfcc4487499a013419e655cf2da1d091b7e0efdc", size = 113301, upload-time = "2026-10-07T12:22:47.647Z" }, + { url = "https://files.pythonhosted.org/packages/6f/f2/540db3a70572a8c23a28aba3e9c358ce0ffffbafc990905c1343aa265b31/tomli-2.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:69491c143d2fe063046e0301e62a810bed338fa4d1ce0fd870c27dc1e09b0d84", size = 109744, upload-time = "2026-10-07T12:22:48.925Z" }, + { url = "https://files.pythonhosted.org/packages/e4/49/caf6b307766eb9567664a8707e9d6be5fcc0e8903f18781c6677a60d80c7/tomli-2.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d3182ee2d887e507bd67319a0a61105d1dd33facc111329559a233b772c1a105", size = 162899, upload-time = "2026-10-07T12:22:50.088Z" }, + { url = "https://files.pythonhosted.org/packages/d3/c8/68cfce773a2733a49c74f99d627fb461bd990756860099eac25617889585/tomli-2.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:521345fd1f19d45b8df87657aaa38b6f2ca3800059fadf428e7ebf479a383646", size = 162080, upload-time = "2026-10-07T12:22:51.558Z" }, + { url = "https://files.pythonhosted.org/packages/7e/b2/e5bb8651fdad593f670501a7d718b1a7f73f064d44dea15e04c04dfef45d/tomli-2.5.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e95c7614e705bfe2b04b27aa124adec59752d15813df37e2156747cab3a006b", size = 273380, upload-time = "2026-10-07T12:22:52.918Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d2/9e2d7f8b1dfe0e2b34c245986ebd55c4c553ea4ce6c47c443b332673253f/tomli-2.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ac2027d37c3afbdf4bdd377f2676f6f1d2122a5be1f1137b49dced590b37e75", size = 283228, upload-time = "2026-10-07T12:22:54.173Z" }, + { url = "https://files.pythonhosted.org/packages/ba/df/ec7b876b7b1a2718bd74a3743c076fff565b04029ba33e8f61fac262739f/tomli-2.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c414be4ed9d3cac80c42e348fa5a956117d1a48227f48026e31f59cb4a7671eb", size = 277189, upload-time = "2026-10-07T12:22:55.342Z" }, + { url = "https://files.pythonhosted.org/packages/7d/7b/e192d9eed0b9cb80da799f4d77052297fb9a2c3cc9b19f571f56ea88add6/tomli-2.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9b03d7dc168353b4132965bde20feceabaa470e570c6f59660dfae59b1f9eeb3", size = 283632, upload-time = "2026-10-07T12:22:56.735Z" }, + { url = "https://files.pythonhosted.org/packages/84/50/ff94454e75461d75623e47401ed323d65c10aab8fe9033242c20cd2fdf32/tomli-2.5.0-cp314-cp314-win32.whl", hash = "sha256:6f041843c4d3a37245c0c056fd955b186bf8b1fb85690cbe40b81230891dc34b", size = 103535, upload-time = "2026-10-07T12:22:58.084Z" }, + { url = "https://files.pythonhosted.org/packages/54/0b/bdacf05f963bd6026ebf6eeb0beda847d1d60e03e440725c64a4e08a0afd/tomli-2.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:f4b653094e18f9031102d3a1da5c729c8f222d85225b18037dac621695e46e1a", size = 114621, upload-time = "2026-10-07T12:22:59.2Z" }, + { url = "https://files.pythonhosted.org/packages/61/99/53f438fa6ae4f9d4ed0ddde3e7242b3bdc34b48c8f9948b72b9e9b127676/tomli-2.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:3f89d10c1ff6a38d992c27fc8a4816af71a909e08a40ec66934240b1e74347c3", size = 111572, upload-time = "2026-10-07T12:23:00.479Z" }, + { url = "https://files.pythonhosted.org/packages/b9/20/1f88f19427d380a40e90a770e087489eaafe4aeee070ae88ed2bbec00acd/tomli-2.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:e9e15b4a6c7dd6b85b5fbab29488a73f1f70de516942308daa266bf0e0aeb0d4", size = 171814, upload-time = "2026-10-07T12:23:01.914Z" }, + { url = "https://files.pythonhosted.org/packages/d0/56/cbe5079c9f9a54b9b3e27fc82f08f3cb36edee75561679f53d2380c801d6/tomli-2.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e12bbcd32897272fb05929110362ae9ff4c1b9bb26bd9e971e71dcd3275b4c3d", size = 171324, upload-time = "2026-10-07T12:23:03.18Z" }, + { url = "https://files.pythonhosted.org/packages/2b/30/1d53fd3b0f1cb3ba542e345ec32c26aefdddc4e829e4f3429af8a4f27782/tomli-2.5.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20aa36de8f2cf87237143bc1fa1aae8d6612c09118f4da21c6a684db5dd1f6f9", size = 297441, upload-time = "2026-10-07T12:23:04.345Z" }, + { url = "https://files.pythonhosted.org/packages/66/d9/0800acb6a111686f764c1b91ef15cc42a20a66a46013bb42220f1d2c61c1/tomli-2.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22185fad8a1e622f064e78008018a0dd3323550dcb479cb7a1d296888d74024f", size = 307476, upload-time = "2026-10-07T12:23:05.671Z" }, + { url = "https://files.pythonhosted.org/packages/e8/63/30a8f3cd51b5bec37f04744bad0b0dc6160df84aad4f27b0e9283d66f221/tomli-2.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:984012f71908165449a951de2050d52f276bfe3aa5d5f570f63ddad814370374", size = 296113, upload-time = "2026-10-07T12:23:07.202Z" }, + { url = "https://files.pythonhosted.org/packages/ab/18/0b9ffc597e69c5a1e20a7823cb60d54b39a9f54e91edcb8574f022186758/tomli-2.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f79203b3965b4000e91808aaa7c040206093f2b8bf86f455982f2274c9ccf442", size = 307725, upload-time = "2026-10-07T12:23:08.508Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c7/18f8baae0b5607a60e8e19b4a7fedee43a8ff6458e3896dcbbadeeac9c22/tomli-2.5.0-cp314-cp314t-win32.whl", hash = "sha256:91294a9fb94a75542f6e46e4a2ae709bd8d9b51134098cae5cf3bea5478b6d03", size = 108546, upload-time = "2026-10-07T12:23:09.956Z" }, + { url = "https://files.pythonhosted.org/packages/72/34/4cca9739254130627bde87500b3f2b512154fe2f278efa7e2a5e10ad4bcb/tomli-2.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f15e3e0b835a6d68b10c86bf80a3149780498d6911c93c3ffd1861d19f9200f1", size = 117814, upload-time = "2026-10-07T12:23:11.486Z" }, + { url = "https://files.pythonhosted.org/packages/7d/fb/afa530d47dd80a78fce43beac6bc6e00f84558eafcffbc6f37b21e80d056/tomli-2.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6664b7ae7af7294256c53960a6103077f4914cec8ff98479c352f622c6f6b2f0", size = 115188, upload-time = "2026-10-07T12:23:12.728Z" }, + { url = "https://files.pythonhosted.org/packages/66/98/316fdc00f8c0939e6fe50461dd343c162d3ad51d1286eb25b7db54361d50/tomli-2.5.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a525685c2f97da40762b8695eb7aa0af4c8344ca1905c73e4e29cb04d34607dc", size = 162775, upload-time = "2026-10-07T12:23:13.941Z" }, + { url = "https://files.pythonhosted.org/packages/c5/22/7b10fa5bb01c9539f53f69b619361b19350acc73657772ea7ac70ba309a8/tomli-2.5.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:9dbb18c1cfb2f6517942fc9314437f66aa06d94436ffb1f06102ef3572f35276", size = 161406, upload-time = "2026-10-07T12:23:15.215Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e7/1a069d86dfd20f1f84f71c63faed9f83c1d890bc06c27d82dc7d888fb573/tomli-2.5.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:752e8b1aa6a4367ef8bf6a1a1e005540f7ed055ba36d7193796812ca5404eb52", size = 273855, upload-time = "2026-10-07T12:23:16.471Z" }, + { url = "https://files.pythonhosted.org/packages/ae/83/d1ef43d1687d092ab9c235455c76e6e709483b346b056f086095c7c263a5/tomli-2.5.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c47300f9bf791808f77d82747691c4bb09cb14bdf3060cca99b42cdc4361d5a7", size = 284910, upload-time = "2026-10-07T12:23:18.166Z" }, + { url = "https://files.pythonhosted.org/packages/cc/05/f4d9cf7de61822ece0c3873f30d291e324911c71a378b8bfe5ced13fd9f5/tomli-2.5.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:19b0dd8749f4ea2f112c5fcfb3c5248390c899d7e2e173f1d91abee1fa0ff391", size = 277723, upload-time = "2026-10-07T12:23:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/42/28/78262493141fa543151cf005760c3cb01d09fc28a11f993c05109902cb8c/tomli-2.5.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:57b1c3b01fab802e2899bc3d168dca320e14165e2fd9fd584760fb4ca5826859", size = 285115, upload-time = "2026-10-07T12:23:20.698Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b9/e1dab9a30bcb677b5cc5cee810609cfd64f24306a3055767dd3fda00b1e0/tomli-2.5.0-cp315-cp315-win32.whl", hash = "sha256:667e521b37a6c5ccaa044202c235b530f90177ffe2cd4a64ecc213c7dd535feb", size = 103475, upload-time = "2026-10-07T12:23:21.941Z" }, + { url = "https://files.pythonhosted.org/packages/4c/bd/31a3790c11d6ea95fcf5e6022ac0f8d0543c9b61120b730fc481bd43d3b4/tomli-2.5.0-cp315-cp315-win_amd64.whl", hash = "sha256:d747252933c8a65ef6bd8da0fbb7ce28a90eb6119d8cd00772cd528aa07b68d5", size = 114589, upload-time = "2026-10-07T12:23:23.098Z" }, + { url = "https://files.pythonhosted.org/packages/47/a2/4f6310fa699364f0e3af7ee3af88dddd9af066d33e716a0265bbe2b3ea84/tomli-2.5.0-cp315-cp315-win_arm64.whl", hash = "sha256:75dbcde8751b0a960aa3de173aa5e894d590755c6d7758b7e774c06f1dc3cbdd", size = 111493, upload-time = "2026-10-07T12:23:24.233Z" }, + { url = "https://files.pythonhosted.org/packages/68/14/00853f0b396d8971107ae1921bb5b322fdee1650d2f16bf06c20adb532e5/tomli-2.5.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:2419c2a189551987b59d80e63ec355671283336f41c6b9b89462df679c7d0c57", size = 171380, upload-time = "2026-10-07T12:23:25.512Z" }, + { url = "https://files.pythonhosted.org/packages/89/ad/fa6949321dadee46b27363974fb197b94c911c3b0f7a5fd26d7dc18fc2a0/tomli-2.5.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:0dc598040da8d42cf20f0be588ed7004f46db12a0ac6c32e03a59dccedaaadcd", size = 170553, upload-time = "2026-10-07T12:23:26.855Z" }, + { url = "https://files.pythonhosted.org/packages/53/aa/3056c919eb3e084df3752b2cf5f865dcc04af0b27dba2f66d7b28af4633a/tomli-2.5.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49096930c8d886c9bbdab62d2d0d17ce823ddeea522309a190b36245d5b49e01", size = 294428, upload-time = "2026-10-07T12:23:28.132Z" }, + { url = "https://files.pythonhosted.org/packages/96/b2/faeeb5d8769ea3832021d73e892c8391eae7b4b4f8b55a789127bd8b18a9/tomli-2.5.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8ade5023067f99fe72b88accd30d0ea05a158e9e32a11f124e731ea9695313f", size = 304909, upload-time = "2026-10-07T12:23:29.381Z" }, + { url = "https://files.pythonhosted.org/packages/f6/52/f094c09e73fb654b621716d019acb5d29bdfd1be01df80c281d552bda48d/tomli-2.5.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b69564772b5c8f22ea5f498dff08cfa825045b4d4c4400529000bdf818aa3b2a", size = 293220, upload-time = "2026-10-07T12:23:30.608Z" }, + { url = "https://files.pythonhosted.org/packages/86/f5/0c30541078ca4b505ce3bd76ed931facbfec524dd018535d691d1af0a6d2/tomli-2.5.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:8ff3a2ca028c7eee0c777f9a092038d0a594a9fa04e215f929a22c329e2cb142", size = 305705, upload-time = "2026-10-07T12:23:32.181Z" }, + { url = "https://files.pythonhosted.org/packages/05/74/590e7d19d6a118fc5cc5704ff358e21d95b8573f6b9443b1519f29ca8825/tomli-2.5.0-cp315-cp315t-win32.whl", hash = "sha256:62fc1bc8eb03e3a9cadfca713d65614ed8e09d974a283295ffe3a831976b4dc5", size = 108432, upload-time = "2026-10-07T12:23:33.496Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b8/63a75cfb27a17c38550e44025d3a6e7be64516fd8608a3b75703bf37d81b/tomli-2.5.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f3fcbc57b1791fa6cbe5d8434179d51de12be1a4811469529f47f6e7487a2571", size = 117281, upload-time = "2026-10-07T12:23:34.648Z" }, + { url = "https://files.pythonhosted.org/packages/72/01/e8c1debb2173973372934c68fc8e46170ab60ef23ed4592dff4dec6e8993/tomli-2.5.0-cp315-cp315t-win_arm64.whl", hash = "sha256:d2ba24db8a9376921b5e87b4762b9adb0f3f1deaea68f2b8b0bb2c11efb9c3e7", size = 115069, upload-time = "2026-10-07T12:23:35.77Z" }, + { url = "https://files.pythonhosted.org/packages/60/3f/3e3f8fd0919249b0200c80fbc4f9a1e70be19f9883da71dfb7f8b9ab8aca/tomli-2.5.0-py3-none-any.whl", hash = "sha256:32a7b79ac57a2e83670ce329ccf675798bc5a2094783a63676866b70503f2e2b", size = 14765, upload-time = "2026-10-07T12:23:36.875Z" }, +] + +[[package]] +name = "tomli-w" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, +] + [[package]] name = "tornado" version = "6.6" From c073104b34bb1e7f0739b90bfeeeac4b2a2ee90b Mon Sep 17 00:00:00 2001 From: ks6088ts Date: Mon, 9 Nov 2026 08:13:32 +0900 Subject: [PATCH 2/7] feat(cli): add --version, config command, and JSON logging Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/template_python/__init__.py | 9 +++- src/template_python/__main__.py | 3 +- src/template_python/cli.py | 35 ++++++++++++-- src/template_python/log.py | 41 ++++++++++++++++ src/template_python/logging.py | 14 ------ src/template_python/settings.py | 8 +++- tests/conftest.py | 21 +++++++++ tests/test_cli.py | 84 +++++++++++++++++++++++++++++++++ tests/test_log.py | 46 ++++++++++++++++++ tests/test_logging.py | 19 -------- tests/test_settings.py | 34 ++++++++++++- 11 files changed, 272 insertions(+), 42 deletions(-) create mode 100644 src/template_python/log.py delete mode 100644 src/template_python/logging.py create mode 100644 tests/conftest.py create mode 100644 tests/test_log.py delete mode 100644 tests/test_logging.py diff --git a/src/template_python/__init__.py b/src/template_python/__init__.py index 8e0f674..c7ce31b 100644 --- a/src/template_python/__init__.py +++ b/src/template_python/__init__.py @@ -1,5 +1,12 @@ """Reusable building blocks for the template project.""" +from importlib.metadata import PackageNotFoundError, version + from template_python.core import greeting -__all__ = ["greeting"] +try: + __version__ = version("template-python") +except PackageNotFoundError: # pragma: no cover - only for an uninstalled source tree + __version__ = "0.0.0+unknown" + +__all__ = ["__version__", "greeting"] diff --git a/src/template_python/__main__.py b/src/template_python/__main__.py index 5d5bc5d..6dc25e0 100644 --- a/src/template_python/__main__.py +++ b/src/template_python/__main__.py @@ -2,4 +2,5 @@ from template_python.cli import app -app() +if __name__ == "__main__": + app(prog_name="template-python") diff --git a/src/template_python/cli.py b/src/template_python/cli.py index 2aaafe2..f4ea853 100644 --- a/src/template_python/cli.py +++ b/src/template_python/cli.py @@ -6,34 +6,54 @@ import typer from pydantic import ValidationError +from template_python import __version__ from template_python.core import greeting -from template_python.logging import configure_logging -from template_python.settings import get_project_settings +from template_python.log import configure_logging +from template_python.settings import ProjectSettings, get_project_settings app = typer.Typer( add_completion=False, help="A minimal, typed Python project template.", no_args_is_help=True, + pretty_exceptions_show_locals=False, ) logger = logging.getLogger(__name__) +def _print_version(value: bool) -> None: + if value: + typer.echo(f"template-python {__version__}") + raise typer.Exit + + @app.callback() def main( + ctx: typer.Context, verbose: Annotated[ bool, typer.Option("--verbose", "-v", help="Enable debug logging."), ] = False, + _version: Annotated[ + bool, + typer.Option( + "--version", + "-V", + callback=_print_version, + is_eager=True, + help="Show the version and exit.", + ), + ] = False, ) -> None: - """Configure the application before running a command.""" + """Load settings and configure logging before running a command.""" try: settings = get_project_settings() except ValidationError as exc: typer.echo(f"Invalid configuration: {exc}", err=True) raise typer.Exit(code=2) from exc - configure_logging("DEBUG" if verbose else settings.log_level) + configure_logging("DEBUG" if verbose else settings.log_level, settings.log_format) logger.debug("Loaded settings for %s", settings.name) + ctx.obj = settings @app.command() @@ -49,3 +69,10 @@ def hello( except ValueError as exc: raise typer.BadParameter(str(exc), param_hint="--name") from exc typer.echo(message) + + +@app.command("config") +def show_config(ctx: typer.Context) -> None: + """Print the effective configuration as JSON; secret values are masked.""" + settings = ctx.ensure_object(ProjectSettings) + typer.echo(settings.model_dump_json(indent=2)) diff --git a/src/template_python/log.py b/src/template_python/log.py new file mode 100644 index 0000000..7be6524 --- /dev/null +++ b/src/template_python/log.py @@ -0,0 +1,41 @@ +"""Process logging configuration for the application boundary.""" + +import json +import logging +import sys +from datetime import UTC, datetime +from typing import override + +TEXT_FORMAT = "%(asctime)s [%(levelname)s] %(name)s: %(message)s" +LOG_FORMATS = frozenset({"text", "json"}) + + +class JsonFormatter(logging.Formatter): + """Render each record as a single-line JSON object for log collectors.""" + + @override + def format(self, record: logging.LogRecord) -> str: + payload: dict[str, str] = { + "timestamp": datetime.fromtimestamp(record.created, tz=UTC).isoformat(), + "level": record.levelname, + "logger": record.name, + "message": record.getMessage(), + } + if record.exc_info: + payload["exception"] = self.formatException(record.exc_info) + return json.dumps(payload, ensure_ascii=False) + + +def configure_logging(level: str, log_format: str = "text") -> None: + """Configure root logging on stderr; call once at the application boundary.""" + numeric_level = logging.getLevelNamesMapping().get(level.upper()) + if numeric_level is None: + msg = f"unsupported log level: {level}" + raise ValueError(msg) + if log_format not in LOG_FORMATS: + msg = f"unsupported log format: {log_format}" + raise ValueError(msg) + + handler = logging.StreamHandler(sys.stderr) + handler.setFormatter(JsonFormatter() if log_format == "json" else logging.Formatter(TEXT_FORMAT)) + logging.basicConfig(level=numeric_level, handlers=[handler], force=True) diff --git a/src/template_python/logging.py b/src/template_python/logging.py deleted file mode 100644 index 870f87b..0000000 --- a/src/template_python/logging.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Application logging configuration.""" - -import logging - -LOG_FORMAT = "%(asctime)s [%(levelname)s] %(name)s: %(message)s" - - -def configure_logging(level: str) -> None: - """Configure process logging at the application boundary.""" - numeric_level = logging.getLevelNamesMapping().get(level.upper()) - if numeric_level is None: - msg = f"unsupported log level: {level}" - raise ValueError(msg) - logging.basicConfig(level=numeric_level, format=LOG_FORMAT, force=True) diff --git a/src/template_python/settings.py b/src/template_python/settings.py index 1084bde..d77e142 100644 --- a/src/template_python/settings.py +++ b/src/template_python/settings.py @@ -5,13 +5,19 @@ from pydantic_settings import BaseSettings, SettingsConfigDict LogLevel = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] +LogFormat = Literal["text", "json"] class ProjectSettings(BaseSettings): - """Settings loaded from ``PROJECT_*`` environment variables.""" + """Settings loaded from ``PROJECT_*`` environment variables and an optional ``.env`` file. + + Declare credentials as ``pydantic.SecretStr`` so they are masked in ``repr`` and in + the ``config`` command output. + """ name: str = "template-python" log_level: LogLevel = "INFO" + log_format: LogFormat = "text" model_config = SettingsConfigDict( env_file=".env", diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..a1dd759 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,21 @@ +import logging +import os +from collections.abc import Iterator +from pathlib import Path + +import pytest + + +@pytest.fixture(autouse=True) +def isolated_environment(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Iterator[None]: + """Keep tests independent of the developer's shell, ``.env`` file, and logging state.""" + for key in os.environ: + if key.upper().startswith("PROJECT_"): + monkeypatch.delenv(key) + monkeypatch.chdir(tmp_path) + + root = logging.getLogger() + handlers, level = root.handlers[:], root.level + yield + root.handlers[:] = handlers + root.setLevel(level) diff --git a/tests/test_cli.py b/tests/test_cli.py index 38e2faa..3ca717b 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,7 +1,15 @@ +import json +import runpy +import sys +from pathlib import Path + import pytest +from pydantic import SecretStr from typer.testing import CliRunner +from template_python import __version__, cli from template_python.cli import app +from template_python.settings import ProjectSettings runner = CliRunner() @@ -13,6 +21,29 @@ def test_cli_help() -> None: assert "A minimal, typed Python project template." in result.stdout +def test_cli_without_command_shows_help() -> None: + result = runner.invoke(app, []) + + assert result.exit_code == 2 + assert "Usage:" in result.stdout + + +@pytest.mark.parametrize("flag", ["--version", "-V"]) +def test_cli_version(flag: str) -> None: + result = runner.invoke(app, [flag]) + + assert result.exit_code == 0 + assert result.stdout == f"template-python {__version__}\n" + + +def test_cli_version_ignores_invalid_settings(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("PROJECT_LOG_LEVEL", "VERBOSE") + + result = runner.invoke(app, ["--version"]) + + assert result.exit_code == 0 + + def test_cli_greets_name() -> None: result = runner.invoke(app, ["hello", "--name", "Ada"]) @@ -25,6 +56,18 @@ def test_cli_verbose_logging() -> None: assert result.exit_code == 0 assert "Loaded settings for template-python" in result.stderr + assert result.stdout == "Hello, World!\n" + + +def test_cli_json_logging(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("PROJECT_LOG_FORMAT", "json") + + result = runner.invoke(app, ["--verbose", "hello"]) + + assert result.exit_code == 0 + record = json.loads(result.stderr.splitlines()[0]) + assert record["level"] == "DEBUG" + assert record["message"] == "Loaded settings for template-python" def test_cli_rejects_blank_name() -> None: @@ -41,3 +84,44 @@ def test_cli_reports_invalid_settings(monkeypatch: pytest.MonkeyPatch) -> None: assert result.exit_code == 2 assert "Invalid configuration" in result.stderr + + +def test_cli_reads_dotenv_from_working_directory(tmp_path: Path) -> None: + (tmp_path / ".env").write_text("PROJECT_NAME=from-dotenv\n", encoding="utf-8") + + result = runner.invoke(app, ["config"]) + + assert result.exit_code == 0 + assert json.loads(result.stdout)["name"] == "from-dotenv" + + +def test_cli_config_shows_effective_settings(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("PROJECT_LOG_FORMAT", "json") + + result = runner.invoke(app, ["config"]) + + assert result.exit_code == 0 + assert json.loads(result.stdout) == {"name": "template-python", "log_level": "INFO", "log_format": "json"} + + +def test_cli_config_masks_secrets(monkeypatch: pytest.MonkeyPatch) -> None: + class SettingsWithSecret(ProjectSettings): + api_key: SecretStr = SecretStr("do-not-print") + + monkeypatch.setattr(cli, "get_project_settings", SettingsWithSecret) + + result = runner.invoke(app, ["config"]) + + assert result.exit_code == 0 + assert "do-not-print" not in result.stdout + assert json.loads(result.stdout)["api_key"] == "**********" + + +def test_module_entry_point(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None: + monkeypatch.setattr(sys, "argv", ["template_python", "hello", "--name", "Module"]) + + with pytest.raises(SystemExit) as exc_info: + runpy.run_module("template_python", run_name="__main__") + + assert exc_info.value.code == 0 + assert capsys.readouterr().out == "Hello, Module!\n" diff --git a/tests/test_log.py b/tests/test_log.py new file mode 100644 index 0000000..ff43f1a --- /dev/null +++ b/tests/test_log.py @@ -0,0 +1,46 @@ +import json +import logging + +import pytest + +from template_python.log import configure_logging + + +def test_configure_logging_emits_text_once(capsys: pytest.CaptureFixture[str]) -> None: + configure_logging("INFO") + + logging.getLogger("template.test").info("ready") + + captured = capsys.readouterr() + assert captured.err.count("ready") == 1 + assert "[INFO] template.test: ready" in captured.err + + +def test_configure_logging_emits_json(capsys: pytest.CaptureFixture[str]) -> None: + configure_logging("warning", "json") + + logger = logging.getLogger("template.test") + logger.info("hidden") + try: + raise RuntimeError("boom") # noqa: TRY301 + except RuntimeError: + logger.exception("failed") + + lines = capsys.readouterr().err.splitlines() + assert len(lines) == 1 + record = json.loads(lines[0]) + assert record["level"] == "ERROR" + assert record["logger"] == "template.test" + assert record["message"] == "failed" + assert "RuntimeError: boom" in record["exception"] + assert record["timestamp"].endswith("+00:00") + + +def test_configure_logging_rejects_unknown_level() -> None: + with pytest.raises(ValueError, match="unsupported log level"): + configure_logging("TRACE") + + +def test_configure_logging_rejects_unknown_format() -> None: + with pytest.raises(ValueError, match="unsupported log format"): + configure_logging("INFO", "xml") diff --git a/tests/test_logging.py b/tests/test_logging.py deleted file mode 100644 index bc06d1e..0000000 --- a/tests/test_logging.py +++ /dev/null @@ -1,19 +0,0 @@ -import logging - -import pytest - -from template_python.logging import configure_logging - - -def test_configure_logging_emits_once(capsys: pytest.CaptureFixture[str]) -> None: - configure_logging("INFO") - - logging.getLogger("template.test").info("ready") - - captured = capsys.readouterr() - assert captured.err.count("ready") == 1 - - -def test_configure_logging_rejects_unknown_level() -> None: - with pytest.raises(ValueError, match="unsupported log level"): - configure_logging("TRACE") diff --git a/tests/test_settings.py b/tests/test_settings.py index 22f07f0..6656065 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -1,21 +1,51 @@ +from pathlib import Path + import pytest from pydantic import ValidationError from template_python.settings import ProjectSettings +def test_settings_defaults() -> None: + settings = ProjectSettings(_env_file=None) + + assert settings.name == "template-python" + assert settings.log_level == "INFO" + assert settings.log_format == "text" + + def test_settings_use_prefixed_environment(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("PROJECT_NAME", "example") monkeypatch.setenv("PROJECT_LOG_LEVEL", "DEBUG") + monkeypatch.setenv("PROJECT_LOG_FORMAT", "json") settings = ProjectSettings(_env_file=None) assert settings.name == "example" assert settings.log_level == "DEBUG" + assert settings.log_format == "json" + + +def test_environment_overrides_dotenv(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + (tmp_path / ".env").write_text("PROJECT_NAME=from-file\nPROJECT_LOG_LEVEL=ERROR\n", encoding="utf-8") + monkeypatch.setenv("PROJECT_NAME", "from-env") + + settings = ProjectSettings() + + assert settings.name == "from-env" + assert settings.log_level == "ERROR" + + +def test_settings_reject_unknown_dotenv_entry(tmp_path: Path) -> None: + (tmp_path / ".env").write_text("PROJECT_UNKNOWN=1\n", encoding="utf-8") + + with pytest.raises(ValidationError): + ProjectSettings() -def test_settings_reject_invalid_log_level(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setenv("PROJECT_LOG_LEVEL", "VERBOSE") +@pytest.mark.parametrize(("variable", "value"), [("PROJECT_LOG_LEVEL", "VERBOSE"), ("PROJECT_LOG_FORMAT", "xml")]) +def test_settings_reject_invalid_values(monkeypatch: pytest.MonkeyPatch, variable: str, value: str) -> None: + monkeypatch.setenv(variable, value) with pytest.raises(ValidationError): ProjectSettings(_env_file=None) From d0b2b908f90cd9a05abd87d244c90a548c6eaa63 Mon Sep 17 00:00:00 2001 From: ks6088ts Date: Mon, 9 Nov 2026 08:13:32 +0900 Subject: [PATCH 3/7] build: run hooks from uv.lock and add audit, secret, and file scans Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .pre-commit-config.yaml | 50 +++++++++++++++++++--- Makefile | 93 ++++++++++++++++++++++++++++++++--------- 2 files changed, 119 insertions(+), 24 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e7968c7..eedad3a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,16 +1,56 @@ +# Python tools run from uv.lock so hooks, Make targets, and CI use identical versions. +# Repository hooks below are updated by Dependabot (pre-commit ecosystem). repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: + - id: check-added-large-files + - id: check-case-conflict + - id: check-merge-conflict + - id: check-toml - id: check-yaml + - id: detect-private-key - id: end-of-file-fixer + - id: mixed-line-ending + args: [--fix=lf] - id: trailing-whitespace - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.17.2 + - repo: https://github.com/gitleaks/gitleaks + rev: v8.30.1 hooks: + - id: gitleaks + - repo: local + hooks: + - id: uv-lock + name: uv lock --check + entry: uv lock --check + language: system + files: ^(pyproject\.toml|uv\.lock)$ + pass_filenames: false - id: ruff-check - types_or: [python, pyi] - args: [--fix] + name: ruff check + entry: uv run --locked ruff check --fix --force-exclude + language: system + types_or: [python, pyi, jupyter] - id: ruff-format + name: ruff format + entry: uv run --locked ruff format --force-exclude + language: system + types_or: [python, pyi, jupyter] + - id: mypy + name: mypy + entry: uv run --locked mypy + language: system types_or: [python, pyi] -exclude: "generated/.*|artifacts/.*" + pass_filenames: false + - id: actionlint + name: actionlint + entry: uv run --locked actionlint + language: system + files: ^\.github/workflows/.+\.ya?ml$ + - id: zizmor + name: zizmor + entry: uv run --locked zizmor --offline --min-severity low . + language: system + files: ^\.github/ + pass_filenames: false +exclude: ^(site|dist|build)/ diff --git a/Makefile b/Makefile index 5844f40..fb43f69 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,20 @@ +SHELL := bash +.SHELLFLAGS := -eu -o pipefail -c + # Git GIT_REVISION ?= $(shell git rev-parse --short HEAD) -GIT_TAG ?= $(shell git describe --tags --abbrev=0 --always | sed -e s/v//g) +GIT_TAG ?= $(shell git describe --tags --abbrev=0 --always | sed -e 's/^v//') + +# Container images used as tools. Pin by digest; refresh with +# `docker buildx imagetools inspect :`. +HADOLINT_IMAGE ?= hadolint/hadolint:v2.15.1@sha256:32dac94127fd60b7b7e3fbfc65e1383b9b5e25c9bfd7b8536de7a539fe68a12d +TRIVY_IMAGE ?= aquasec/trivy:0.76.1@sha256:790fd66d1697395d8693d1acaa1ea702406a20f109b6fc7bac8db623eca6c06c +GITLEAKS_IMAGE ?= zricethezav/gitleaks:v8.30.1@sha256:c00b6bd0aeb3071cbcb79009cb16a60dd9e0a7c60e2be9ab65d25e6bc8abbb7f +TRIVY_CACHE_VOLUME ?= template-python-trivy-cache + +# Lowest supported Python, used with the lowest declared direct dependencies. +LOWEST_PYTHON ?= 3.12 +LOWEST_VENV ?= build/venv-lowest .PHONY: help help: @@ -11,11 +25,15 @@ help: info: ## show information @echo "GIT_REVISION: $(GIT_REVISION)" @echo "GIT_TAG: $(GIT_TAG)" + @echo "PACKAGE_VERSION: $$(uv version --short)" + +# --- +# Python +# --- .PHONY: install-deps-dev install-deps-dev: ## install dependencies for development uv sync --locked --group dev - @which actionlint || echo "install actionlint https://github.com/rhysd/actionlint" .PHONY: install-hooks install-hooks: ## install Git hooks @@ -39,23 +57,29 @@ format: ## format code .PHONY: fix fix: ## apply auto-fixes - uv run --locked ruff check --fix + uv run --locked ruff check --fix . uv run --locked ruff format . .PHONY: type-check type-check: ## check types with mypy - uv run --locked mypy --config-file pyproject.toml + uv run --locked mypy .PHONY: lint -lint: type-check ## lint +lint: type-check ## lint Python code and GitHub workflows uv run --locked ruff check . - actionlint - uv run --locked zizmor --offline --strict-collection --min-severity high . + uv run --locked actionlint + uv run --locked zizmor --offline --min-severity low . .PHONY: test -test: ## run tests +test: ## run tests with coverage uv run --locked pytest +.PHONY: test-lowest +test-lowest: ## run tests with the lowest supported Python and direct dependency versions + uv venv --quiet --clear --python $(LOWEST_PYTHON) $(LOWEST_VENV) + uv pip install --quiet --python $(LOWEST_VENV) --resolution lowest-direct --group test . + $(LOWEST_VENV)/bin/python -m pytest -p no:cacheprovider + .PHONY: build build: ## build wheel and source distribution uv build --clear @@ -65,23 +89,58 @@ package-check: build ## test the built wheel and installed CLI @wheel=$$(find dist -maxdepth 1 -name '*.whl' -type f -print -quit); \ test -n "$$wheel"; \ uv run --isolated --no-project --with "$$wheel" template-python hello --name Package | grep -Fx "Hello, Package!"; \ - uv run --isolated --no-project --with "$$wheel" python -m template_python hello --name Module | grep -Fx "Hello, Module!" + uv run --isolated --no-project --with "$$wheel" python -m template_python hello --name Module | grep -Fx "Hello, Module!"; \ + uv run --isolated --no-project --with "$$wheel" template-python --version | grep -Fx "template-python $$(uv version --short)" .PHONY: hooks-check -hooks-check: ## check all configured hooks +hooks-check: ## run all configured Git hooks against all files uv run --locked prek run --all-files .PHONY: ci-test ci-test: install-deps-ci format-check lint test package-check ## run CI tests .PHONY: update -update: ## update packages +update: ## upgrade all locked packages uv lock --upgrade .PHONY: jupyterlab jupyterlab: ## run Jupyter Lab uv run --locked --no-dev --group notebook jupyter lab +# --- +# Security +# --- + +.PHONY: audit +audit: ## audit locked dependencies for known vulnerabilities + @requirements=$$(mktemp); trap 'rm -f "$$requirements"' EXIT; \ + uv export --quiet --locked --all-groups --no-emit-project --format requirements-txt --output-file "$$requirements"; \ + uv run --locked pip-audit --requirement "$$requirements" --require-hashes --disable-pip --strict --progress-spinner off + +.PHONY: secret-scan +secret-scan: ## scan the Git history for secrets (requires Docker) + docker run --rm \ + -v "$(CURDIR):/repo:ro" \ + -e GIT_CONFIG_COUNT=1 -e GIT_CONFIG_KEY_0=safe.directory -e GIT_CONFIG_VALUE_0='*' \ + $(GITLEAKS_IMAGE) git /repo --redact --no-banner --verbose + +.PHONY: security-scan +security-scan: ## scan files for vulnerabilities, misconfigurations, and secrets (requires Docker) + docker run --rm \ + -v "$(CURDIR):/src:ro" \ + -v $(TRIVY_CACHE_VOLUME):/root/.cache/trivy \ + $(TRIVY_IMAGE) fs \ + --skip-version-check \ + --scanners vuln,misconfig,secret \ + --skip-dirs /src/.venv,/src/build,/src/dist,/src/site \ + --exit-code 1 \ + --ignore-unfixed \ + --severity HIGH,CRITICAL \ + /src + +.PHONY: ci-test-security +ci-test-security: install-deps-ci audit secret-scan security-scan ## run CI security scans + # --- # Docker # --- @@ -89,11 +148,6 @@ DOCKER_REPO_NAME ?= ks6088ts DOCKER_IMAGE_NAME ?= template-python DOCKER_COMMAND ?= hello --name Container -# Tools -HADOLINT_VERSION ?= v2.15.1 -TRIVY_VERSION ?= 0.74.0 -TRIVY_CACHE_VOLUME ?= template-python-trivy-cache - .PHONY: docker-build docker-build: ## build Docker image docker build \ @@ -109,14 +163,15 @@ docker-run: ## run Docker container .PHONY: docker-lint docker-lint: ## lint Dockerfile - docker run --rm -i hadolint/hadolint:$(HADOLINT_VERSION) < Dockerfile + docker run --rm -i $(HADOLINT_IMAGE) < Dockerfile .PHONY: docker-scan docker-scan: ## scan Docker image docker run --rm \ -v /var/run/docker.sock:/var/run/docker.sock \ -v $(TRIVY_CACHE_VOLUME):/root/.cache/trivy \ - aquasec/trivy:$(TRIVY_VERSION) image \ + $(TRIVY_IMAGE) image \ + --skip-version-check \ --exit-code 1 \ --ignore-unfixed \ --severity HIGH,CRITICAL \ @@ -135,7 +190,7 @@ install-deps-docs: ## install dependencies for documentation .PHONY: docs docs: ## build documentation - uv run --locked --no-dev --group docs mkdocs build + uv run --locked --no-dev --group docs mkdocs build --strict .PHONY: docs-serve docs-serve: ## serve documentation From 6004e999b303af59f168d3d163ac9e75320a171a Mon Sep 17 00:00:00 2001 From: ks6088ts Date: Mon, 9 Nov 2026 08:13:32 +0900 Subject: [PATCH 4/7] build(docker): build a hash-verified, digest-pinned uv image Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .dockerignore | 28 ++++++++---------------- Dockerfile | 59 +++++++++++++++++++++++++++++++++------------------ compose.yml | 7 ++++++ 3 files changed, 54 insertions(+), 40 deletions(-) diff --git a/.dockerignore b/.dockerignore index e2d4424..3fa44b9 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,19 +1,9 @@ -.git -.github -.venv -.coverage -.pytest_cache -.ruff_cache -.mypy_cache -dist -site -notebooks -tests -docs -.devcontainer -.vscode -*.egg-info -__pycache__ -*.py[cod] -.env -.env.* +# Allowlist: only files needed to build the image enter the build context. +* +!pyproject.toml +!uv.lock +!README.md +!LICENSE +!src/ +**/__pycache__ +**/*.py[cod] diff --git a/Dockerfile b/Dockerfile index 82c20e6..704e75d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,37 +1,54 @@ -FROM python:3.13-slim-trixie AS build +# syntax=docker/dockerfile:1 -WORKDIR /build +# Build a virtual environment from uv.lock; hashes in the lock file are verified. +FROM python:3.14-slim-trixie@sha256:9aaa474097d094040649f2162a627c0049b408f52138d980916788d80f04802f AS build + +COPY --from=astral/uv:0.13.7@sha256:96f40d3904aad8d0bd010539001d36c8e2188b981e1334471a90c4a1d93bb490 /uv /usr/local/bin/uv + +ENV UV_COMPILE_BYTECODE=1 \ + UV_LINK_MODE=copy \ + UV_PYTHON_DOWNLOADS=never \ + UV_PROJECT_ENVIRONMENT=/app/.venv + +WORKDIR /src + +# Install dependencies first so this layer is reused until the lock file changes. +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=uv.lock,target=uv.lock \ + --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ + uv sync --locked --no-dev --no-install-project -COPY --from=astral/uv:0.12.19 /uv /usr/local/bin/uv COPY pyproject.toml uv.lock README.md LICENSE ./ COPY src ./src +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync --locked --no-dev --no-editable -RUN uv export \ - --locked \ - --no-dev \ - --no-emit-project \ - --no-hashes \ - --output-file /dist/requirements.txt \ - && uv build --wheel --out-dir /dist - -FROM python:3.13-slim-trixie +FROM python:3.14-slim-trixie@sha256:9aaa474097d094040649f2162a627c0049b408f52138d980916788d80f04802f ARG GIT_REVISION="0000000" ARG GIT_TAG="0.0.0" -LABEL org.opencontainers.image.source="https://github.com/ks6088ts/template-python" \ +LABEL org.opencontainers.image.title="template-python" \ + org.opencontainers.image.description="A minimal, typed Python CLI template" \ + org.opencontainers.image.licenses="MIT" \ + org.opencontainers.image.source="https://github.com/ks6088ts/template-python" \ org.opencontainers.image.revision="${GIT_REVISION}" \ org.opencontainers.image.version="${GIT_TAG}" -RUN groupadd --system --gid 10001 app \ - && useradd --system --uid 10001 --gid 10001 --home-dir /app app +# Apply Debian security fixes published after the base image was built, and remove pip +# (and its vendored libraries) because the runtime never installs packages. +RUN apt-get update \ + && apt-get upgrade --yes --no-install-recommends \ + && rm -rf /var/lib/apt/lists/* \ + && python -m pip uninstall --yes --root-user-action=ignore pip \ + && groupadd --system --gid 10001 app \ + && useradd --system --uid 10001 --gid 10001 --no-create-home --home-dir /app app + +COPY --from=build --chown=root:root /app/.venv /app/.venv -COPY --from=build /dist/requirements.txt /tmp/requirements.txt -COPY --from=build /dist/*.whl /tmp/ -RUN pip install --no-cache-dir --requirement /tmp/requirements.txt \ - && pip install --no-cache-dir --no-deps /tmp/*.whl \ - && pip uninstall --yes pip \ - && rm /tmp/*.whl /tmp/requirements.txt +ENV PATH="/app/.venv/bin:${PATH}" \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 USER 10001:10001 WORKDIR /app diff --git a/compose.yml b/compose.yml index fa15063..fcba76b 100644 --- a/compose.yml +++ b/compose.yml @@ -3,7 +3,14 @@ services: build: context: . dockerfile: Dockerfile + image: template-python:local command: ["hello", "--name", "Compose"] environment: PROJECT_LOG_LEVEL: INFO + PROJECT_LOG_FORMAT: json PROJECT_NAME: template-python-compose + read_only: true + security_opt: + - no-new-privileges:true + cap_drop: + - ALL From 20ac47d9fa14ad4a7be8321ac877c92feb8bc3c6 Mon Sep 17 00:00:00 2001 From: ks6088ts Date: Mon, 9 Nov 2026 08:13:32 +0900 Subject: [PATCH 5/7] ci: add security, CodeQL, Scorecard, and signed GHCR release workflows Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/dependabot.yaml | 32 +++++++- .github/workflows/azure-static-web-app.yaml | 21 +++-- .github/workflows/codeql.yaml | 45 +++++++++++ .github/workflows/docker-release.yaml | 48 ----------- .github/workflows/docker.yaml | 11 +-- .github/workflows/ghcr-release.yaml | 52 ------------ .github/workflows/github-pages.yaml | 17 ++-- .github/workflows/release.yaml | 90 +++++++++++++++++++++ .github/workflows/scorecard.yaml | 39 +++++++++ .github/workflows/security.yaml | 33 ++++++++ .github/workflows/test.yaml | 79 +++++++++--------- 11 files changed, 300 insertions(+), 167 deletions(-) create mode 100644 .github/workflows/codeql.yaml delete mode 100644 .github/workflows/docker-release.yaml delete mode 100644 .github/workflows/ghcr-release.yaml create mode 100644 .github/workflows/release.yaml create mode 100644 .github/workflows/scorecard.yaml create mode 100644 .github/workflows/security.yaml diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 8216109..3f4ebda 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -1,22 +1,48 @@ -# https://docs.github.com/en/code-security/dependabot/ecosystems-supported-by-dependabot/supported-ecosystems-and-repositories#supported-ecosystems-and-repositories +# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference +# A cooldown delays adoption of freshly published releases to reduce supply-chain risk. version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" + cooldown: + default-days: 7 + groups: + github-actions: + patterns: ["*"] - package-ecosystem: "uv" directory: "/" schedule: interval: "weekly" - # https://docs.github.com/ja/code-security/dependabot/dependabot-version-updates/optimizing-pr-creation-version-updates#setting-up-a-cooldown-period-for-dependency-updates cooldown: - default-days: 5 + default-days: 7 + semver-major-days: 14 + groups: + python-dev: + dependency-type: "development" + update-types: ["minor", "patch"] + python-runtime: + dependency-type: "production" + update-types: ["minor", "patch"] - package-ecosystem: "pre-commit" directory: "/" schedule: interval: "weekly" + cooldown: + default-days: 7 + groups: + pre-commit: + patterns: ["*"] - package-ecosystem: "docker" directory: "/" schedule: interval: "weekly" + cooldown: + default-days: 7 + - package-ecosystem: "devcontainers" + directory: "/" + schedule: + interval: "weekly" + cooldown: + default-days: 7 diff --git a/.github/workflows/azure-static-web-app.yaml b/.github/workflows/azure-static-web-app.yaml index 9869cc9..12a3421 100644 --- a/.github/workflows/azure-static-web-app.yaml +++ b/.github/workflows/azure-static-web-app.yaml @@ -10,24 +10,21 @@ permissions: jobs: deploy: runs-on: ubuntu-latest - timeout-minutes: 5 + timeout-minutes: 10 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - name: Set up uv with caching enabled - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 + # No cache in publishing workflows to avoid cache poisoning. + - name: Set up uv + uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 with: - enable-cache: true - version: "0.12.19" - python-version: "3.13" - - name: Generate documentation - shell: bash - run: | - make install-deps-docs - make docs + enable-cache: false + - name: Build documentation + run: make ci-test-docs + # Upstream has both a v1 tag (pinned here) and a v1 branch, which zizmor reports as ambiguous. - name: Deploy - uses: Azure/static-web-apps-deploy@1a947af9992250f3bc2e68ad0754c0b0c11566c9 # v1 + uses: Azure/static-web-apps-deploy@1a947af9992250f3bc2e68ad0754c0b0c11566c9 # v1 # zizmor: ignore[ref-version-mismatch] with: azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }} action: "upload" diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml new file mode 100644 index 0000000..150f9d9 --- /dev/null +++ b/.github/workflows/codeql.yaml @@ -0,0 +1,45 @@ +name: codeql + +on: + push: + branches: + - "main" + pull_request: + branches: + - "main" + schedule: + - cron: "23 4 * * 1" +permissions: + contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +jobs: + analyze: + name: analyze (${{ matrix.language }}) + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + actions: read + contents: read + security-events: write + strategy: + fail-fast: false + matrix: + language: + - actions + - python + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Initialize CodeQL + uses: github/codeql-action/init@33d7b85da1a04171e211befe2c739303b9a4c066 # v4.39.3 + with: + languages: ${{ matrix.language }} + build-mode: none + queries: security-extended + - name: Perform CodeQL analysis + uses: github/codeql-action/analyze@33d7b85da1a04171e211befe2c739303b9a4c066 # v4.39.3 + with: + category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/docker-release.yaml b/.github/workflows/docker-release.yaml deleted file mode 100644 index a1f5590..0000000 --- a/.github/workflows/docker-release.yaml +++ /dev/null @@ -1,48 +0,0 @@ -name: docker-release -on: - push: - tags: - - "v*" -permissions: - contents: read -jobs: - release: - runs-on: "ubuntu-latest" - timeout-minutes: 10 - steps: - - name: Check out the repo - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 - - name: Log in to Docker Hub - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - # https://github.com/docker/build-push-action/issues/42#issuecomment-915323168 - - name: Set Versions - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - id: set_version - with: - script: | - const tag = context.ref.substring(10) - const no_v = tag.replace('v', '') - const dash_index = no_v.lastIndexOf('-') - const no_dash = (dash_index > -1) ? no_v.substring(0, dash_index) : no_v - core.setOutput('tag', tag) - core.setOutput('no-v', no_v) - core.setOutput('no-dash', no_dash) - - name: Build and push tag ${{steps.set_version.outputs.no-dash}} - uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 - with: - context: . - platforms: linux/amd64,linux/arm64 - push: true - build-args: | - GIT_REVISION=${{ github.sha }} - GIT_TAG=${{steps.set_version.outputs.no-dash}} - tags: | - ${{ secrets.DOCKERHUB_USERNAME }}/template-python:${{steps.set_version.outputs.no-dash}} - ${{ secrets.DOCKERHUB_USERNAME }}/template-python:latest diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 17ee4be..e5337b2 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -10,16 +10,17 @@ on: - "main" permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: docker: runs-on: "ubuntu-latest" - timeout-minutes: 5 + timeout-minutes: 10 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 # to retrieve git tags persist-credentials: false - - name: Run CI tests for Docker - shell: bash - run: | - make ci-test-docker + - name: Lint, build, scan, and run the image + run: make ci-test-docker diff --git a/.github/workflows/ghcr-release.yaml b/.github/workflows/ghcr-release.yaml deleted file mode 100644 index 5ec4c77..0000000 --- a/.github/workflows/ghcr-release.yaml +++ /dev/null @@ -1,52 +0,0 @@ -name: ghcr -on: - push: - tags: - - "v*" -permissions: - packages: write - contents: read -env: - IMAGE_NAME: template-python -jobs: - ghcr: - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 - - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Set Versions - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - id: set_version - with: - script: | - const tag = context.ref.substring(10) - const no_v = tag.replace('v', '') - const dash_index = no_v.lastIndexOf('-') - const no_dash = (dash_index > -1) ? no_v.substring(0, dash_index) : no_v - core.setOutput('tag', tag) - core.setOutput('no-v', no_v) - core.setOutput('no-dash', no_dash) - - name: Set meta data tag = ${{steps.set_version.outputs.no-dash}} - uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 - id: meta - with: - images: ghcr.io/${{ github.repository_owner }}/${{ env.IMAGE_NAME }} - tags: | - type=semver,pattern={{version}},value=v${{steps.set_version.outputs.no-dash}} - type=raw,value=latest - - uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 - with: - push: true - context: . - platforms: linux/amd64,linux/arm64 - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/github-pages.yaml b/.github/workflows/github-pages.yaml index c1b892e..405dca1 100644 --- a/.github/workflows/github-pages.yaml +++ b/.github/workflows/github-pages.yaml @@ -4,11 +4,9 @@ on: push: branches: - main + workflow_dispatch: -permissions: - contents: read - id-token: write - pages: write +permissions: {} concurrency: group: pages @@ -21,16 +19,19 @@ jobs: url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest timeout-minutes: 10 + permissions: + contents: read + id-token: write + pages: write steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false + # No cache in publishing workflows to avoid cache poisoning. - name: Set up uv - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 + uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 with: - enable-cache: true - version: "0.12.19" - python-version: "3.13" + enable-cache: false - name: Build documentation run: make ci-test-docs - name: Upload Pages artifact diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..de373d4 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,90 @@ +name: release + +on: + push: + tags: + - "v*" +permissions: {} +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false +jobs: + verify: + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Verify that the tag matches the package version + shell: bash + run: | + version=$(python3 -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])') + if [ "v${version}" != "${GITHUB_REF_NAME}" ]; then + echo "::error::Tag ${GITHUB_REF_NAME} does not match pyproject.toml version ${version} (expected v${version})" + exit 1 + fi + ghcr: + needs: verify + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + attestations: write + contents: read + id-token: write + packages: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Set up QEMU + uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 + # PEP 440 tags: pre-releases (e.g. v1.0.0rc1) get only their exact tag, never latest or major.minor. + - name: Generate image metadata + id: meta + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=pep440,pattern={{version}} + type=pep440,pattern={{major}}.{{minor}} + - name: Build image for scanning + uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 + with: + context: . + load: true + platforms: linux/amd64 + tags: release-scan/template-python:candidate + - name: Scan image + run: make docker-scan DOCKER_REPO_NAME=release-scan DOCKER_IMAGE_NAME=template-python GIT_TAG=candidate + - name: Log in to GHCR + uses: docker/login-action@8aa5831fb8d48d8f76594c7ddde61ae58019f550 # v4.8.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push + id: push + uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + provenance: mode=max + sbom: true + build-args: | + GIT_REVISION=${{ github.sha }} + GIT_TAG=${{ steps.meta.outputs.version }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + annotations: ${{ steps.meta.outputs.annotations }} + - name: Attest build provenance + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-name: ghcr.io/${{ github.repository }} + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: true diff --git a/.github/workflows/scorecard.yaml b/.github/workflows/scorecard.yaml new file mode 100644 index 0000000..1b1ebd6 --- /dev/null +++ b/.github/workflows/scorecard.yaml @@ -0,0 +1,39 @@ +name: scorecard + +on: + branch_protection_rule: + push: + branches: + - "main" + schedule: + - cron: "41 5 * * 1" +permissions: {} +jobs: + analysis: + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + actions: read + contents: read + id-token: write # publish results to the OpenSSF API + security-events: write # upload SARIF to code scanning + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Run OpenSSF Scorecard + uses: ossf/scorecard-action@fa7117db83c52d787ae5a89f350999701505d78e # v2.5.0 + with: + results_file: results.sarif + results_format: sarif + publish_results: true + - name: Upload SARIF artifact + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: scorecard-sarif + path: results.sarif + retention-days: 5 + - name: Upload SARIF to code scanning + uses: github/codeql-action/upload-sarif@33d7b85da1a04171e211befe2c739303b9a4c066 # v4.39.3 + with: + sarif_file: results.sarif diff --git a/.github/workflows/security.yaml b/.github/workflows/security.yaml new file mode 100644 index 0000000..c3c96bf --- /dev/null +++ b/.github/workflows/security.yaml @@ -0,0 +1,33 @@ +name: security + +on: + push: + branches: + - "main" + pull_request: + branches: + - "main" + schedule: + # Weekly, so newly disclosed vulnerabilities are found without code changes. + - cron: "17 3 * * 1" + workflow_dispatch: +permissions: + contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +jobs: + scan: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 # scan the full Git history for secrets + persist-credentials: false + - name: Set up uv + uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 + with: + enable-cache: true + - name: Audit dependencies, secrets, and files + run: make ci-test-security diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 2f9a1d3..a62574e 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -15,68 +15,69 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: - test: + quality: + name: quality gate + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + # uv version comes from [tool.uv] required-version; Python from .python-version. + - name: Set up uv + uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 + with: + enable-cache: true + - name: Run Python quality gate + run: make ci-test + - name: Build documentation + run: make ci-test-docs + compatibility: + name: python ${{ matrix.python-version }} strategy: fail-fast: false matrix: python-version: - "3.12" - "3.13" - - "3.14" + - "3.15" runs-on: ubuntu-latest timeout-minutes: 5 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - name: Set up uv with caching enabled - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 + - name: Set up uv + uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 with: enable-cache: true - version: "0.12.19" python-version: ${{ matrix.python-version }} - - name: Install actionlint - if: matrix.python-version == '3.13' - shell: bash - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - mkdir -p "$RUNNER_TEMP/actionlint" - gh release download v1.7.12 --repo rhysd/actionlint \ - --pattern 'actionlint_1.7.12_linux_amd64.tar.gz' \ - --pattern 'actionlint_1.7.12_checksums.txt' \ - --dir "$RUNNER_TEMP/actionlint" - (cd "$RUNNER_TEMP/actionlint" && sha256sum -c --ignore-missing actionlint_1.7.12_checksums.txt) - tar -xzf "$RUNNER_TEMP/actionlint/actionlint_1.7.12_linux_amd64.tar.gz" -C "$RUNNER_TEMP/actionlint" actionlint - echo "$RUNNER_TEMP/actionlint" >> "$GITHUB_PATH" - - name: Run CI tests - if: matrix.python-version == '3.13' - shell: bash - run: | - make ci-test - make ci-test-docs - - name: Run Python compatibility tests - if: matrix.python-version != '3.13' - shell: bash - run: | - uv sync --locked --no-dev --group test - uv run --locked --no-dev --group test pytest - hooks: + - name: Run tests + run: uv run --locked --no-dev --group test pytest + lowest: + name: lowest dependencies runs-on: ubuntu-latest timeout-minutes: 5 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - name: Set up uv with caching enabled - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 + - name: Set up uv + uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 with: enable-cache: true - version: "0.12.19" - python-version: "3.13" - - name: Set up Python 3.13 - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + - name: Run tests with lowest direct dependencies + run: make test-lowest + hooks: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - python-version: "3.13" + persist-credentials: false + - name: Set up uv + uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 + with: + enable-cache: true - name: Run configured hooks run: make hooks-check From c57cf4640de3720b09cc20c226e6256d346e1ece Mon Sep 17 00:00:00 2001 From: ks6088ts Date: Mon, 9 Nov 2026 08:13:32 +0900 Subject: [PATCH 6/7] chore: add governance files and align Dev Container Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .devcontainer/devcontainer.json | 14 ++++++++--- .editorconfig | 18 ++++++++++++++ .gitattributes | 3 +++ .github/CODEOWNERS | 2 ++ .github/ISSUE_TEMPLATE/bug_report.yml | 29 ++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 5 ++++ .github/ISSUE_TEMPLATE/feature_request.yml | 18 ++++++++++++++ .github/pull_request_template.md | 13 ++++++++++ .vscode/extensions.json | 12 +++++---- CONTRIBUTING.md | 13 ++++++++++ SECURITY.md | 26 +++++++++++++++++++ 11 files changed, 144 insertions(+), 9 deletions(-) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .github/CODEOWNERS create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/pull_request_template.md create mode 100644 CONTRIBUTING.md create mode 100644 SECURITY.md diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 3e1a95a..2db3a50 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,17 +1,23 @@ { "name": "template-python", - // https://mcr.microsoft.com/en-us/product/devcontainers/python/tags - "image": "mcr.microsoft.com/devcontainers/python:dev-3.13-bookworm", + // https://mcr.microsoft.com/en-us/artifact/mar/devcontainers/python/tags + "image": "mcr.microsoft.com/devcontainers/python:3-3.14-trixie", + "features": { + // Run `make ci-test-docker` and other Docker-based checks against the host daemon. + "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {} + }, "customizations": { "vscode": { "extensions": [ "charliermarsh.ruff", - "GitHub.copilot-chat", + "editorconfig.editorconfig", + "github.vscode-github-actions", "ms-python.mypy-type-checker", "ms-python.python" ] } }, - "postCreateCommand": "pipx install uv==0.12.19 && make install-deps-dev", + // Keep the uv version within [tool.uv] required-version in pyproject.toml. + "postCreateCommand": "pipx install uv==0.13.7 && make install-deps-dev && make install-hooks", "remoteUser": "vscode" } diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9b7b80c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{toml,yaml,yml}] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[Makefile] +indent_style = tab diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..03c52fd --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +* text=auto eol=lf +*.png binary +uv.lock linguist-generated=true diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..ce6c125 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# Default reviewers for every change. Replace with your team when adopting the template. +* @ks6088ts diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..7019f56 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,29 @@ +name: Bug report +description: Report unexpected behavior +labels: ["bug"] +body: + - type: textarea + id: what-happened + attributes: + label: What happened? + description: Include the exact command and the first error message. + validations: + required: true + - type: textarea + id: expected + attributes: + label: What did you expect? + validations: + required: true + - type: input + id: version + attributes: + label: Version + description: Output of `template-python --version` or the commit SHA. + validations: + required: true + - type: input + id: environment + attributes: + label: Environment + description: OS, Python version, and uv version. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..004da4a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Report a security vulnerability + url: https://github.com/ks6088ts/template-python/security/advisories/new + about: Please report vulnerabilities privately. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..80b5e76 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,18 @@ +name: Feature request +description: Propose a change or improvement +labels: ["enhancement"] +body: + - type: textarea + id: problem + attributes: + label: Problem + description: What concrete problem would this solve? + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposal + description: The simplest change that solves the problem, and alternatives considered. + validations: + required: true diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..80320ff --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,13 @@ +## Summary + + + +## Verification + + + +## Checklist + +- [ ] Checks for the changed area pass ([checks by change type](../docs/development.md#checks-by-change-type)) +- [ ] English and Japanese documentation are updated if behavior changed +- [ ] No secrets, credentials, or private configuration are included diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 20ccbd0..4d87bc2 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,7 +1,9 @@ { - "recommendations": [ - "charliermarsh.ruff", - "ms-python.mypy-type-checker", - "ms-python.python" - ] + "recommendations": [ + "charliermarsh.ruff", + "editorconfig.editorconfig", + "github.vscode-github-actions", + "ms-python.mypy-type-checker", + "ms-python.python" + ] } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..f79d3a8 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,13 @@ +# Contributing + +Thank you for improving this project. The documentation is the single source of truth for +procedures; this page only links to it. + +1. Set up the environment with the [development guide](docs/development.md#set-up-once). +2. Decide where a change belongs with the + [engineering guide](docs/architecture/README.md#where-to-change-code). +3. Run the [checks for the changed area](docs/development.md#checks-by-change-type). +4. Update the English and Japanese documentation together when behavior changes. +5. Open a pull request using the template and describe the verification you performed. + +Report security issues privately as described in [SECURITY.md](SECURITY.md). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..2acae81 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,26 @@ +# Security policy + +## Supported versions + +Only the latest release and the `main` branch receive security fixes. + +## Reporting a vulnerability + +Do not open a public issue for a suspected vulnerability. Report it privately through +[GitHub private vulnerability reporting](https://github.com/ks6088ts/template-python/security/advisories/new) +and include: + +- the affected version or commit, +- steps to reproduce or a proof of concept, +- the impact you expect. + +You should receive an acknowledgement within 5 business days. Please allow time for a fix +before any public disclosure. + +## Automated safeguards + +The repository continuously runs dependency auditing (pip-audit), secret scanning +(gitleaks), file and image scanning (Trivy), static analysis (CodeQL, Ruff security rules), +workflow analysis (actionlint, zizmor), and OpenSSF Scorecard. Release images are published +with an SBOM and signed build provenance. See the +[engineering guide](docs/architecture/README.md#security-and-supply-chain) for details. From 3c2dd4ccd50354b4254fe97c0fff3ae8e9388c20 Mon Sep 17 00:00:00 2001 From: ks6088ts Date: Mon, 9 Nov 2026 08:13:32 +0900 Subject: [PATCH 7/7] docs: document the toolchain, security, release, and adoption Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .env.template | 2 + README.md | 69 +++---- docs/architecture/README.ja.md | 317 ++++++++++++++++++--------------- docs/architecture/README.md | 191 +++++++++++--------- docs/development.ja.md | 223 ++++++++++++----------- docs/development.md | 126 ++++++++----- docs/index.ja.md | 22 ++- docs/index.md | 22 ++- mkdocs.yml | 8 + 9 files changed, 549 insertions(+), 431 deletions(-) diff --git a/.env.template b/.env.template index 26d749b..4761817 100644 --- a/.env.template +++ b/.env.template @@ -5,3 +5,5 @@ # Project Settings PROJECT_NAME=template-python PROJECT_LOG_LEVEL=INFO +# text or json +PROJECT_LOG_FORMAT=text diff --git a/README.md b/README.md index 0cb8787..234db1e 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,18 @@ [![test](https://github.com/ks6088ts/template-python/actions/workflows/test.yaml/badge.svg?branch=main)](https://github.com/ks6088ts/template-python/actions/workflows/test.yaml?query=branch%3Amain) +[![security](https://github.com/ks6088ts/template-python/actions/workflows/security.yaml/badge.svg?branch=main)](https://github.com/ks6088ts/template-python/actions/workflows/security.yaml?query=branch%3Amain) +[![codeql](https://github.com/ks6088ts/template-python/actions/workflows/codeql.yaml/badge.svg?branch=main)](https://github.com/ks6088ts/template-python/actions/workflows/codeql.yaml?query=branch%3Amain) [![docker](https://github.com/ks6088ts/template-python/actions/workflows/docker.yaml/badge.svg?branch=main)](https://github.com/ks6088ts/template-python/actions/workflows/docker.yaml?query=branch%3Amain) -[![container-release](https://github.com/ks6088ts/template-python/actions/workflows/container-release.yaml/badge.svg)](https://github.com/ks6088ts/template-python/actions/workflows/container-release.yaml) +[![release](https://github.com/ks6088ts/template-python/actions/workflows/release.yaml/badge.svg)](https://github.com/ks6088ts/template-python/actions/workflows/release.yaml) [![docs](https://github.com/ks6088ts/template-python/actions/workflows/github-pages.yaml/badge.svg)](https://github.com/ks6088ts/template-python/actions/workflows/github-pages.yaml) +[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/ks6088ts/template-python/badge)](https://scorecard.dev/viewer/?uri=github.com/ks6088ts/template-python) # template-python [日本語ガむド](docs/index.ja.md) | [Documentation](https://ks6088ts-labs.github.io/template-python) -A small, typed Python package and CLI template for starting a library or -command-line application without assembling a toolchain from scratch. +A small, typed Python CLI template with a production-grade toolchain: static +analysis, tests with coverage, vulnerability and secret scanning, hardened +containers, and CI/CD with signed releases. [Typer](https://typer.tiangolo.com/) handles the CLI, [Pydantic Settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) validates configuration, and [uv](https://docs.astral.sh/uv/) manages the locked @@ -16,15 +20,12 @@ environment. Containers, notebooks, and deployment are optional. ## Quick start -Start with **Python 3.12+**, **uv 0.13.6**, and **GNU Make** for local development. -The current local environment uses **Python 3.13.12** and **uv 0.13.6**. -CI pins **uv 0.12.19** and uses **Python 3.13** for the full quality gate; -compatibility tests also cover **Python 3.12 and 3.14**. +You need **uv 0.13.x** (`>=0.13.1,<0.14`, declared in `pyproject.toml`) and +**GNU Make**. uv installs the default **Python 3.14** from `.python-version`; +Python **3.12–3.15** are supported and tested. See the [uv installation guide](https://docs.astral.sh/uv/getting-started/installation/) -if uv is not installed. Docker, actionlint, and deployment credentials are not -needed to run the example or unit tests. - -From a terminal: +if uv is not installed. Docker and deployment credentials are not needed to run +the example or the Python quality gate. ```shell git clone https://github.com/ks6088ts/template-python.git @@ -39,26 +40,23 @@ Expected output: Hello, Ada! ``` -Then run `make test`. No virtual environment activation or `.env` file is -required. The equivalent module entry point is -`uv run --locked python -m template_python hello --name Ada`. - -Check the installed uv version and the Python runtime used by the project with: +Then run `make ci-test`. No virtual environment activation or `.env` file is +required. Other entry points: ```shell -uv --version -uv run --locked python --version +uv run --locked template-python --version +uv run --locked template-python config +uv run --locked python -m template_python hello --name Ada ``` ## Choose your next task -Read only the guide needed for the change: - | I want to... | Start here | | --- | --- | | Change behavior, run checks, or resolve a setup failure | [Development guide](docs/development.md) | | Decide where code belongs or whether to add an abstraction | [Architecture and engineering guide](docs/architecture/README.md) | | Configure the CLI | [Configuration reference](docs/development.md#configuration) | +| Understand the security and release pipeline | [Security and supply chain](docs/architecture/README.md#security-and-supply-chain) | | Turn this template into a new project | [Template adoption checklist](docs/architecture/README.md#adopting-the-template) | | Preview or edit documentation | [Documentation workflow](docs/development.md#documentation) | @@ -72,10 +70,11 @@ CLI (settings + logging) -> reusable core behavior | --- | --- | | [core.py](src/template_python/core.py) | Deterministic behavior, independent of CLI and environment | | [cli.py](src/template_python/cli.py) | Commands, user-facing errors, and application setup | -| [settings.py](src/template_python/settings.py), [logging.py](src/template_python/logging.py) | Configuration validation and logging setup | +| [settings.py](src/template_python/settings.py), [log.py](src/template_python/log.py) | Configuration validation and logging setup | | [tests/](tests/) | Behavior and CLI contract tests | | [Makefile](Makefile) | Supported local and CI commands; run `make help` to list them | | [pyproject.toml](pyproject.toml), [uv.lock](uv.lock) | Dependencies, tool configuration, and resolved versions | +| [.github/](.github/) | CI/CD workflows, Dependabot, and contribution templates | | [docs/](docs/) | Task-oriented guides in English and Japanese | ## Before submitting a change @@ -83,19 +82,18 @@ CLI (settings + logging) -> reusable core behavior | Changed area | Check | | --- | --- | | Python, dependencies, or workflows | `make ci-test` | +| Dependencies | `make audit` and `make test-lowest` | | Documentation | `make ci-test-docs` | | Container assets | `make ci-test-docker` | -The Python gate requires [actionlint](https://github.com/rhysd/actionlint/blob/main/docs/install.md) -on `PATH`. The container gate requires a running Docker daemon. If a change spans -areas, run each corresponding check. See the -[development guide](docs/development.md#checks-by-change-type) for focused checks -and Git hooks. +Docker-based checks (`ci-test-docker`, `secret-scan`, `security-scan`) require a +running Docker daemon. See the +[development guide](docs/development.md#checks-by-change-type) for focused +checks and Git hooks, and [CONTRIBUTING.md](CONTRIBUTING.md) for the pull +request flow. ## Optional assets and releases -Use these only when your project needs them: - | Asset | Entry point | | --- | --- | | Container example | `make docker-build docker-run` or `docker compose up --build` | @@ -104,13 +102,16 @@ Use these only when your project needs them: | VS Code Dev Container | [.devcontainer/](.devcontainer/) | | Image publishing and site deployment | [.github/workflows/](.github/workflows/) | -No registry or cloud account is needed for local development. Remove unused -assets and their related dependencies, Make targets, workflows, and links -together; follow the [adoption checklist](docs/architecture/README.md#adopting-the-template). +No registry or cloud account is needed for local development. Tags matching +`v*` publish a multi-platform image with an SBOM and build provenance to GHCR +after verifying that the tag equals `v` + the package version. This template does +not publish to PyPI. See [delivery](docs/architecture/README.md#delivery) before +enabling releases, and remove unused assets with the +[adoption checklist](docs/architecture/README.md#adopting-the-template). + +## Security -Tags matching `v*` publish images to Docker Hub and GHCR only after checking that -the tag matches the package version. This template does not publish to PyPI. -See [delivery rules](docs/architecture/README.md#delivery) before enabling releases. +Report vulnerabilities privately; see [SECURITY.md](SECURITY.md). ## License diff --git a/docs/architecture/README.ja.md b/docs/architecture/README.ja.md index 0c3e5ad..cef078b 100644 --- a/docs/architecture/README.ja.md +++ b/docs/architecture/README.ja.md @@ -1,165 +1,190 @@ # アヌキテクチャず゚ンゞニアリングガむド -コヌドの眮き堎所、䟝存関係の远加、配垃アセットの倉曎を刀断するずきに䜿いたす。 -実行手順ずトラブル察応は[開発ガむド](../development.ja.md)を参照しおください。 +コヌドの配眮、䟝存関係の远加、配垃甚アセットの倉曎を刀断するずきに䜿いたす。 +実行手順やトラブルシュヌティングは[開発ガむド](../development.ja.md)を参照しおください。 -## 既定の蚭蚈 +## 暙準の蚭蚈 { #default-design } -1぀のむンストヌル可胜なパッケヌゞず、薄いアプリケヌション境界で構成したす。 +このリポゞトリは、むンストヌル可胜なパッケヌゞ 1 ぀ず、薄いアプリケヌション境界 1 ぀で +構成されおいたす。 ```text -CLITyper、蚭定、ログ -> コアロゞック倀ず具䜓的な䟋倖 +CLITyper、蚭定、ログ -> コアの凊理倀ず具䜓的な䟋倖 ``` -`src/template_python/core.py` は、環境倉数、ログ、CLI の副䜜甚を持たない決定的な -凊理を担圓したす。`cli.py` が蚭定を読み、ログを構成しおコアを呌び出したす。 -`__init__.py` は CLI を import せず再利甚可胜な凊理を公開し、 -`__main__.py` は起動だけを担圓したす。 +`src/template_python/core.py` には、環境、ログ、CLI の副䜜甚を持たない決定的な凊理を +眮きたす。`cli.py` は Typer のコヌルバックで蚭定を䞀床だけ読み蟌み、ログを構成し、 +蚭定を Click のコンテキスト`ctx.obj`に栌玍しおコマンドぞ枡し、コアを呌び出したす。 +`__init__.py` は CLI を import せずに再利甚可胜な凊理ず `__version__` を公開し、 +`__main__.py` はガヌド付きの゚ントリヌポむントに限定したす。 -この分離により、ラむブラリの利甚者は関数を呌ぶだけなら、Typer、環境倉数の -読み蟌み、プロセス党䜓のログ蚭定を理解する必芁がありたせん。 +これにより、ラむブラリの利甚者は関数を呌ぶだけのために Typer、環境倉数の読み蟌み、 +プロセス党䜓のログ蚭定を理解する必芁がなくなりたす。 - +## 倉曎箇所の早芋衚 { #where-to-change-code } -## 倉曎箇所の早芋衚 - -| 倉曎したいこず | 䞻な倉曎先 | テストする契玄 | +| 倉曎内容 | 䞻な倉曎箇所 | テストする契玄 | | --- | --- | --- | -| 再利甚可胜な凊理 | `src/template_python/core.py` たたは目的の明確な新モゞュヌル | `tests/test_core.py` たたは察応する新テストで、倀・正芏化・具䜓的な䟋倖を確認 | -| コマンド・オプション・利甚者向け゚ラヌ | `src/template_python/cli.py` | `tests/test_cli.py` で stdout・stderr・終了コヌドを確認 | -| 環境倉数による蚭定 | `src/template_python/settings.py` ず `.env.template` | `tests/test_settings.py` で既定倀・蚱容倀・拒吊を確認 | -| ログ蚭定 | `src/template_python/logging.py` | `tests/test_logging.py` でレベル怜蚌・出力を確認 | -| ラむブラリの公開 API | `src/template_python/__init__.py` | CLI を初期化せず公開 API を import・実行できるこずを確認 | -| ビルド・䟝存関係 | `pyproject.toml` ず `uv.lock` | `make package-check` でむンストヌル枈み wheel の動䜜を確認 | +| 再利甚する凊理 | `src/template_python/core.py` たたは責務を絞った新しいモゞュヌル | 倀、正芏化、具䜓的な䟋倖。`tests/test_core.py` たたは察応する新しいテスト | +| コマンド、オプション、利甚者向け゚ラヌ | `src/template_python/cli.py` | 暙準出力、暙準゚ラヌ出力、終了コヌド。`tests/test_cli.py` | +| 環境蚭定 | `src/template_python/settings.py` ず `.env.template` | 既定倀、有効な倀、優先順䜍、拒吊。`tests/test_settings.py` | +| ログ蚭定 | `src/template_python/log.py` | レベルず圢匏の怜蚌、出力されるレコヌド。`tests/test_log.py` | +| 公開ラむブラリ API | `src/template_python/__init__.py` | CLI を初期化せずに公開 API を import しお呌べるこず | +| ビルドや䟝存関係の倉曎 | `pyproject.toml` ず `uv.lock` | `make package-check` でむンストヌル埌の wheel、`make test-lowest` で䞋限を確認 | -## 認知負荷を小さく保぀ +## 認知負荷を小さく保぀ { #keep-the-cognitive-cost-small } -既定の方針には理由がありたす。将来必芁かもしれないものではなく、具䜓的な芁求に -応じお倉曎したす。 +既定の方針は意図的に遞んでいたす。将来の可胜性ではなく、具䜓的な芁件があるずきに +倉曎しおください。 -| 刀断 | たず遞ぶもの | 芋盎す条件 | +| 刀断 | 既定の方針 | 芋盎す条件 | +| --- | --- | --- | +| 新しい凊理の眮き堎所 | 責務を絞ったモゞュヌルに、名前付きの関数ずしお眮く | 具䜓的な倖郚 I/O の芁件があり、アプリケヌションのアダプタヌが必芁になった | +| コヌドを共通化するか | 局所に眮き、責務が分かる名前を付ける | 耇数の実際の呌び出し元で、同じ安定した動䜜が必芁になった | +| クラスやフレヌムワヌクを远加するか | 関数ず明瀺的な匕数を優先する | 状態やラむフサむクルの管理が必芁で、単玔な組み合わせでは足りない | +| 䟝存関係やツヌルを远加するか | 暙準ラむブラリず既存のツヌルを䜿う | 明確で枬定できる䞍足があり、実行時ず保守のコストに芋合う | +| 任意のアセットを远加するか | 䜿わないなら眮かない、たたは削陀する | 実際の配垃や開発の芁件で必芁になった | + +- 汎甚的な `utils` / `common`、基底クラスの階局、サヌビスロケヌタヌ、 + 先回りした DI フレヌムワヌクは避けたす。 +- 暙準ラむブラリず同じ名前のモゞュヌルは䜜りたせんログ蚭定は `logging.py` ではなく + `log.py` に眮いおいたす。 +- 入力、出力、責務が分かる名前を䜿いたす。小さな凊理を理解するために、 + 䜕局もたどる必芁がないようにしたす。 +- 倀を返すか、具䜓的な䟋倖を送出したす。倱敗を成功に芋える既定倀に倉えたり、 + ゚ラヌを広く握り぀ぶしたり、シヌクレットをログに出したりしないでください。 +- 各䜜業の正匏な手順は 1 か所にたずめたす。耇数のドキュメントやワヌクフロヌに + コマンドをコピヌせず、リンクしたす。 + +自明でない刀断は、問題、最も単玔な代替案、遞んだトレヌドオフ、怜蚌方法を +プルリク゚ストの説明に蚘茉したす。別の蚭蚈ドキュメントは、その理由を倉曎埌も +長く残す必芁がある堎合にだけ䜜成したす。 + +## 技術遞定 { #technology-choices } + +広く䜿われ、継続的に保守され、むンタヌフェヌスが安定したツヌルを遞んでいたす。 +バヌゞョンは珟圚のメゞャヌ0.x のツヌルはマむナヌたでに制限し、`uv.lock` で +解決したす。 + +| 関心事 | 遞定 | 理由 | | --- | --- | --- | -| 新しい凊理の眮き堎所 | 目的の明確なモゞュヌルの名前付き関数 | 倖郚 I/O の具䜓的な芁求によりアプリケヌション偎の adapter が必芁 | -| 共通化するか | 責務が分かる名前で局所的に実装 | 耇数の実際の利甚箇所で同じ安定した振る舞いが必芁 | -| class・framework を远加するか | 関数ず明瀺的な匕数 | 状態や lifecycle の所有者が必芁で、単玔な組み合わせでは䞍足 | -| 䟝存関係・ツヌルを远加するか | 暙準ラむブラリず既存ツヌル | 実行時・保守時のコストに芋合う、枬定可胜な䞍足がある | -| 任意アセットを䜿うか | 未䜿甚のたたにするか削陀 | 実際の配垃・開発芁件で必芁 | - -- `utils`・`common` の寄せ集め、基底クラス階局、サヌビスロケヌタ、 - 将来を想像した DI framework は避けたす。 -- 入出力ず責務を衚す名前を䜿いたす。小さな凊理を理解するために、䜕局ものコヌドを - 远う必芁がない構造にしたす。 -- 倀を返すか、具䜓的な䟋倖を送出したす。倱敗を成功に芋える既定倀ぞ眮き換えたり、 - 広く握り぀ぶしたり、秘密情報をログぞ出したりしたせん。 -- 手順の正本を1぀にしたす。耇数の文曞や Workflow ぞコマンドをコピヌせず、 - 参照・再利甚したす。 - -自明ではない刀断は、問題、最も単玔な代替案、遞んだトレヌドオフ、怜蚌方法を -PR の説明に残したす。別の蚭蚈文曞は、その倉曎埌も理由を参照する必芁があるずき -だけ䜜りたす。 - -## 情報の正本 - -| 察象 | 正本 | +| 環境、ロック、ビルド | uv、`uv_build` | Python、仮想環境、ロック、ビルドを 1 ぀で扱い、ハッシュ付きで再珟できる | +| CLI | TyperClick ベヌス | 型ヒント駆動で、成熟した Click の䞊に構築されおいる | +| 蚭定 | Pydantic Settings | 型付きで怜蚌された環境倉数ず `.env` の読み蟌み | +| 敎圢ず Lint | Ruff | フォヌマッタヌず、bandit`S`のセキュリティルヌルを含む幅広いルヌル | +| 型 | mypystrictず Pydantic プラグむン | 基準ずなる型チェッカヌ | +| テスト | pyteststrict モヌド、pytest-cov | 事実䞊の暙準 | +| Git フック | prekpre-commit 互換 | `.pre-commit-config.yaml` を読み、別の Python 環境が䞍芁 | +| 䟝存関係の監査 | pip-audit | PyPA が保守。`uv audit` はただ experimental | +| シヌクレットスキャン | gitleaks | フックず履歎党䜓のスキャン | +| ファむルずむメヌゞのスキャン | Trivyむメヌゞのダむゞェストを固定 | ロックファむル、Dockerfile、シヌクレット、OS パッケヌゞ | +| ワヌクフロヌの解析 | actionlint、zizmor、CodeQL`actions` | 構文、セキュリティ、デヌタフロヌの怜査 | +| ドキュメント | MkDocs + Material | 安定しおいお広く䜿われおいる。Material は 2027-05-05 にサポヌトが終了するそれたでは重倧な修正が提䟛される。終了日たでに、`mkdocs.yml` をそのたた読める埌継の [Zensical](https://zensical.org/docs/compatibility/mkdocs/migration/) ぞの移行を蚈画する | + +## 正本の䞀芧 { #sources-of-truth } + +| 関心事 | 正本 | | --- | --- | | メタデヌタ、䟝存関係、ツヌル蚭定 | `pyproject.toml` | -| 解決枈み䟝存バヌゞョン | `uv.lock` | +| 解決枈みの䟝存関係ずツヌルのバヌゞョン | `uv.lock` | +| uv のバヌゞョン範囲 | `pyproject.toml` の `[tool.uv] required-version`CI の `setup-uv` も参照 | +| 既定の Python / サポヌトするバヌゞョン | `.python-version` / `requires-python`、classifiers、`.github/workflows/test.yaml` | | ロヌカルず CI のコマンド | `Makefile` | -| 最䜎 Python・テスト察象バヌゞョン | `pyproject.toml`・`.github/workflows/test.yaml` | -| 蚭定の schema・蚭定䟋 | `src/template_python/settings.py`・`.env.template` | -| コンテナ動䜜 | `Dockerfile` ず `compose.yml` | -| ドキュメント構成 | `mkdocs.yml` | - -宣蚀ず、その宣蚀から生成するものは䞀緒に曎新したす。䟝存関係は -`pyproject.toml` で倉曎し、`uv lock` で lock を再生成したす。 -CI は同じ怜蚌を独自に実装せず、Make target を呌びたす。 - -## ランタむムのルヌル - -- パッケヌゞの import 時にログ蚭定や環境倉数読み蟌みを行いたせん。 -- コアは Typer や蚭定に䟝存せず、CLI が倀ず䟋倖を衚瀺・終了コヌドぞ倉換したす。 -- `ProjectSettings` が `PROJECT_*` ず任意の `.env` を䞀元管理したす。 - 別の dotenv loader を远加したせん。 -- root logger の蚭定は CLI callback で䞀床だけ行い、ラむブラリは名前付き - logger を取埗するだけにしたす。 -- CLI は `[project.scripts]` で公開し、`python -m template_python` も維持したす。 - -## 品質ゲヌト - -[倉曎内容に応じた怜蚌](../development.ja.md#checks-by-change-type)を遞びたす。 -Python の品質ゲヌトは、敎圢、型、Lint、Workflow のルヌル、党テストを確認し、 -branch coverage **95% 以䞊**を芁求したす。wheel ず sdist をビルドした埌、 -wheel の CLI ずモゞュヌル起動を隔離環境で確認したす。 -sdist のむンストヌル詊隓は行いたせん。 - -内郚構造ではなく、倱敗時を含む芳枬可胜な契玄をテストしたす。互換性を壊す必芁が -明瀺されおいない限り、stdout、stderr、終了コヌド、import 時の動䜜を保ちたす。 - -## 配垃 - -- Pull Request ず察象ブランチで Python ずコンテナを怜蚌したす。 -- `main` から GitHub Pages ぞ英日ドキュメントを公開したす。 -- `v*` tag から Docker Hub ず GHCR ぞ multi-platform image を公開したす。 -- GHCR release には push 枈み digest に察応する provenance を付䞎したす。 -- Azure Static Web Apps は手動デプロむを維持したす。 - -Release Workflow は、tag が `v` ず `pyproject.toml` の version を連結した倀に -䞀臎するこずを確認しおから公開したす。Docker Hub には -`DOCKERHUB_USERNAME` ず `DOCKERHUB_TOKEN` の secret が必芁です。 -GHCR は Workflow の GitHub token を䜿いたす。手動のサむト公開には -`AZURE_STATIC_WEB_APPS_API_TOKEN` が必芁です。これらの認蚌情報はロヌカル開発に -䞍芁です。PyPI ぞの公開は蚭定されおいたせん。 - -倖郚 Action は commit SHA で固定したす。Workflow は明瀺的な最小暩限、 -checkout credential の無効化、timeout を䜿い、自動キャンセルは埌続実行で -眮き換えられる release 以倖の凊理に限定したす。 - - - -## テンプレヌトの導入 - -新しいプロゞェクトを䜜ったずきに䞀床行い、公開を有効にする前に確認したす。 - -1. `pyproject.toml` の配垃名、説明、version、URL、console script を蚭定し、 - ラむセンスが適切か確認したす。 -2. import 名を倉えるなら `src/template_python/` を改名し、import、テストの参照、 - script の参照先、モゞュヌル起動コマンド、coverage などのツヌル蚭定も揃えたす。 - 型付きパッケヌゞの `py.typed` は維持したす。 -3. 蚭定の既定倀、`.env.template`、Notebook の䟋、Makefile の配垃物怜蚌コマンドず - image の既定倀、Dockerfile の起動先ず label、Compose、Dev Container 名、 - docs の URL、Workflow の image 名、README の badge を曎新したす。 - 配垃名はハむフン、Python の import 名はアンダヌスコアを䜿いたす。 -4. 次の衚に埓い䞍芁なアセットを倖し、実行できない CI の参照を残したせん。 -5. `uv lock` を実行し、残したアセットを怜蚌したす。゜ヌスの盎接 import だけでなく、 - 改名埌のむンストヌル枈み CLI を確認したす。 -6. 残した配垃 Workflow にだけ、リポゞトリの公開蚭定ず secret を甚意したす。 - release tag ずパッケヌゞ version を䞀臎させたす。 - -### 任意アセット - -Notebook、docs、コンテナ、registry、Azure、Dev Container は独立したサンプルです。 -別のアプリケヌション動䜜を䜜らず、むンストヌル枈みパッケヌゞを利甚したす。 - -| 倖すアセット | 同時に削陀・曎新するもの | -| --- | --- | -| Notebook | `notebooks/`、`notebook` 䟝存グルヌプ、`jupyterlab` Make target | -| ドキュメントサむト | `docs/`、`mkdocs.yml`、`docs` グルヌプず Make target、䞡サむト公開 Workflow、`test.yaml` の docs コマンド | -| コンテナ | Dockerfile、Compose、`.dockerignore`、Docker の Make target・倉数、コンテナ怜蚌・公開 Workflow | -| registry 公開だけ | 䞍芁な job たたは container-release Workflow。必芁ならロヌカルのコンテナ怜蚌は残す | -| 手動のサむト公開だけ | `azure-static-web-app.yaml` ずデプロむの説明 | -| Dev Container | `.devcontainer/` ず環境構築の説明 | - -䟝存グルヌプを倉曎したら lock を再生成し、badge、リンク、ガむドの項目も同じ倉曎で -敎理したす。認蚌情報を芁求する未䜿甚の Workflow を残さないでください。 - -倚蚀語出力が MkDocs 1 の plugin ecosystem に䟝存する間、MkDocs は 2 未満に -固定したす。 - -## 倉曎チェックリスト - -- [ ] 倉曎の目的が1぀で、䞊蚘のモゞュヌル境界を守っおいる。 -- [ ] 期埅する倀ず倱敗をテストし、互換性を倉える堎合は明瀺しおいる。 -- [ ] 䟝存宣蚀、lock、圱響する配垃アセットが䞀臎しおいる。 -- [ ] コマンドは既存 Make target を再利甚し、新ツヌルは実際の䞍足を補っおいる。 -- [ ] 圱響する英日ドキュメントを曎新し、手順を重耇させおいない。 -- [ ] 倉曎した各領域の怜蚌が成功し、必芁なむンストヌル枈みパッケヌゞの確認も行った。 +| Git フック | `.pre-commit-config.yaml` | +| 蚭定スキヌマ / 䞊曞き䟋 | `src/template_python/settings.py` / `.env.template` | +| コンテナの動䜜 | `Dockerfile`、`.dockerignore`、`compose.yml` | +| 䟝存関係の曎新方針 | `.github/dependabot.yaml` | +| ドキュメントのナビゲヌション | `mkdocs.yml` | + +宣蚀ず、そこから生成されるアセットは䞀緒に曎新したす。䟝存関係の宣蚀は +`pyproject.toml` で倉曎し、`uv lock` で `uv.lock` を再生成したす。CI は同じ怜蚌を +別に実装せず、Make タヌゲットを呌び出したす。 + +## 実行時のルヌル { #runtime-rules } + +- 公開パッケヌゞは、ログ蚭定や環境倉数の読み蟌みを行わずに import できる状態を + 保ちたす。 +- コアモゞュヌルは Typer や蚭定に䟝存したせん。CLI がコアの倀ず䟋倖を、利甚者向けの + 出力ず終了コヌドに倉換したす。 +- `PROJECT_*` 環境倉数ず任意の `.env` の読み蟌みは `ProjectSettings` が担圓したす。 + 2 ぀目の dotenv ロヌダヌは远加しないでください。コマンドは蚭定を再床読み蟌たず、 + コンテキストから取埗したす。 +- ルヌトロガヌの蚭定は CLI のコヌルバックで䞀床だけ行いたす。ラむブラリのモゞュヌルは + 名前付きロガヌを取埗するだけにしたす。ログを収集する環境では + `PROJECT_LOG_FORMAT=json` を䜿いたす。 +- シヌクレットは `SecretStr` で宣蚀したす。Typer の敎圢された䟋倖衚瀺ではロヌカル + 倉数を衚瀺しないため、トレヌスバックからシヌクレットが挏れたせん。 +- コマンドは `[project.scripts]` で公開し、`python -m template_python` は補助的な + 実行方法ずしお維持したす。 + +## 品質ゲヌト { #quality-gate } + +[倉曎内容ごずの怜蚌](../development.ja.md#checks-by-change-type)に埓いたす。 +Python の怜蚌では、敎圢、strict な型、拡匵した Ruff のルヌルbandit のセキュリティ +ルヌルを含む、ワヌクフロヌのルヌル、党テストの分岐カバレッゞ 95% 以䞊を確認したす。 +さらに wheel ず sdist をビルドし、むンストヌルした wheel の CLI、モゞュヌル実行、 +`--version` を隔離環境で実行したす。sdist からのむンストヌルは怜蚌したせん。 + +内郚構造に結び぀いたテストではなく、倱敗時を含む倖から芳枬できる契玄をテストしたす。 +明瀺的に互換性を砎る倉曎でない限り、暙準出力、暙準゚ラヌ出力、終了コヌド、 +import 時の動䜜を維持したす。 + +## セキュリティずサプラむチェヌン { #security-and-supply-chain } + +| å±€ | 察策 | 堎所 | +| --- | --- | --- | +| ゜ヌスコヌド | Ruff の `S` ルヌル、CodeQL`python`、`actions`、security-extended | `make lint`、`codeql.yaml` | +| シヌクレット | gitleaks のフック、履歎党䜓のスキャン、Trivy のシヌクレットスキャナヌ、`detect-private-key` | フック、`make secret-scan`、`make security-scan` | +| 䟝存関係 | ロック枈みの党パッケヌゞを pip-audit で監査ハッシュ怜蚌、Trivy による `uv.lock` のスキャン、クヌルダりン付きの Dependabot | `make audit`、`make security-scan`、`dependabot.yaml` | +| ワヌクフロヌ | Actions を SHA で固定、最小限の `permissions`、`persist-credentials: false`、公開甚ワヌクフロヌではキャッシュを䜿わない、actionlint ず zizmor | `.github/workflows/`、`make lint` | +| コンテナ | ベヌスむメヌゞず uv むメヌゞをダむゞェストで固定、ハッシュ付きロックから `uv sync --locked`、非 root ナヌザヌ、実行むメヌゞに pip を含めない、hadolint、Trivy によるむメヌゞのスキャン | `Dockerfile`、`make ci-test-docker` | +| リリヌス | タグずバヌゞョンの䞀臎確認、公開前のスキャン、SBOM ず max モヌドのプロベナンス、GitHub の artifact attestation | `release.yaml` | +| 党䜓の健党性 | OpenSSF Scorecard を毎週公開 | `scorecard.yaml` | + +`security.yaml` はプルリク゚スト、`main`、毎週のスケゞュヌルで +`make ci-test-security` を実行したす。新しく公開された脆匱性は、コヌドを倉曎しなくおも +CI の倱敗ずしお怜出されたす。 + +## 配垃 { #delivery } + +- プルリク゚ストず察象ブランチでは、Python、セキュリティ、CodeQL、コンテナの怜蚌を + 実行したす。 +- `main` では、英語ず日本語のドキュメントを GitHub Pages の成果物ずしお公開したす。 +- `v*` タグでは `release.yaml` が動き、マルチプラットフォヌム`linux/amd64`、 + `linux/arm64`のむメヌゞを GHCR に公開したす。 +- Azure Static Web Apps ぞのデプロむは手動のたたです。 + +リリヌスワヌクフロヌは、たずタグが `v` ず `pyproject.toml` のバヌゞョンを連結したものず +完党に䞀臎するこずを確認したす。次にむメヌゞをビルドしお Trivy でスキャンし、 +SBOM ずビルドプロベナンスを付けお push し、GitHub の artifact attestation を蚘録したす。 +タグは PEP 440 に埓いたす。`v1.2.3` は `1.2.3`、`1.2`、`latest` を公開し、 +`v1.3.0rc1` のようなプレリリヌスは `1.3.0rc1` だけを公開したす。むメヌゞは次のように +怜蚌できたす。 + +```shell +gh attestation verify oci://ghcr.io/ks6088ts/template-python: --owner ks6088ts +``` + +GHCR はワヌクフロヌの `GITHUB_TOKEN` を䜿いたす。手動のサむトデプロむには +`AZURE_STATIC_WEB_APPS_API_TOKEN` が必芁です。これらの認蚌情報はロヌカル開発には +䞍芁です。PyPI ず Docker Hub ぞの公開は蚭定しおいたせん。 + +## テンプレヌトの導入 { #adopting-the-template } + +1. 配垃名ずパッケヌゞ名を倉曎したす。`pyproject.toml`、`src/`、テスト、`Makefile`、 + `Dockerfile`、`compose.yml`、ワヌクフロヌ、`mkdocs.yml`、ドキュメントに含たれる + `template-python` / `template_python` を眮き換え、`uv lock` を実行したす。 +2. `pyproject.toml` の URL、`.github/CODEOWNERS`、`SECURITY.md`、バッゞ、 + `mkdocs.yml` のオヌナヌ名を眮き換えたす。 +3. 蚭定のプレフィックス`PROJECT_`を決め、`.env.template` ず蚭定リファレンスを + 曎新したす。 +4. `hello` コマンドず `core.greeting` を実際の凊理ずテストに眮き換えたす。 + `--version` ず `config` は残したす。 +5. 䜿わない任意のアセットは、䟝存関係、Make タヌゲット、ワヌクフロヌ、リンクず䞀緒に + 削陀したす。察象は Notebook のグルヌプず `notebooks/`、MkDocs ず Pages/SWA の + ワヌクフロヌ、Dev Container、`compose.yml` です。 +6. リポゞトリの蚭定で、private vulnerability reporting、Dependabot アラヌト、 + コヌドスキャン、`test`・`security`・`codeql`・`docker` のチェックを必須ずする + ブランチ保護を有効にしたす。 +7. `make ci-test ci-test-docs ci-test-security ci-test-docker` を実行し、 + 最初のリリヌスのタグを付けたす。 diff --git a/docs/architecture/README.md b/docs/architecture/README.md index ddc9bc7..85ed01c 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -4,7 +4,7 @@ Use this guide when deciding where code belongs, adding a dependency, or changin delivery assets. For executable procedures and troubleshooting, use the [development guide](../development.md). -## Default design +## Default design { #default-design } The repository has one installable package and one thin application boundary: @@ -13,25 +13,27 @@ CLI (Typer, settings, logging) -> core behavior (values and specific exceptions) ``` `src/template_python/core.py` contains deterministic behavior with no -environment, logging, or CLI side effects. `cli.py` loads settings, configures -logging, and calls the core. `__init__.py` exports reusable behavior without -importing the CLI; `__main__.py` is only an entry point. +environment, logging, or CLI side effects. `cli.py` loads settings once in the +Typer callback, configures logging, stores the settings on the Click context +(`ctx.obj`) for commands, and calls the core. `__init__.py` exports reusable +behavior and `__version__` without importing the CLI; `__main__.py` is only a +guarded entry point. This keeps library consumers from needing to understand Typer, environment loading, or process-global logging just to call a function. -## Where to change code +## Where to change code { #where-to-change-code } | Change | Primary location | Contract to test | | --- | --- | --- | | Reusable behavior | `src/template_python/core.py` or a focused new module | Values, normalization, and specific exceptions in `tests/test_core.py` or a matching new test module | | Command, option, or user-facing error | `src/template_python/cli.py` | stdout, stderr, and exit codes in `tests/test_cli.py` | -| Environment configuration | `src/template_python/settings.py` and `.env.template` | Defaults, valid values, and rejection in `tests/test_settings.py` | -| Logging setup | `src/template_python/logging.py` | Level validation and emitted messages in `tests/test_logging.py` | +| Environment configuration | `src/template_python/settings.py` and `.env.template` | Defaults, valid values, precedence, and rejection in `tests/test_settings.py` | +| Logging setup | `src/template_python/log.py` | Level/format validation and emitted records in `tests/test_log.py` | | Public library API | `src/template_python/__init__.py` | Import and call the public API without initializing the CLI | -| Build or dependency change | `pyproject.toml` and `uv.lock` | Installed wheel behavior via `make package-check` | +| Build or dependency change | `pyproject.toml` and `uv.lock` | Installed wheel behavior via `make package-check`; lower bounds via `make test-lowest` | -## Keep the cognitive cost small +## Keep the cognitive cost small { #keep-the-cognitive-cost-small } Defaults are deliberate. Change them for a concrete requirement, not a possible future one. @@ -46,6 +48,8 @@ future one. - Avoid generic `utils` / `common` buckets, base-class hierarchies, service locators, and speculative dependency-injection frameworks. +- Do not name modules after the standard library (the logging setup lives in + `log.py`, not `logging.py`). - Use names that reveal inputs, outputs, and responsibility. A reader should not need to follow several layers to understand a small operation. - Return values or raise specific exceptions. Do not turn failures into @@ -57,112 +61,129 @@ For a non-obvious decision, include the problem, simplest alternative, chosen trade-off, and verification in the PR description. A separate design document is only useful when the rationale needs to outlive the change. -## Sources of truth +## Technology choices { #technology-choices } + +The stack favors widely adopted, actively maintained tools with stable +interfaces. Versions are bounded to the current major (or minor for 0.x tools) +and resolved in `uv.lock`. + +| Concern | Choice | Why | +| --- | --- | --- | +| Environment, lock, build | uv, `uv_build` | One tool for Python, venv, locking, and builds; reproducible with hashes | +| CLI | Typer (on Click) | Type-hint driven, mature Click foundation | +| Configuration | Pydantic Settings | Validated, typed environment and `.env` loading | +| Format and lint | Ruff | Formatter plus a broad rule set including bandit (`S`) security rules | +| Types | mypy (strict) with the Pydantic plugin | Reference type checker | +| Tests | pytest (strict mode), pytest-cov | De facto standard | +| Git hooks | prek (pre-commit compatible) | Reads `.pre-commit-config.yaml`; no separate Python environment | +| Dependency audit | pip-audit | PyPA-maintained; `uv audit` is still experimental | +| Secret scan | gitleaks | Hook and full-history scan | +| File and image scan | Trivy (pinned image digest) | Lock file, Dockerfile, secrets, and OS packages | +| Workflow analysis | actionlint, zizmor, CodeQL (`actions`) | Syntax, security, and data-flow checks | +| Documentation | MkDocs + Material | Stable and widely used. Material reaches end of life on 2027-05-05 (critical fixes until then); plan a migration to its successor [Zensical](https://zensical.org/docs/compatibility/mkdocs/migration/), which reads `mkdocs.yml`, before that date | + +## Sources of truth { #sources-of-truth } | Concern | Source | | --- | --- | | Metadata, dependencies, tool configuration | `pyproject.toml` | -| Resolved dependency versions | `uv.lock` | +| Resolved dependency and tool versions | `uv.lock` | +| uv version range | `[tool.uv] required-version` in `pyproject.toml` (also read by `setup-uv` in CI) | +| Default Python / supported versions | `.python-version` / `requires-python`, classifiers, and `.github/workflows/test.yaml` | | Local and CI commands | `Makefile` | -| Minimum Python / tested versions | `pyproject.toml` / `.github/workflows/test.yaml` | +| Git hooks | `.pre-commit-config.yaml` | | Configuration schema / example overrides | `src/template_python/settings.py` / `.env.template` | -| Container behavior | `Dockerfile` and `compose.yml` | +| Container behavior | `Dockerfile`, `.dockerignore`, and `compose.yml` | +| Dependency update policy | `.github/dependabot.yaml` | | Documentation navigation | `mkdocs.yml` | Update declarations and derived assets together. Dependency declarations change in `pyproject.toml`; regenerate `uv.lock` with `uv lock`. CI should call Make targets instead of maintaining another implementation of the same checks. -## Runtime rules +## Runtime rules { #runtime-rules } - The public package must remain importable without configuring logging or reading environment variables. - Core modules do not depend on Typer or settings. The CLI translates their values and exceptions into user-facing output and exit codes. - `ProjectSettings` owns `PROJECT_*` environment and optional `.env` loading. - Do not add a second dotenv loader. + Do not add a second dotenv loader. Commands read settings from the context + rather than loading them again. - Configure root logging once in the CLI callback. Library modules only obtain - named loggers. + named loggers. Use `PROJECT_LOG_FORMAT=json` where logs are collected. +- Declare secrets as `SecretStr`. Typer's pretty exceptions do not show local + variables, so tracebacks do not leak secret values. - Expose commands through `[project.scripts]`; keep `python -m template_python` as a convenience path. -## Quality gate +## Quality gate { #quality-gate } Use the [checks by change type](../development.md#checks-by-change-type). -The Python gate checks formatting, types, lint, workflow rules, and all tests -with branch coverage of at least 95%. It builds wheel and sdist, then runs the -installed wheel's CLI and module entry point in isolation. It does not smoke-test -the sdist installation. +The Python gate checks formatting, strict types, an extended Ruff rule set +(including bandit security rules), workflow rules, and all tests with branch +coverage of at least 95%. It builds wheel and sdist, then runs the installed +wheel's CLI, module entry point, and `--version` in isolation. It does not +smoke-test the sdist installation. Test observable contracts, including failure paths, instead of coupling tests to internal structure. Keep stdout, stderr, exit codes, and import-time behavior stable unless the change explicitly requires a compatibility break. -## Delivery +## Security and supply chain { #security-and-supply-chain } + +| Layer | Control | Where | +| --- | --- | --- | +| Source | Ruff `S` rules, CodeQL (`python`, `actions`, security-extended) | `make lint`, `codeql.yaml` | +| Secrets | gitleaks hook, full-history scan, Trivy secret scanner, `detect-private-key` | Hooks, `make secret-scan`, `make security-scan` | +| Dependencies | pip-audit on every locked package (hash-verified), Trivy on `uv.lock`, Dependabot with cooldown | `make audit`, `make security-scan`, `dependabot.yaml` | +| Workflows | Actions pinned by SHA, minimal `permissions`, `persist-credentials: false`, no cache in publishing workflows, actionlint and zizmor | `.github/workflows/`, `make lint` | +| Container | Digest-pinned base and uv images, `uv sync --locked` from hashed lock, non-root user, no pip in the runtime image, hadolint, Trivy image scan | `Dockerfile`, `make ci-test-docker` | +| Release | Tag/version check, pre-release scan, SBOM and max-mode provenance, GitHub artifact attestation | `release.yaml` | +| Posture | OpenSSF Scorecard published weekly | `scorecard.yaml` | + +`security.yaml` runs `make ci-test-security` on pull requests, on `main`, and +weekly, so newly disclosed vulnerabilities fail CI without a code change. -- Pull requests and supported branches run Python and container validation. +## Delivery { #delivery } + +- Pull requests and supported branches run Python, security, CodeQL, and + container validation. - `main` publishes the bilingual documentation with GitHub Pages artifacts. -- `v*` tags publish multi-platform images to Docker Hub and GHCR. -- GHCR releases include build provenance tied to the pushed digest. +- `v*` tags run `release.yaml`, which publishes a multi-platform + (`linux/amd64`, `linux/arm64`) image to GHCR. - Azure Static Web Apps deployment remains manual. -The release workflow verifies that the tag is exactly `v` followed by the -`pyproject.toml` version before publishing. It requires `DOCKERHUB_USERNAME` and -`DOCKERHUB_TOKEN` secrets for Docker Hub; GHCR uses the workflow's GitHub token. -The manual site workflow requires `AZURE_STATIC_WEB_APPS_API_TOKEN`. These -credentials are not required for local development. PyPI publishing is not -configured. - -Third-party actions are pinned by commit SHA. Workflows use explicit minimum -permissions, disabled checkout credentials, bounded timeouts, and cancellation -only for superseded non-release runs. - -## Adopting the template - -Do this once when creating a project, before enabling publishing. - -1. Set the project identity: distribution name, description, version, URLs, and - console script in `pyproject.toml`. Confirm the license is appropriate. -2. If renaming the import package, rename `src/template_python/` and update - imports, test references, the script target, module-entry commands, and tool - paths such as coverage. Preserve `py.typed` for the typed package. -3. Update settings defaults, `.env.template`, notebook examples, Makefile smoke - commands and image defaults, Dockerfile entry point and labels, Compose, - Dev Container name, documentation URLs, workflow image names, and README - badges. Distribution names use hyphens; Python import names use underscores. -4. Remove unneeded assets using the table below; do not leave dead CI references. -5. Run `uv lock` and checks for the retained assets. Verify the renamed installed - CLI, not just a direct source import. -6. Configure repository publishing settings and secrets only for retained - delivery workflows. Match the package version to the release tag. - -### Optional assets - -Notebook, docs, containers, registries, Azure, and the Dev Container are -independent examples. They must consume the installed package rather than -creating alternative application behavior. - -| Remove this asset | Also remove or update | -| --- | --- | -| Notebook | `notebooks/`, the `notebook` dependency group, and `jupyterlab` Make target | -| Documentation site | `docs/`, `mkdocs.yml`, the `docs` group and Make targets, both site deployment workflows, and the docs command in `test.yaml` | -| Containers | Dockerfile, Compose, `.dockerignore`, Docker Make targets/variables, and container validation/release workflows | -| Registry publishing only | Unneeded jobs or the container-release workflow; keep local container checks if still useful | -| Manual site deployment only | `azure-static-web-app.yaml` and deployment references | -| Dev Container | `.devcontainer/` and setup references | - -In the same change, regenerate the lock file if groups changed and clean up -badges, links, and guide entries. Avoid keeping an unused workflow that requires -credentials. - -MkDocs is intentionally constrained below version 2 while multilingual output -depends on the MkDocs 1 plugin ecosystem. - -## Change checklist - -- [ ] The change has one clear purpose and uses the module boundaries above. -- [ ] Tests cover the expected values and failures; compatibility changes are explicit. -- [ ] Dependency declarations, lock file, and affected delivery assets agree. -- [ ] Commands reuse existing Make targets; any new tool fills a demonstrated gap. -- [ ] Affected English and Japanese documentation is updated without duplicating procedures. -- [ ] Checks for each changed area pass, including installed-package checks where relevant. +The release workflow first verifies that the tag is exactly `v` followed by the +`pyproject.toml` version. It builds and scans an image with Trivy, then pushes +with an SBOM and build provenance and records a GitHub artifact attestation. +Tags follow PEP 440: `v1.2.3` publishes `1.2.3`, `1.2`, and `latest`; a +pre-release such as `v1.3.0rc1` publishes only `1.3.0rc1`. Verify an image with: + +```shell +gh attestation verify oci://ghcr.io/ks6088ts/template-python: --owner ks6088ts +``` + +GHCR uses the workflow's `GITHUB_TOKEN`. The manual site workflow requires +`AZURE_STATIC_WEB_APPS_API_TOKEN`. These credentials are not required for local +development. PyPI and Docker Hub publishing are not configured. + +## Adopting the template { #adopting-the-template } + +1. Rename the distribution and package: `template-python` / `template_python` + in `pyproject.toml`, `src/`, tests, `Makefile`, `Dockerfile`, `compose.yml`, + workflows, `mkdocs.yml`, and docs. Run `uv lock`. +2. Replace the owner in `pyproject.toml` URLs, `.github/CODEOWNERS`, + `SECURITY.md`, badges, and `mkdocs.yml`. +3. Choose the settings prefix (`PROJECT_`) and update `.env.template` and the + configuration reference. +4. Replace the `hello` command and `core.greeting` with real behavior and tests; + keep `--version` and `config`. +5. Remove unused optional assets together with their dependencies, Make + targets, workflows, and links: the notebook group and `notebooks/`, MkDocs and + the Pages/SWA workflows, the Dev Container, or `compose.yml`. +6. In repository settings, enable private vulnerability reporting, Dependabot + alerts, code scanning, and branch protection requiring the `test`, + `security`, `codeql`, and `docker` checks. +7. Run `make ci-test ci-test-docs ci-test-security ci-test-docker` and tag the + first release. diff --git a/docs/development.ja.md b/docs/development.ja.md index 3797313..b6db61f 100644 --- a/docs/development.ja.md +++ b/docs/development.ja.md @@ -5,15 +5,14 @@ [゚ンゞニアリングガむド](architecture/README.ja.md)を参照しおください。 以䞋のコマンドはすべおリポゞトリのルヌトで実行したす。 -## 最初に䞀床だけ準備する +## 最初に䞀床だけ準備する { #set-up-once } | ツヌル | 必芁になる堎面 | | --- | --- | -| Python 3.12+ | パッケヌゞずテスト。CI の党怜蚌に合わせるなら 3.13 | -| [uv 0.12.19](https://docs.astral.sh/uv/getting-started/installation/) | ロック枈み䟝存関係の管理ずコマンド実行 | +| [uv 0.13.x](https://docs.astral.sh/uv/getting-started/installation/) | 垞に必芁。`pyproject.toml` で `>=0.13.1,<0.14` を芁求 | +| Python 3.12〜3.15 | uv が必芁に応じお導入。`.python-version` で 3.14 を遞択 | | GNU Make | リポゞトリの暙準コマンド | -| [actionlint](https://github.com/rhysd/actionlint/blob/main/docs/install.md) | `make lint` ず `make ci-test`。CI は 1.7.12 を䜿甚 | -| Docker ず起動枈みの daemon | コンテナ関連のコマンドだけ | +| Docker ず起動枈みのデヌモン | コンテナ怜蚌、`secret-scan`、`security-scan` のみ | ```shell make install-deps-dev @@ -21,31 +20,32 @@ uv run --locked template-python --help make test ``` -`.venv` は uv が管理したす。別の `pip install` でプロゞェクトの䟝存関係を -導入したり、仮想環境を有効化したりする必芁はありたせん。 -`make install-deps-dev` が衚瀺する actionlint の案内は、Python テストの実行を -劚げたせん。 +`.venv` は `uv` が管理したす。䟝存関係を別途 `pip install` でむンストヌルしないで +ください。仮想環境の有効化も䞍芁です。Ruff、mypy、actionlint、zizmor、pip-audit は +`uv.lock` からむンストヌルされるため、グロヌバルなツヌルは䞍芁です。 -Git hook を䜿う堎合だけ、次を実行したす。 +Git フックを䞀床むンストヌルしたす。 ```shell make install-hooks make hooks-check ``` -Hook は YAML、空癜、Ruff のルヌルを確認したす。型怜査、テスト、党品質ゲヌトの -代わりにはなりたせん。 +フックはファむルの敎圢確認、秘密鍵ずシヌクレットの怜出gitleaks、 +`uv lock --check`、Ruff、mypy、actionlint、zizmor を実行したす。Python ツヌルは +`uv run --locked` 経由で動くため、フックず CI のバヌゞョンは垞に䞀臎したす。 +フックはテストを実行しないので、push 前に `make ci-test` を実行しおください。 -## 日垞の開発 +## 日々の開発 { #everyday-development } 1. [倉曎箇所の早芋衚](architecture/README.ja.md#where-to-change-code)で、 - 倉曎するモゞュヌルずテストを特定したす。 -2. 期埅する出力ず倱敗時の振る舞いを、小さなテストで定矩したす。 -3. その契玄を満たす最も単玔な倉曎を実装したす。 -4. 察象のテストを実行し、敎圢ず倉曎範囲に合う怜蚌を行いたす。 -5. 振る舞いが倉わる堎合は、察応する英語ず日本語の説明を曎新したす。 + 察象モゞュヌルず察応するテストを確認したす。 +2. 期埅する出力ず倱敗時の動䜜を、察象を絞ったテストで定矩したす。 +3. その契玄を満たす最小限の倉曎を実装したす。 +4. 察象テスト、敎圢、倉曎した領域の怜蚌を順に実行したす。 +5. 動䜜を倉曎した堎合は、関連する英語ず日本語のドキュメントを曎新したす。 -挚拶や CLI を倉曎する堎合の䟋です。 +挚拶や CLI を倉曎する堎合は、次のコマンドから始めたす。 ```shell uv run --locked pytest --no-cov tests/test_core.py tests/test_cli.py @@ -53,120 +53,143 @@ make fix make ci-test ``` -`--no-cov` は察象を絞った反埩䜜業だけに䜿いたす。䞀郚のテストでパッケヌゞ党䜓の -カバレッゞを枬るず、倉曎ずは無関係な䞍足で倱敗するためです。 -`make test` ず `make ci-test` は党テストず branch coverage を実行し、 -**95% 以䞊**を芁求したす。䞀郚のテストを通すために閟倀を䞋げないでください。 +`--no-cov` は、察象を絞った確認のためだけに䜿いたす。䞀郚のテストだけで +パッケヌゞ党䜓のカバレッゞ基準を枬るず、誀った倱敗になるためです。 +`make test` ず `make ci-test` は党テストを分岐カバレッゞ付きで実行し、 +**95%** 以䞊を芁求したすテンプレヌト自䜓は 100%。pytest は strict モヌドで動き、 +譊告を゚ラヌずしお扱いたす。通すためだけに基準を䞋げたり、譊告を広く無芖したり +しないでください。 - +テストは䞀時的な䜜業ディレクトリで、`PROJECT_*` 倉数を取り陀いた状態で実行されたす +`tests/conftest.py` を参照。手元のシェルや `.env` は結果に圱響したせん。 -### 倉曎内容に応じた怜蚌 +### 倉曎内容ごずの怜蚌 { #checks-by-change-type } -| 倉曎察象 | 䜜業䞭の確認 | 提出前の怜蚌 | +| 倉曎した領域 | 玠早い確認 | 提出前 | | --- | --- | --- | -| Python の凊理・テスト | `--no-cov` 付きの察象テスト、`make fix`、`make type-check` | `make ci-test` | -| 䟝存関係・パッケヌゞ情報 | `uv lock`、`make package-check` | `make ci-test`。圱響する堎合は docs・コンテナも怜蚌 | -| Markdown・`mkdocs.yml` | `make docs-serve` | `make ci-test-docs` | -| GitHub Workflow | `actionlint` | `make ci-test` ず、その Workflow が呌ぶ怜蚌 | -| Dockerfile・Compose | `make docker-build docker-run` | `make ci-test-docker`。Compose 倉曎時は `docker compose up --build` も実行 | +| Python の動䜜やテスト | 察象テストを `--no-cov` 付きで実行、`make fix`、`make type-check` | `make ci-test` | +| 䟝存関係やパッケヌゞ情報 | `uv lock`、`make package-check` | `make ci-test`、`make audit`、`make test-lowest` | +| Markdown や `mkdocs.yml` | `make docs-serve` | `make ci-test-docs` | +| GitHub ワヌクフロヌや Dependabot | `uv run --locked actionlint`、`uv run --locked zizmor --offline .` | `make ci-test` | +| Dockerfile や Compose | `make docker-build docker-run` | `make ci-test-docker`。Compose を倉曎した堎合は `docker compose up --build` も実行 | -耇数の領域を倉曎したら、該圓する怜蚌をすべお行いたす。ドキュメントだけの倉曎で -Python やコンテナの品質ゲヌトを実行する必芁はありたせん。 +耇数の領域にたたがる堎合は、該圓する怜蚌をすべお実行したす。 -新しい手順を芚える代わりに、既存コマンドを䜿いたす。 - -| コマンド | 実行するこず | +| コマンド | 内容 | | --- | --- | -| `make help` | 利甚できる Make target の䞀芧衚瀺 | -| `make fix` | Ruff による自動修正ず敎圢。自動倉曎の内容は確認する | -| `make lint` | mypy strict、Ruff、actionlint、オフラむンの zizmor | -| `make test` | branch coverage 付きの pytest | -| `make package-check` | wheel ず sdist をビルドし、wheel の CLI ずモゞュヌル起動を隔離環境で確認 | -| `make ci-test` | dev 䟝存関係の同期、敎圢確認、Lint、テスト、配垃物怜蚌 | - -CI は Python 3.13 で党品質ゲヌトず docs ビルド、3.12 ず 3.14 で互換性テスト、 -別の job で hook の怜蚌を行いたす。 - - - -## 蚭定リファレンス - -CLI は既定倀で動䜜したす。ロヌカル蚭定を倉曎する堎合だけ、 +| `make help` | 利甚できる Make タヌゲットを衚瀺 | +| `make fix` | Ruff の自動修正ず敎圢を適甚。倉曎内容は確認する | +| `make lint` | strict な mypy、Ruffセキュリティルヌルを含む、actionlint、zizmor を実行 | +| `make test` | 分岐カバレッゞ付きで pytest を実行 | +| `make test-lowest` | Python 3.12 ず、宣蚀した盎接䟝存の䞋限バヌゞョンでテスト | +| `make package-check` | wheel ず sdist をビルドし、むンストヌル埌の CLI、モゞュヌル実行、`--version` を確認 | +| `make ci-test` | 開発甚䟝存関係を同期し、敎圢、Lint、テスト、パッケヌゞ怜蚌を実行 | +| `make audit` | ロック枈みの党䟝存関係を pip-audit で監査ハッシュ怜蚌、pip 䞍䜿甚 | +| `make secret-scan` | gitleaks で Git 履歎党䜓をスキャンDocker | +| `make security-scan` | Trivy で脆匱な䟝存関係、蚭定ミス、シヌクレットをスキャンDocker | +| `make ci-test-security` | `audit`、`secret-scan`、`security-scan` を実行 | + +CI は Python 3.14 で党怜蚌ずドキュメントのビルドを行い、3.12・3.13・3.15 の互換性 +テスト、䞋限䟝存のゞョブ、フックを別ゞョブで実行したす。セキュリティスキャンは +プルリク゚ストず毎週実行されたす。詳现は +[セキュリティずサプラむチェヌン](architecture/README.ja.md#security-and-supply-chain) +を参照しおください。 + +## 蚭定 { #configuration } + +CLI は既定倀だけで動䜜したす。ロヌカルで倀を倉曎する堎合のみ、 `.env.template` を `.env` にコピヌしたす。 ```shell cp .env.template .env ``` -| 環境倉数 | 既定倀 | 蚱容倀・圱響 | +| 倉数 | 既定倀 | 指定できる倀ず効果 | | --- | --- | --- | -| `PROJECT_NAME` | `template-python` | 起動時の debug ログに䜿う文字列。挚拶の出力は倉わらない | +| `PROJECT_NAME` | `template-python` | デバッグ甚の起動ログに衚瀺する文字列。挚拶文は倉わりたせん | | `PROJECT_LOG_LEVEL` | `INFO` | `DEBUG`、`INFO`、`WARNING`、`ERROR`、`CRITICAL` | +| `PROJECT_LOG_FORMAT` | `text` | 人が読む `text`、たたは 1 行 1 JSON オブゞェクトの `json`ログ収集基盀やコンテナ向け | -優先順䜍は、環境倉数、`.env`、既定倀の順です。倉数名の倧文字・小文字は区別 -したせんが、ログレベルの**倀**は䞊蚘の倧文字衚蚘にしたす。 -`.env` は䜜業ディレクトリから読み、未知の項目は拒吊したす。別のアプリケヌションの -`.env` を流甚しないでください。ロヌカル蚭定や秘密情報はコミットしたせん。 +優先順䜍は、環境倉数、`.env`、既定倀の順です。 +倉数名の倧文字ず小文字は区別したせんが、**倀**は䞊蚘の遞択肢ず完党に䞀臎させおください。 +`.env` は䜜業ディレクトリから読み蟌たれ、未知の `PROJECT_*` 項目ぱラヌになりたす。 +別のアプリケヌションの `.env` は再利甚しないでください。ロヌカル蚭定やシヌクレットは +バヌゞョン管理に含めないでください。 ```shell PROJECT_LOG_LEVEL=DEBUG uv run --locked template-python hello --name Ada -uv run --locked template-python --verbose hello --name Ada +PROJECT_LOG_FORMAT=json uv run --locked template-python --verbose hello +uv run --locked template-python config ``` -`--verbose` はグロヌバルオプションなので、`hello` の前に眮きたす。 -蚭定を怜蚌した埌で debug ログを有効にするため、䞍正な蚭定を回避する甚途には -䜿えたせん。正垞な挚拶は stdout、ログず怜蚌゚ラヌは stderr に出力したす。 -空癜だけの `--name` ず䞍正な蚭定は、終了コヌド **2** で倱敗したす。 +`config` は有効な蚭定を JSON で出力したす。認蚌情報は `pydantic.SecretStr` で宣蚀 +しおください。倀は `**********` ず衚瀺されたす。 -## 䟝存関係 - -- `pyproject.toml` の䟝存宣蚀を線集し、`uv lock` を実行しお䞡方のファむルを - コミットしたす。解決枈みの lock ゚ントリを手で線集したせん。 -- `make update` は党䜓の曎新を意図した堎合だけ䜿いたす。既存バヌゞョンの同期では - なく、`uv lock --upgrade` を実行するコマンドです。 -- 実行時の䟝存関係は `project.dependencies`、開発・テスト・docs・Notebook の - ツヌルは既存の各グルヌプぞ远加したす。 -- 既存コマンドは必芁な䟝存グルヌプを遞んで同期したす。docs ず Notebook は - `--no-dev` を䜿い、その埌の dev コマンドで必芁な dev グルヌプが再び同期されたす。 - - - -## ドキュメントの開発 +| グロヌバルオプション | 効果 | +| --- | --- | +| `--verbose`、`-v` | 蚭定の怜蚌埌にデバッグログを遞択。䞍正な蚭定を有効にはしたせん | +| `--version`、`-V` | `template-python ` を衚瀺しお終了。蚭定が䞍正でも動䜜したす | +| `--help` | ヘルプを衚瀺。コマンドなしで実行した堎合もヘルプを衚瀺終了コヌド 2 | + +グロヌバルオプションはコマンドより前に指定したす`template-python --verbose hello`。 +コマンドの出力は暙準出力ぞ、ログず怜蚌゚ラヌは暙準゚ラヌ出力ぞ曞き蟌たれたす。 +空癜だけの `--name` や䞍正な蚭定は、終了コヌド **2** で終了したす。 + +## 䟝存関係 { #dependencies } + +- 䟝存関係の宣蚀は `pyproject.toml` で倉曎し、`uv lock` を実行しおから、 + マニフェストず `uv.lock` の䞡方をコミットしたす。ロックファむルの解決結果は + 手䜜業で線集しないでください。 +- 䞋限バヌゞョンを正しく保ちたす。`make test-lowest` は盎接䟝存の䞋限バヌゞョンを + むンストヌルし、成功する必芁がありたす。 +- `make update` は、意図的に䟝存関係を広く曎新する堎合だけ䜿いたす。 + 珟圚のバヌゞョンを同期するのではなく、`uv lock --upgrade` を実行したす。 + 実行埌は `make audit` を実行しおください。 +- 実行時の䟝存関係は `project.dependencies` に眮き、開発、テスト、ドキュメント、 + Notebook 甚のツヌルは既存のグルヌプに眮きたす。 +- Dependabot は Python パッケヌゞ、GitHub Actions、pre-commit フック、Docker の + ベヌスむメヌゞ、Dev Container の feature に぀いお、7 日間のクヌルダりン埌に + グルヌプ化した曎新を毎週䜜成したす。`Makefile` のツヌルむメヌゞず + Dev Container の uv バヌゞョンは手動で固定しおいたす。uv を䞊げる堎合は + `[tool.uv] required-version` ず合わせお曎新しおください。 + +## ドキュメント { #documentation } ```shell make docs-serve ``` -MkDocs が衚瀺する URL通垞は `http://127.0.0.1:8000`を開きたす。 -`Ctrl+C` でプレビュヌを終了し、次のコマンドで怜蚌したす。 +MkDocs が衚瀺するアドレス通垞は `http://127.0.0.1:8000`を開きたす。 +`Ctrl+C` でプレビュヌを止めおから、次のコマンドで怜蚌したす。 ```shell make ci-test-docs ``` -英語ず日本語の出力は、Git 管理察象倖の `site/` に生成されたす。 -`page.md` ず `page.ja.md` は䞀緒に線集しおください。ペヌゞを远加するずきは -`mkdocs.yml` のナビゲヌションず日本語のラベルも远加したす。 -ドキュメント同士は盞察リンクにしたす。README は導入、このペヌゞは手順、 -゚ンゞニアリングガむドは蚭蚈理由を担圓したす。 - - +ビルドは strict モヌドで実行され、英語ず日本語のペヌゞを Git の管理察象倖である +`site/` に出力したす。`page.md` ず `page.ja.md` は䞀緒に線集したす。他のペヌゞから +リンクされる芋出しには `## 芋出し { #heading }` のように明瀺的な ID を付け、 +䞡蚀語でアンカヌを共有したす。新しいペヌゞは日本語のナビゲヌション名も含めお +`mkdocs.yml` に远加し、ドキュメント間のリンクには盞察パスを䜿いたす。導入手順は +README、䜜業手順はこのガむド、蚭蚈理由ぱンゞニアリングガむドに眮きたす。 -## トラブルシュヌティング +## トラブルシュヌティング { #troubleshooting } -怜蚌を無効にしたり、耇数のツヌルを䞀床に倉曎したりせず、最初に倱敗した -コマンドから原因を確認したす。 +怜蚌を無効化したり、耇数のツヌルを同時に倉曎したりせず、最初に倱敗したコマンドから +修正したす。 -| 症状 | 確認するこず・次の行動 | +| 症状 | 確認するこず / 次の察応 | | --- | --- | -| `uv: command not found` | 指定バヌゞョンの uv を導入し、`PATH` ず `uv --version` を確認する | -| Python のバヌゞョンが合わない | 䟋ずしお `uv sync --locked --group dev --python 3.13` で察応 interpreter を遞ぶ | -| Lint・CI で `actionlint: command not found` | actionlint を導入しお `actionlint --version` を確認する。察象の確認には `make test` を䜿える | -| lock の曎新が必芁ず衚瀺される | 意図した䟝存倉曎なら `uv lock` で䞡方を曎新する。それ以倖は `--locked` を倖さず宣蚀ず lock のずれを調べる | -| `Invalid configuration`・終了コヌド 2 | `PROJECT_*` ず䜜業ディレクトリの `.env` を確認し、倧文字の蚱容ログレベルを䜿う | -| 察象テストだけの実行でカバレッゞ䞍足 | 察象を絞る間は `--no-cov`、実際の品質ゲヌトには `make test` を䜿う | -| Docker daemon に接続できない | Docker を起動しお再実行する。Python だけの䜜業ならコンテナ環境は䞍芁 | -| ペヌゞが衚瀺されない・リンクが切れおいる | `mkdocs.yml`、英日のファむル、リンク先を確認し、䞡蚀語を再ビルドする | - -盞談するずきは、実行コマンド、最初の゚ラヌ、OS、Python・uv のバヌゞョン、 -関連する倉曎を添えたす。秘密情報や非公開の蚭定は䌏せおください。 +| `uv: command not found` | uv 0.13.x をむンストヌルし、`PATH` に含たれるこずを `uv --version` で確認 | +| `Required uv version ... does not match` | `[tool.uv] required-version` の範囲に uv を合わせる䟋: `uv self update` | +| Python のバヌゞョンが合わない | サポヌト察象のむンタヌプリタヌを指定。䟋: `uv sync --locked --group dev --python 3.13` | +| ロックファむルの曎新が必芁ず衚瀺される | 意図した䟝存関係の倉曎なら `uv lock` を実行しお䞡方を確認。そうでなければ `--locked` を倖さず差異の原因を調べる | +| `make audit` が脆匱性を報告する | パッケヌゞを曎新`uv lock --upgrade-package `しお再実行。ID を無芖する堎合は理由を蚘録する | +| `make test-lowest` が倱敗する | `pyproject.toml` の䞋限を動䜜する最初のバヌゞョンぞ䞊げおから `uv lock` | +| `Invalid configuration` / 終了コヌド 2 | `PROJECT_*` 倉数ず䜜業ディレクトリの `.env` を確認し、指定できる倀を䜿う | +| 䞀郚のテストでカバレッゞ基準を満たせない | その堎合は `--no-cov` を䜿い、正匏なカバレッゞ刀定は `make test` で行う | +| Docker デヌモンに接続できない | Docker を起動しおから再実行。コンテナずスキャンのタヌゲット以倖では䞍芁 | +| 新しいペヌゞが衚瀺されない、たたはリンク切れになる | `mkdocs.yml`、䞡蚀語のファむル、芋出し ID、リンク先を確認しお再ビルド | + +問い合わせる際は、実行したコマンド、最初の゚ラヌ、OS、Python ず uv のバヌゞョン、 +関連する倉曎を蚘茉しおください。シヌクレットや非公開の蚭定は䌏せおください。 diff --git a/docs/development.md b/docs/development.md index 85422d6..515f5d5 100644 --- a/docs/development.md +++ b/docs/development.md @@ -5,15 +5,14 @@ For module boundaries and design decisions, use the [engineering guide](architecture/README.md). All commands below run from the repository root. -## Set up once +## Set up once { #set-up-once } | Tool | When it is needed | | --- | --- | -| Python 3.12+ | Package and tests; prefer 3.13 to match the full CI job | -| [uv 0.12.19](https://docs.astral.sh/uv/getting-started/installation/) | Locked dependencies and command execution | +| [uv 0.13.x](https://docs.astral.sh/uv/getting-started/installation/) | Always; `pyproject.toml` requires `>=0.13.1,<0.14` | +| Python 3.12–3.15 | Installed by uv on demand; `.python-version` selects 3.14 | | GNU Make | The repository's standard commands | -| [actionlint](https://github.com/rhysd/actionlint/blob/main/docs/install.md) | `make lint` and `make ci-test`; CI uses 1.7.12 | -| Docker with a running daemon | Container commands only | +| Docker with a running daemon | Container checks, `secret-scan`, and `security-scan` only | ```shell make install-deps-dev @@ -22,20 +21,22 @@ make test ``` `uv` manages `.venv`; do not install project dependencies with a separate -`pip install` command. No activation is needed. The actionlint reminder printed -by `make install-deps-dev` does not prevent Python tests from running. +`pip install` command. No activation is needed. Ruff, mypy, actionlint, zizmor, +and pip-audit are installed from `uv.lock`, so no global tools are required. -Optional Git hooks: +Install the Git hooks once: ```shell make install-hooks make hooks-check ``` -Hooks check YAML, whitespace, and Ruff rules. They do not replace type checking, -tests, or the full quality gate. +Hooks run file hygiene checks, private-key and secret detection (gitleaks), +`uv lock --check`, Ruff, mypy, actionlint, and zizmor. Python tools run through +`uv run --locked`, so hooks and CI use identical versions. Hooks do not run the +tests; use `make ci-test` before pushing. -## Everyday development +## Everyday development { #everyday-development } 1. Find the module and its test using the [change map](architecture/README.md#where-to-change-code). @@ -54,37 +55,46 @@ make ci-test `--no-cov` is only for the focused feedback loop: measuring a subset against the whole-package coverage threshold would give misleading failures. `make test` -and `make ci-test` run all tests with branch coverage and enforce **95%** minimum -coverage. Do not lower the threshold to make a focused run pass. +and `make ci-test` run all tests with branch coverage and enforce at least +**95%** (the template itself is at 100%). pytest runs in strict mode and turns +warnings into errors. Do not lower the threshold or ignore warnings broadly to +make a run pass. -### Checks by change type +Tests run in an isolated temporary working directory with `PROJECT_*` variables +removed (see `tests/conftest.py`), so your shell and `.env` do not affect results. + +### Checks by change type { #checks-by-change-type } | Changed area | Focused feedback | Before submitting | | --- | --- | --- | | Python behavior or tests | Selected tests with `--no-cov`, `make fix`, `make type-check` | `make ci-test` | -| Dependencies or package metadata | `uv lock`, `make package-check` | `make ci-test`; docs/container checks if affected | +| Dependencies or package metadata | `uv lock`, `make package-check` | `make ci-test`, `make audit`, `make test-lowest` | | Markdown or `mkdocs.yml` | `make docs-serve` | `make ci-test-docs` | -| GitHub workflows | `actionlint` | `make ci-test` and the checks invoked by the changed workflow | +| GitHub workflows or Dependabot | `uv run --locked actionlint`, `uv run --locked zizmor --offline .` | `make ci-test` | | Dockerfile or Compose | `make docker-build docker-run` | `make ci-test-docker`; `docker compose up --build` if Compose changed | -Run the union of checks when a change spans areas. Documentation-only changes -do not need the Python or container gate. - -Useful commands, without a new workflow to memorize: +Run the union of checks when a change spans areas. | Command | What it does | | --- | --- | | `make help` | Lists supported Make targets | | `make fix` | Applies Ruff fixes and formatting; review any automatic changes | -| `make lint` | Runs strict mypy, Ruff, actionlint, and offline zizmor | +| `make lint` | Runs strict mypy, Ruff (including security rules), actionlint, and zizmor | | `make test` | Runs pytest with branch coverage | -| `make package-check` | Builds wheel and sdist, then tests the wheel's installed CLI and module entry point in isolation | +| `make test-lowest` | Runs tests on Python 3.12 with the lowest declared direct dependency versions | +| `make package-check` | Builds wheel and sdist, then tests the installed CLI, module entry point, and `--version` | | `make ci-test` | Syncs dev dependencies and runs formatting, lint, tests, and package checks | +| `make audit` | Audits every locked dependency with pip-audit (hash-verified, no pip) | +| `make secret-scan` | Scans the full Git history with gitleaks (Docker) | +| `make security-scan` | Scans files with Trivy for vulnerable dependencies, misconfigurations, and secrets (Docker) | +| `make ci-test-security` | Runs `audit`, `secret-scan`, and `security-scan` | -CI runs the full gate and docs build on Python 3.13, compatibility tests on -3.12 and 3.14, and hooks in a separate job. +CI runs the full gate and docs build on Python 3.14, compatibility tests on +3.12, 3.13, and 3.15, the lowest-dependency job, and hooks in separate jobs. +Security scans run on pull requests and weekly; see +[security and supply chain](architecture/README.md#security-and-supply-chain). -## Configuration +## Configuration { #configuration } The CLI works with defaults. Copy `.env.template` to `.env` only if local overrides are needed: @@ -97,35 +107,51 @@ cp .env.template .env | --- | --- | --- | | `PROJECT_NAME` | `template-python` | String used in the debug startup log; does not change the greeting | | `PROJECT_LOG_LEVEL` | `INFO` | `DEBUG`, `INFO`, `WARNING`, `ERROR`, or `CRITICAL` | +| `PROJECT_LOG_FORMAT` | `text` | `text` for humans, `json` for one JSON object per line (log collectors, containers) | Environment variables override `.env`, which overrides defaults. -Names are case-insensitive; log-level **values** must use the uppercase choices -above. `.env` is read from the working directory and unknown entries are +Names are case-insensitive; **values** must use the exact choices above. `.env` +is read from the working directory and unknown `PROJECT_*` entries are rejected, so do not reuse an unrelated application's `.env`. Keep local settings and secrets out of version control. ```shell PROJECT_LOG_LEVEL=DEBUG uv run --locked template-python hello --name Ada -uv run --locked template-python --verbose hello --name Ada +PROJECT_LOG_FORMAT=json uv run --locked template-python --verbose hello +uv run --locked template-python config ``` -`--verbose` is a global option: place it before `hello`. It selects debug logging -after settings validation; it cannot make an invalid setting valid. -Successful greeting output goes to stdout; logs and validation errors go to -stderr. Blank `--name` values and invalid settings exit with code **2**. +`config` prints the effective settings as JSON. Declare credentials as +`pydantic.SecretStr`; they are printed as `**********`. + +| Global option | Effect | +| --- | --- | +| `--verbose`, `-v` | Selects debug logging after settings validation; it cannot make an invalid setting valid | +| `--version`, `-V` | Prints `template-python ` and exits, even if settings are invalid | +| `--help` | Shows help; running without a command also shows help (exit code 2) | + +Place global options before the command (`template-python --verbose hello`). +Command output goes to stdout; logs and validation errors go to stderr. Blank +`--name` values and invalid settings exit with code **2**. -## Dependencies +## Dependencies { #dependencies } - Edit dependency declarations in `pyproject.toml`, run `uv lock`, and commit both the manifest and `uv.lock`. Do not hand-edit resolved lock entries. +- Keep lower bounds honest: `make test-lowest` installs the lowest allowed direct + dependency versions and must pass. - Use `make update` only for an intentional broad upgrade: it runs - `uv lock --upgrade`, not just a sync of the current versions. + `uv lock --upgrade`, not just a sync of the current versions. Run `make audit` + afterwards. - Keep runtime dependencies in `project.dependencies`; keep development, test, docs, and notebook tools in their existing groups. -- Existing commands select and sync their dependency groups. Docs and notebook - commands use `--no-dev`; a later dev command restores the dev group as needed. +- Dependabot opens grouped weekly updates after a 7-day cooldown for Python + packages, GitHub Actions, pre-commit hooks, Docker base images, and Dev + Container features. Tool images in the `Makefile` and the uv version in the Dev + Container are pinned by hand; update them together with `[tool.uv] + required-version` when raising uv. -## Documentation +## Documentation { #documentation } ```shell make docs-serve @@ -138,27 +164,31 @@ preview with `Ctrl+C`, then validate with: make ci-test-docs ``` -The build writes English and Japanese pages to the ignored `site/` directory. -Edit `page.md` and `page.ja.md` together. Add new pages to `mkdocs.yml`, including -Japanese navigation labels. Use relative links between documentation pages. -Keep onboarding in the README, procedures here, and design rationale in the -engineering guide. +The build runs in strict mode and writes English and Japanese pages to the +ignored `site/` directory. Edit `page.md` and `page.ja.md` together. Give +headings that are linked from other pages an explicit ID such as +`## Heading { #heading }` so both languages share anchors. Add new pages to +`mkdocs.yml`, including Japanese navigation labels. Use relative links between +documentation pages. Keep onboarding in the README, procedures here, and design +rationale in the engineering guide. -## Troubleshooting +## Troubleshooting { #troubleshooting } Fix the first failing command rather than disabling checks or changing several tools at once. | Symptom | What to check / next action | | --- | --- | -| `uv: command not found` | Install the pinned uv version and ensure its directory is on `PATH`; confirm with `uv --version` | +| `uv: command not found` | Install uv 0.13.x and ensure its directory is on `PATH`; confirm with `uv --version` | +| `Required uv version ... does not match` | Upgrade or downgrade uv into the range in `[tool.uv] required-version` (for example `uv self update`) | | Python version is incompatible | Select a supported interpreter, for example `uv sync --locked --group dev --python 3.13` | -| `actionlint: command not found` during lint/CI | Install actionlint and confirm `actionlint --version`; `make test` remains available for focused work | | Lock file needs updating | If dependencies intentionally changed, run `uv lock` and review both files; otherwise investigate manifest/lock drift rather than removing `--locked` | -| `Invalid configuration` / exit code 2 | Check `PROJECT_*` variables and the working directory's `.env`; use an accepted uppercase log level | +| `make audit` reports a vulnerability | Upgrade the package (`uv lock --upgrade-package `) and rerun; only ignore an ID with a documented reason | +| `make test-lowest` fails | Raise the lower bound in `pyproject.toml` to the first version that works, then `uv lock` | +| `Invalid configuration` / exit code 2 | Check `PROJECT_*` variables and the working directory's `.env`; use an accepted value | | A selected test run fails coverage | Use `--no-cov` for the subset, then run `make test` for the actual coverage gate | -| Docker cannot connect to the daemon | Start Docker before retrying; container tooling is not needed for Python-only work | -| A new page is absent or a link is broken | Check `mkdocs.yml`, both language files, and the link target, then rebuild both languages | +| Docker cannot connect to the daemon | Start Docker before retrying; it is only needed for container and scan targets | +| A new page is absent or a link is broken | Check `mkdocs.yml`, both language files, heading IDs, and the link target, then rebuild | When requesting help, include the exact command, first failure, OS, Python/uv versions, and relevant change. Redact secrets and private configuration. diff --git a/docs/index.ja.md b/docs/index.ja.md index 85e1e35..85f661f 100644 --- a/docs/index.ja.md +++ b/docs/index.ja.md @@ -1,13 +1,14 @@ # template-python -Python 3.12+ 向けの、型付きパッケヌゞ兌 CLI テンプレヌトです。たずサンプルを -動かし、その埌は目的に合うガむドだけを読んでください。コンテナ、Notebook、 -デプロむは任意であり、開発を始めるための前提ではありたせん。 +Python 3.12+ 向けの、型付き CLI テンプレヌトです。本番運甚に耐えるツヌルチェヌンを +備えおいたす。たずサンプルを動かし、その埌は目的に合うガむドだけを読んでください。 +コンテナ、Notebook、デプロむは任意であり、開発を始めるための前提ではありたせん。 ## たず動かす -[uv 0.12.19](https://docs.astral.sh/uv/getting-started/installation/)、 -Python 3.12+、GNU Make を甚意し、リポゞトリのルヌトで実行したす。 +[uv 0.13.x](https://docs.astral.sh/uv/getting-started/installation/) ず GNU Make を +甚意したす。Python 3.14 は `.python-version` に埓っお uv が導入したす。 +リポゞトリのルヌトで実行したす。 ```shell make install-deps-dev @@ -21,8 +22,9 @@ CLI の出力は次のずおりです。 Hello, Ada! ``` -`.env` の䜜成や仮想環境の有効化は䞍芁です。CI の党怜蚌は Python 3.13 で行い、 -3.12 ず 3.14 でも互換性テストを実行したす。 +`.env` の䜜成や仮想環境の有効化は䞍芁です。CI の党怜蚌は Python 3.14 で行い、 +3.12・3.13・3.15 でも互換性テストを実行したす。宣蚀した䟝存関係の䞋限バヌゞョンも +3.12 でテストしたす。 ## 目的から探す @@ -31,6 +33,7 @@ Hello, Ada! | 開発環境を甚意し、コヌドを倉曎・怜蚌する | [開発ガむド](development.ja.md) | | 倉曎するモゞュヌルや蚭蚈方針を刀断する | [アヌキテクチャず゚ンゞニアリング](architecture/README.ja.md) | | 環境倉数を蚭定し、CLI の゚ラヌを理解する | [蚭定リファレンス](development.ja.md#configuration) | +| スキャナヌ、CI、リリヌスの仕組みを理解する | [セキュリティずサプラむチェヌン](architecture/README.ja.md#security-and-supply-chain) | | コマンドの倱敗から埩旧する | [トラブルシュヌティング](development.ja.md#troubleshooting) | | テンプレヌトを改名し、䞍芁な機胜を倖す | [テンプレヌトの導入](architecture/README.ja.md#adopting-the-template) | | ドキュメントをプレビュヌ・怜蚌する | [ドキュメントの開発](development.ja.md#documentation) | @@ -39,7 +42,8 @@ Hello, Ada! - 再利甚する凊理はコアぞ、蚭定・ログ・衚瀺・終了コヌドは CLI ぞ眮きたす。 - コマンドの正本は `Makefile` です。ツヌルのオプションを芚える代わりに - `make help` を䜿いたす。 -- 䟝存関係の宣蚀は `pyproject.toml`、解決枈みバヌゞョンは `uv.lock` が管理したす。 + `make help` を䜿いたす。CI も同じタヌゲットを呌びたす。 +- 䟝存関係ずツヌル蚭定の宣蚀は `pyproject.toml`、解決枈みバヌゞョンは `uv.lock` が + 管理したす。Git フックが䜿うリンタヌも `uv.lock` に含たれたす。 - 英語ず日本語のペヌゞを揃えたす。詳现な手順を README に重耇させず、 該圓するガむドぞリンクしたす。 diff --git a/docs/index.md b/docs/index.md index c391fd2..eb02267 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,13 +1,14 @@ # template-python -A typed Python 3.12+ package and CLI template. Start with the runnable example, -then read only the guide needed for your task. Containers, notebooks, and -deployment are optional, not prerequisites. +A typed Python 3.12+ CLI template with a production-grade toolchain. Start with +the runnable example, then read only the guide needed for your task. Containers, +notebooks, and deployment are optional, not prerequisites. ## Run the example -Install [uv 0.12.19](https://docs.astral.sh/uv/getting-started/installation/), -Python 3.12+, and GNU Make. Run these commands from the repository root: +Install [uv 0.13.x](https://docs.astral.sh/uv/getting-started/installation/) +and GNU Make. uv installs Python 3.14 from `.python-version`. Run these commands +from the repository root: ```shell make install-deps-dev @@ -21,8 +22,9 @@ The CLI prints: Hello, Ada! ``` -No `.env` file or virtual environment activation is required. Python 3.13 is the -full CI baseline; 3.12 and 3.14 also have compatibility tests. +No `.env` file or virtual environment activation is required. Python 3.14 is the +full CI baseline; 3.12, 3.13, and 3.15 also have compatibility tests, and the +lowest declared dependency versions are tested on 3.12. ## Find the next step @@ -31,6 +33,7 @@ full CI baseline; 3.12 and 3.14 also have compatibility tests. | Set up development, change code, and select checks | [Development](development.md) | | Find the right module or make a design decision | [Architecture and engineering](architecture/README.md) | | Set environment variables or understand CLI errors | [Configuration](development.md#configuration) | +| Understand scanners, CI, and releases | [Security and supply chain](architecture/README.md#security-and-supply-chain) | | Recover from a failed command | [Troubleshooting](development.md#troubleshooting) | | Rename the template or remove unused assets | [Adopting the template](architecture/README.md#adopting-the-template) | | Preview and validate these pages | [Documentation workflow](development.md#documentation) | @@ -40,7 +43,8 @@ full CI baseline; 3.12 and 3.14 also have compatibility tests. - Reusable behavior belongs in the core; the CLI handles settings, logging, output, and exit codes. - `Makefile` is the command reference; use `make help` rather than memorizing - tool flags. -- `pyproject.toml` declares dependencies; `uv.lock` records resolved versions. + tool flags. CI calls the same targets. +- `pyproject.toml` declares dependencies and tool settings; `uv.lock` records + resolved versions, including the linters used by Git hooks. - Keep English and Japanese pages aligned. Do not duplicate detailed procedures in the README; link to the relevant guide instead. diff --git a/mkdocs.yml b/mkdocs.yml index 682ca33..9e62ba1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -27,6 +27,14 @@ theme: name: Switch to system preference icon: repo: fontawesome/brands/github +markdown_extensions: + - attr_list + - admonition + - tables + - toc: + permalink: true +validation: + anchors: warn plugins: - i18n: docs_structure: suffix