Compare commits
253 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bc34b601be | |||
| 50a36ff93a | |||
| cf63fcb5b5 | |||
| 3abe80523a | |||
| 6576880b0e | |||
| a2f1e06224 | |||
| f6c926f1f5 | |||
| 13895bb04a | |||
| 30c82079b9 | |||
| 0e70a1b524 | |||
| bb55fa9566 | |||
| 02ea1cc72a | |||
|
|
8f72f04d5c | ||
| 29e0f012cd | |||
| 2148714584 | |||
| 308c8e3dad | |||
| eaa899cfc6 | |||
| 46f0002178 | |||
| 44798a6429 | |||
| e0057b46e4 | |||
| 92b54e7ba9 | |||
| fcac8e5a72 | |||
| 40bd929820 | |||
| a36a18aaa6 | |||
| e0064de83d | |||
| f588638331 | |||
| ecded30073 | |||
| 1ce381cb6e | |||
| e703d25efe | |||
| 4d1f4af25b | |||
| f6febb1f77 | |||
| 4e25180b0a | |||
| c00d7db507 | |||
|
|
753fa9cb63 | ||
|
|
c09bd5b612 | ||
| 35ae171783 | |||
| 57fd88b269 | |||
| 08a1cb164a | |||
| d02bf062af | |||
| ee51bcafb4 | |||
| 2fae0f7161 | |||
| 1897eb1c5b | |||
| e222d47d45 | |||
| 3645098bf1 | |||
|
|
96dbbb3cbe | ||
| 815a0cc6e6 | |||
| a33fa47b80 | |||
|
|
12314857d8 | ||
| 4d2bc9975f | |||
| 4e117dc921 | |||
| 6e90c4c363 | |||
| dc69b8c68b | |||
| 947e4310c3 | |||
| bc8ceb502b | |||
| a4a30aad44 | |||
| d0b5423135 | |||
| dc0916a548 | |||
| 3c7967e445 | |||
| fbc1f7720e | |||
| 4133785119 | |||
| 145df76d06 | |||
| bb7efa850a | |||
| f83be3bf37 | |||
| 40d9a1ef9e | |||
| 292d354902 | |||
| eceb2b99ce | |||
| 678f26b0e7 | |||
| ad7a0ed105 | |||
| be54cc3411 | |||
| b87f62e0f5 | |||
| 8bc19fa460 | |||
| 0108b68769 | |||
| 6f0d80ca1e | |||
| 39b042e638 | |||
| 24e5490259 | |||
| 074887cd57 | |||
| 9fb5442ccd | |||
| f16e1c81f1 | |||
| a2c61b625d | |||
| 2c0917b266 | |||
| fabca04771 | |||
| f84f5f02b3 | |||
| 4aa0872949 | |||
| 2d55303213 | |||
| 55563afc7e | |||
| 9564435b11 | |||
| 7fed166c18 | |||
| f6e392b80c | |||
| 5096223b48 | |||
| 2ee53fe375 | |||
| 8d634861f6 | |||
| a529d60f60 | |||
| 14ca0160ba | |||
| f4a24595b1 | |||
| 817acc5e5e | |||
| 4d76fd5de5 | |||
| 495e45d01d | |||
| 718e0a0043 | |||
| cfb6d7a7aa | |||
| c9eb188e05 | |||
| 4a37ffcdc2 | |||
| f9d9e00057 | |||
| 005e2a03ed | |||
| 72b27b7fd2 | |||
|
|
34fa2ef28a | ||
|
|
88eabc3de6 | ||
| 7d94b9073a | |||
| 86317315ed | |||
| 0e62ad5596 | |||
| 225b0e7008 | |||
| e6a6a6042e | |||
| 0ceafc374d | |||
| a9ef02a602 | |||
| e92805409e | |||
| c88b6d773c | |||
| fb4bf5a7a3 | |||
| 30f39ae050 | |||
| fb32cc07c4 | |||
| 50f8c2a33f | |||
| db8fd946ae | |||
|
|
58fe4f0073 | ||
| 54841dbf70 | |||
| d6ad8e8e59 | |||
| 21177ff47f | |||
| 1425bf1f5c | |||
| 353e2785c3 | |||
| 53a7374ac1 | |||
| 51a878cddb | |||
| deedeecef9 | |||
| 71c1c453d6 | |||
| 4f5a963ef6 | |||
| 1d62653871 | |||
| 55abb17f50 | |||
|
|
bdfcb4b677 | ||
| d26a6ae3b2 | |||
| 12b2786ca2 | |||
| bca4c2bede | |||
| c8da243663 | |||
| 3a2913ba1f | |||
| 24f3f9b24a | |||
|
|
a72a2c2bd4 | ||
| 9bafe85b2b | |||
| b4472c7849 | |||
| 4dab6d11bb | |||
| 37b8a21524 | |||
| fe0e913963 | |||
| 54b1cee950 | |||
| 1c0ee099fb | |||
| d7af004842 | |||
| 8fccbda573 | |||
| 1631e11137 | |||
| 0a98f76068 | |||
| 65bc21b162 | |||
| 7a42aeb77c | |||
| 5f38779d52 | |||
| fd9e1ac93b | |||
| e60e3d5fc7 | |||
| f283f9453d | |||
| 50dfdba4e6 | |||
| dd1cf4f198 | |||
| 68f845e773 | |||
| 7f6bbdc82c | |||
| 5ec2411e20 | |||
| 3ecd888537 | |||
| 352b95c141 | |||
| 99f78c8745 | |||
| fb1e8ff672 | |||
| 2c483cefff | |||
| 519175c672 | |||
| 30ed018fd8 | |||
| 7c1cd11e45 | |||
| 6b690eb033 | |||
| be30668eef | |||
|
|
8c2f035e6d | ||
| 04b44b350b | |||
|
|
f2514a6f02 | ||
| 9d85c97b9b | |||
| 0e93cc08b4 | |||
| 223b134776 | |||
| ccaef4c1a7 | |||
| 08c698e833 | |||
| 4ca0630d76 | |||
| d7c3c687f4 | |||
| 405dab8b59 | |||
| cd5b6b63f7 | |||
| 2d2d495f95 | |||
| fca3010042 | |||
| 22a417ac3c | |||
| f61bb4f2e7 | |||
| b5551e227e | |||
| ab834b641a | |||
| db6d8af8b1 | |||
| 61fcd5d70a | |||
| 6455d93cb3 | |||
| 6e60287e99 | |||
| 8d80a4a3a5 | |||
| a18ec9d958 | |||
| 138e23d525 | |||
| dc5bffdd97 | |||
| c06eccc61c | |||
| 94c937d588 | |||
| c86b5d7772 | |||
| 4fc0192731 | |||
| e02305e72d | |||
| b08b1a833f | |||
| a75f28e073 | |||
| 40556e5a2d | |||
| 5757df115d | |||
| 22fc615a28 | |||
| 07f52e9488 | |||
| e04455c911 | |||
| d3235c5ca9 | |||
| 22b77ac141 | |||
| ec63d560f3 | |||
| 2eb28301e4 | |||
| 0366a0346b | |||
| 936d29bbe1 | |||
| 3c894e659d | |||
| f59f8859dc | |||
| 84eda0301f | |||
| efae404d1e | |||
| fc34a7da5b | |||
| 1fd8aae8f6 | |||
| e85c71e73f | |||
| d3d67272a7 | |||
| 59f3422d3e | |||
| 18fe172a54 | |||
| a059d81314 | |||
| 54213ab810 | |||
|
|
370a3574b2 | ||
| 0eaf8680fd | |||
| f42fa2d558 | |||
| c7e5af6d51 | |||
|
|
facb803010 | ||
| f9397b7fa0 | |||
| 5597e02467 | |||
| 0f1143a5bd | |||
| 6cab5091ea | |||
| 3c819cf16e | |||
| 6e06efa6d0 | |||
| 64200a55c5 | |||
| 464e3222d2 | |||
| afb184fefc | |||
| 5de2ed9f96 | |||
| 306f580bdb | |||
| 75f9ba4943 | |||
| b1e2811077 | |||
| 08d57ef4d4 | |||
| 1e67975acb | |||
| baee7ae54b | |||
| a18a424866 | |||
| cfbf4cadbd | |||
|
|
2b7b21dc9b |
414 changed files with 12303 additions and 5148 deletions
4
.dagger/.gitignore
vendored
4
.dagger/.gitignore
vendored
|
|
@ -1,4 +0,0 @@
|
|||
/.venv
|
||||
/**/__pycache__
|
||||
/sdk
|
||||
/.env
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
"""BlumeOps CI — Dagger build functions for container images."""
|
||||
|
||||
from .main import BlumeopsCi as BlumeopsCi
|
||||
768
.dagger/uv.lock
generated
768
.dagger/uv.lock
generated
|
|
@ -1,768 +0,0 @@
|
|||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.12.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
version = "25.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backoff"
|
||||
version = "2.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001, upload-time = "2022-10-05T19:19:32.061Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "beartype"
|
||||
version = "0.22.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866, upload-time = "2025-12-13T06:50:30.72Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blumeops-ci"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "dagger-io" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "dagger-io", editable = "sdk" }]
|
||||
|
||||
[[package]]
|
||||
name = "cattrs"
|
||||
version = "25.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "attrs" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6e/00/2432bb2d445b39b5407f0a90e01b9a271475eea7caf913d7a86bcb956385/cattrs-25.3.0.tar.gz", hash = "sha256:1ac88d9e5eda10436c4517e390a4142d88638fe682c436c93db7ce4a277b884a", size = 509321, upload-time = "2025-10-07T12:26:08.737Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/2b/a40e1488fdfa02d3f9a653a61a5935ea08b3c2225ee818db6a76c7ba9695/cattrs-25.3.0-py3-none-any.whl", hash = "sha256:9896e84e0a5bf723bc7b4b68f4481785367ce07a8a02e7e9ee6eb2819bc306ff", size = 70738, upload-time = "2025-10-07T12:26:06.603Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2026.1.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dagger-io"
|
||||
version = "0.0.0"
|
||||
source = { editable = "sdk" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "beartype" },
|
||||
{ name = "cattrs" },
|
||||
{ name = "exceptiongroup" },
|
||||
{ name = "gql", extra = ["httpx"] },
|
||||
{ name = "httpcore" },
|
||||
{ name = "opentelemetry-exporter-otlp-proto-http" },
|
||||
{ name = "opentelemetry-instrumentation-logging" },
|
||||
{ name = "opentelemetry-sdk" },
|
||||
{ name = "platformdirs" },
|
||||
{ name = "rich" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "anyio", specifier = ">=3.6.2" },
|
||||
{ name = "beartype", specifier = ">=0.22.0" },
|
||||
{ name = "cattrs", specifier = ">=25.1.0" },
|
||||
{ name = "exceptiongroup", specifier = ">=1.3.0" },
|
||||
{ name = "gql", extras = ["httpx"], specifier = ">=4.0" },
|
||||
{ name = "httpcore", specifier = ">=1.0.8" },
|
||||
{ name = "opentelemetry-exporter-otlp-proto-http", specifier = ">=1.23.0" },
|
||||
{ name = "opentelemetry-instrumentation-logging", specifier = ">=0.54b1" },
|
||||
{ name = "opentelemetry-sdk", specifier = ">=1.23.0" },
|
||||
{ name = "platformdirs", specifier = ">=2.6.2" },
|
||||
{ name = "rich", specifier = ">=10.11.0" },
|
||||
{ name = "typing-extensions", specifier = ">=4.13.0" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "aiohttp", specifier = ">=3.9.3" },
|
||||
{ name = "codegen", editable = "sdk/codegen" },
|
||||
{ name = "mypy", specifier = ">=1.8.0" },
|
||||
{ name = "pytest", specifier = ">=8.0.2" },
|
||||
{ name = "pytest-httpx", specifier = ">=0.30.0" },
|
||||
{ name = "pytest-mock", specifier = ">=3.12.0" },
|
||||
{ name = "pytest-subprocess", specifier = ">=1.5.0" },
|
||||
{ name = "ruff", specifier = ">=0.3.4" },
|
||||
{ name = "sphinx", specifier = ">=7.2.6" },
|
||||
{ name = "sphinx-rtd-theme", specifier = ">=2.0.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "googleapis-common-protos"
|
||||
version = "1.72.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "protobuf" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gql"
|
||||
version = "4.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "backoff" },
|
||||
{ name = "graphql-core" },
|
||||
{ name = "yarl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/9f/cf224a88ed71eb223b7aa0b9ff0aa10d7ecc9a4acdca2279eb046c26d5dc/gql-4.0.0.tar.gz", hash = "sha256:f22980844eb6a7c0266ffc70f111b9c7e7c7c13da38c3b439afc7eab3d7c9c8e", size = 215644, upload-time = "2025-08-17T14:32:35.397Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/94/30bbd09e8d45339fa77a48f5778d74d47e9242c11b3cd1093b3d994770a5/gql-4.0.0-py3-none-any.whl", hash = "sha256:f3beed7c531218eb24d97cb7df031b4a84fdb462f4a2beb86e2633d395937479", size = 89900, upload-time = "2025-08-17T14:32:34.029Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
httpx = [
|
||||
{ name = "httpx" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "graphql-core"
|
||||
version = "3.2.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ac/9b/037a640a2983b09aed4a823f9cf1729e6d780b0671f854efa4727a7affbe/graphql_core-3.2.7.tar.gz", hash = "sha256:27b6904bdd3b43f2a0556dad5d579bdfdeab1f38e8e8788e555bdcb586a6f62c", size = 513484, upload-time = "2025-11-01T22:30:40.436Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/14/933037032608787fb92e365883ad6a741c235e0ff992865ec5d904a38f1e/graphql_core-3.2.7-py3-none-any.whl", hash = "sha256:17fc8f3ca4a42913d8e24d9ac9f08deddf0a0b2483076575757f6c412ead2ec0", size = 207262, upload-time = "2025-11-01T22:30:38.912Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.28.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "certifi" },
|
||||
{ name = "httpcore" },
|
||||
{ name = "idna" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.11"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "importlib-metadata"
|
||||
version = "8.7.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "zipp" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "4.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mdurl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multidict"
|
||||
version = "6.7.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-api"
|
||||
version = "1.39.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "importlib-metadata" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-exporter-otlp-proto-common"
|
||||
version = "1.39.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "opentelemetry-proto" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e9/9d/22d241b66f7bbde88a3bfa6847a351d2c46b84de23e71222c6aae25c7050/opentelemetry_exporter_otlp_proto_common-1.39.1.tar.gz", hash = "sha256:763370d4737a59741c89a67b50f9e39271639ee4afc999dadfe768541c027464", size = 20409, upload-time = "2025-12-11T13:32:40.885Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/02/ffc3e143d89a27ac21fd557365b98bd0653b98de8a101151d5805b5d4c33/opentelemetry_exporter_otlp_proto_common-1.39.1-py3-none-any.whl", hash = "sha256:08f8a5862d64cc3435105686d0216c1365dc5701f86844a8cd56597d0c764fde", size = 18366, upload-time = "2025-12-11T13:32:20.2Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-exporter-otlp-proto-http"
|
||||
version = "1.39.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "googleapis-common-protos" },
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "opentelemetry-exporter-otlp-proto-common" },
|
||||
{ name = "opentelemetry-proto" },
|
||||
{ name = "opentelemetry-sdk" },
|
||||
{ name = "requests" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/80/04/2a08fa9c0214ae38880df01e8bfae12b067ec0793446578575e5080d6545/opentelemetry_exporter_otlp_proto_http-1.39.1.tar.gz", hash = "sha256:31bdab9745c709ce90a49a0624c2bd445d31a28ba34275951a6a362d16a0b9cb", size = 17288, upload-time = "2025-12-11T13:32:42.029Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/95/f1/b27d3e2e003cd9a3592c43d099d2ed8d0a947c15281bf8463a256db0b46c/opentelemetry_exporter_otlp_proto_http-1.39.1-py3-none-any.whl", hash = "sha256:d9f5207183dd752a412c4cd564ca8875ececba13be6e9c6c370ffb752fd59985", size = 19641, upload-time = "2025-12-11T13:32:22.248Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-instrumentation"
|
||||
version = "0.60b1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "opentelemetry-semantic-conventions" },
|
||||
{ name = "packaging" },
|
||||
{ name = "wrapt" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/41/0f/7e6b713ac117c1f5e4e3300748af699b9902a2e5e34c9cf443dde25a01fa/opentelemetry_instrumentation-0.60b1.tar.gz", hash = "sha256:57ddc7974c6eb35865af0426d1a17132b88b2ed8586897fee187fd5b8944bd6a", size = 31706, upload-time = "2025-12-11T13:36:42.515Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/77/d2/6788e83c5c86a2690101681aeef27eeb2a6bf22df52d3f263a22cee20915/opentelemetry_instrumentation-0.60b1-py3-none-any.whl", hash = "sha256:04480db952b48fb1ed0073f822f0ee26012b7be7c3eac1a3793122737c78632d", size = 33096, upload-time = "2025-12-11T13:35:33.067Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-instrumentation-logging"
|
||||
version = "0.60b1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "opentelemetry-instrumentation" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/60/a6/4515895b383113677fd2ad21813df5e56108a2df14ebb7916c962c9a0234/opentelemetry_instrumentation_logging-0.60b1.tar.gz", hash = "sha256:98f4b9c7aeb9314a30feee7c002c7ea9abea07c90df5f97fb058b850bc45b89a", size = 9968, upload-time = "2025-12-11T13:37:03.974Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/f9/8a4ce3901bc52277794e4b18c4ac43dc5929806eff01d22812364132f45f/opentelemetry_instrumentation_logging-0.60b1-py3-none-any.whl", hash = "sha256:f2e18cbc7e1dd3628c80e30d243897fdc93c5b7e0c8ae60abd2b9b6a99f82343", size = 12577, upload-time = "2025-12-11T13:36:08.123Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-proto"
|
||||
version = "1.39.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "protobuf" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/49/1d/f25d76d8260c156c40c97c9ed4511ec0f9ce353f8108ca6e7561f82a06b2/opentelemetry_proto-1.39.1.tar.gz", hash = "sha256:6c8e05144fc0d3ed4d22c2289c6b126e03bcd0e6a7da0f16cedd2e1c2772e2c8", size = 46152, upload-time = "2025-12-11T13:32:48.681Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/51/95/b40c96a7b5203005a0b03d8ce8cd212ff23f1793d5ba289c87a097571b18/opentelemetry_proto-1.39.1-py3-none-any.whl", hash = "sha256:22cdc78efd3b3765d09e68bfbd010d4fc254c9818afd0b6b423387d9dee46007", size = 72535, upload-time = "2025-12-11T13:32:33.866Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-sdk"
|
||||
version = "1.39.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "opentelemetry-semantic-conventions" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/eb/fb/c76080c9ba07e1e8235d24cdcc4d125ef7aa3edf23eb4e497c2e50889adc/opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6", size = 171460, upload-time = "2025-12-11T13:32:49.369Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c", size = 132565, upload-time = "2025-12-11T13:32:35.069Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-semantic-conventions"
|
||||
version = "0.60b1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/91/df/553f93ed38bf22f4b999d9be9c185adb558982214f33eae539d3b5cd0858/opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953", size = 137935, upload-time = "2025-12-11T13:32:50.487Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982, upload-time = "2025-12-11T13:32:36.955Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "26.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.5.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "propcache"
|
||||
version = "0.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "protobuf"
|
||||
version = "6.33.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ba/25/7c72c307aafc96fa87062aa6291d9f7c94836e43214d43722e86037aac02/protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c", size = 444465, upload-time = "2026-01-29T21:51:33.494Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/79/af92d0a8369732b027e6d6084251dd8e782c685c72da161bd4a2e00fbabb/protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b", size = 425769, upload-time = "2026-01-29T21:51:21.751Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/75/bb9bc917d10e9ee13dee8607eb9ab963b7cf8be607c46e7862c748aa2af7/protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c", size = 437118, upload-time = "2026-01-29T21:51:24.022Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/6b/e48dfc1191bc5b52950246275bf4089773e91cb5ba3592621723cdddca62/protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5", size = 427766, upload-time = "2026-01-29T21:51:25.413Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/b1/c79468184310de09d75095ed1314b839eb2f72df71097db9d1404a1b2717/protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190", size = 324638, upload-time = "2026-01-29T21:51:26.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/f5/65d838092fd01c44d16037953fd4c2cc851e783de9b8f02b27ec4ffd906f/protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd", size = 339411, upload-time = "2026-01-29T21:51:27.446Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/53/a9443aa3ca9ba8724fdfa02dd1887c1bcd8e89556b715cfbacca6b63dbec/protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0", size = 323465, upload-time = "2026-01-29T21:51:28.925Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "idna" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "14.3.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/74/99/a4cab2acbb884f80e558b0771e97e21e939c5dfb460f488d19df485e8298/rich-14.3.2.tar.gz", hash = "sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8", size = 230143, upload-time = "2026-02-01T16:20:47.908Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/45/615f5babd880b4bd7d405cc0dc348234c5ffb6ed1ea33e152ede08b2072d/rich-14.3.2-py3-none-any.whl", hash = "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69", size = 309963, upload-time = "2026-02-01T16:20:46.078Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.6.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wrapt"
|
||||
version = "1.17.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yarl"
|
||||
version = "1.22.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "multidict" },
|
||||
{ name = "propcache" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zipp"
|
||||
version = "3.23.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" },
|
||||
]
|
||||
|
|
@ -178,10 +178,11 @@ jobs:
|
|||
|
||||
echo "## Documentation"
|
||||
echo ""
|
||||
echo "Download \`$TARBALL\` and configure the quartz container with:"
|
||||
echo "Download \`$TARBALL\` directly, or bump \`docs_version\`"
|
||||
echo "in \`ansible/roles/docs/defaults/main.yml\` and run:"
|
||||
echo ""
|
||||
echo "\`\`\`"
|
||||
echo "DOCS_RELEASE_URL=https://forge.eblu.me/eblume/blumeops/releases/download/$VERSION/$TARBALL"
|
||||
echo "mise run provision-indri -- --tags docs"
|
||||
echo "\`\`\`"
|
||||
} > /tmp/release_body.txt
|
||||
|
||||
|
|
@ -223,18 +224,16 @@ jobs:
|
|||
echo ""
|
||||
echo "Release created successfully!"
|
||||
|
||||
- name: Update docs deployment
|
||||
- name: Bump docs_version in ansible role
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
TARBALL="docs-${VERSION}.tar.gz"
|
||||
DEPLOYMENT_FILE="argocd/manifests/docs/deployment.yaml"
|
||||
RELEASE_URL="https://forge.eblu.me/eblume/blumeops/releases/download/${VERSION}/${TARBALL}"
|
||||
DEFAULTS_FILE="ansible/roles/docs/defaults/main.yml"
|
||||
|
||||
echo "Updating $DEPLOYMENT_FILE with new release URL..."
|
||||
yq -i "(.spec.template.spec.containers[0].env[] | select(.name == \"DOCS_RELEASE_URL\")).value = \"${RELEASE_URL}\"" "$DEPLOYMENT_FILE"
|
||||
echo "Bumping docs_version in $DEFAULTS_FILE to ${VERSION}..."
|
||||
yq -i ".docs_version = \"${VERSION}\"" "$DEFAULTS_FILE"
|
||||
|
||||
echo "Updated deployment:"
|
||||
grep -A1 "DOCS_RELEASE_URL" "$DEPLOYMENT_FILE"
|
||||
echo "Updated defaults:"
|
||||
grep -E "^docs_version:" "$DEFAULTS_FILE"
|
||||
|
||||
- name: Commit release changes
|
||||
env:
|
||||
|
|
@ -248,7 +247,7 @@ jobs:
|
|||
git config user.email "actions@forge.ops.eblu.me"
|
||||
|
||||
# Stage deployment changes
|
||||
git add argocd/manifests/docs/deployment.yaml
|
||||
git add ansible/roles/docs/defaults/main.yml
|
||||
|
||||
# Stage changelog changes if updated
|
||||
if [ "$CHANGELOG_UPDATED" = "true" ]; then
|
||||
|
|
@ -270,34 +269,6 @@ jobs:
|
|||
echo "Changes committed and pushed"
|
||||
fi
|
||||
|
||||
- name: Deploy docs
|
||||
env:
|
||||
ARGOCD_AUTH_TOKEN: ${{ secrets.ARGOCD_AUTH_TOKEN }}
|
||||
run: |
|
||||
echo "Syncing docs app via ArgoCD..."
|
||||
|
||||
# Sync docs app (uses ARGOCD_AUTH_TOKEN env var for auth)
|
||||
argocd app sync docs \
|
||||
--server argocd.ops.eblu.me \
|
||||
--grpc-web \
|
||||
--prune
|
||||
|
||||
# Wait for sync to complete
|
||||
argocd app wait docs \
|
||||
--server argocd.ops.eblu.me \
|
||||
--grpc-web \
|
||||
--timeout 120
|
||||
|
||||
echo "Docs app synced successfully!"
|
||||
|
||||
- name: Purge Fly.io proxy cache
|
||||
env:
|
||||
FLY_API_TOKEN: ${{ secrets.FLY_DEPLOY_TOKEN }}
|
||||
run: |
|
||||
echo "Purging nginx cache on Fly.io proxy..."
|
||||
fly ssh console -a blumeops-proxy -C "sh -c 'rm -rf /tmp/cache && nginx -s reload'"
|
||||
echo "Cache purged"
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
|
|
@ -309,5 +280,12 @@ jobs:
|
|||
echo "Release URL:"
|
||||
echo " https://forge.eblu.me/eblume/blumeops/releases/tag/$VERSION"
|
||||
echo ""
|
||||
echo "Asset URL (for DOCS_RELEASE_URL ConfigMap):"
|
||||
echo "Asset URL:"
|
||||
echo " https://forge.eblu.me/eblume/blumeops/releases/download/$VERSION/$TARBALL"
|
||||
echo ""
|
||||
echo "To deploy on indri, run from gilbert:"
|
||||
echo " mise run provision-indri -- --tags docs"
|
||||
echo ""
|
||||
echo "Then purge the Fly.io proxy cache:"
|
||||
echo " fly ssh console -a blumeops-proxy -C \\"
|
||||
echo " \"sh -c 'rm -rf /tmp/cache && nginx -s reload'\""
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
# Unified container build workflow
|
||||
# Triggers on pushes to main that modify containers/*, or via manual dispatch.
|
||||
# Detects which containers changed and routes to the correct runner:
|
||||
# - Dockerfile containers build on k8s (indri) via Dagger
|
||||
# Manual dispatch only — use `mise run container-build-and-release <name>`.
|
||||
# Shared Dagger helpers (src/blumeops/) make path-based auto-triggers unreliable,
|
||||
# so all container builds are triggered explicitly.
|
||||
# Routes to the correct runner:
|
||||
# - Dockerfile/Dagger containers build on k8s (indri) via Dagger
|
||||
# - Nix containers build on nix-container-builder (ringtail) via nix-build + skopeo
|
||||
name: Build Container
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths: ['containers/**']
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
container:
|
||||
|
|
@ -24,7 +23,7 @@ jobs:
|
|||
detect:
|
||||
runs-on: k8s
|
||||
outputs:
|
||||
dockerfile: ${{ steps.classify.outputs.dockerfile }}
|
||||
dagger: ${{ steps.classify.outputs.dagger }}
|
||||
nix: ${{ steps.classify.outputs.nix }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
|
@ -33,26 +32,19 @@ jobs:
|
|||
ref: ${{ inputs.ref || github.sha }}
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Detect and classify changed containers
|
||||
- name: Classify container build type
|
||||
id: classify
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
CHANGED='["${{ inputs.container }}"]'
|
||||
else
|
||||
CHANGED=$(git diff --name-only HEAD~1 HEAD -- containers/ \
|
||||
| cut -d/ -f2 | sort -u \
|
||||
| jq -R -s -c 'split("\n") | map(select(length > 0))')
|
||||
fi
|
||||
|
||||
echo "Changed containers: $CHANGED"
|
||||
CHANGED='["${{ inputs.container }}"]'
|
||||
echo "Building container: $CHANGED"
|
||||
|
||||
# Classify each container by build type (a container can appear in both)
|
||||
DOCKERFILE='[]'
|
||||
DAGGER='[]'
|
||||
NIX='[]'
|
||||
for name in $(echo "$CHANGED" | jq -r '.[]'); do
|
||||
has_any=false
|
||||
if [ -f "containers/$name/Dockerfile" ]; then
|
||||
DOCKERFILE=$(echo "$DOCKERFILE" | jq -c --arg n "$name" '. + [$n]')
|
||||
if [ -f "containers/$name/container.py" ] || [ -f "containers/$name/Dockerfile" ]; then
|
||||
DAGGER=$(echo "$DAGGER" | jq -c --arg n "$name" '. + [$n]')
|
||||
has_any=true
|
||||
fi
|
||||
if [ -f "containers/$name/default.nix" ]; then
|
||||
|
|
@ -60,22 +52,27 @@ jobs:
|
|||
has_any=true
|
||||
fi
|
||||
if [ "$has_any" = "false" ]; then
|
||||
echo "Warning: $name has neither Dockerfile nor default.nix — skipping"
|
||||
echo "Warning: $name has neither container.py, Dockerfile, nor default.nix — skipping"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "dockerfile=$DOCKERFILE" >> "$GITHUB_OUTPUT"
|
||||
echo "dagger=$DAGGER" >> "$GITHUB_OUTPUT"
|
||||
echo "nix=$NIX" >> "$GITHUB_OUTPUT"
|
||||
echo "Dockerfile builds: $DOCKERFILE"
|
||||
echo "Dagger builds: $DAGGER"
|
||||
echo "Nix builds: $NIX"
|
||||
|
||||
build-dockerfile:
|
||||
build-dagger:
|
||||
needs: detect
|
||||
if: needs.detect.outputs.dockerfile != '[]'
|
||||
if: needs.detect.outputs.dagger != '[]'
|
||||
runs-on: k8s
|
||||
env:
|
||||
# Send Dagger OTLP telemetry to Tempo. Without a real backend the
|
||||
# engine's internal proxy returns 500 on /v1/metrics, causing noisy
|
||||
# retry warnings in every build.
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: http://tempo.tracing.svc.cluster.local:4318
|
||||
strategy:
|
||||
matrix:
|
||||
container: ${{ fromJson(needs.detect.outputs.dockerfile) }}
|
||||
container: ${{ fromJson(needs.detect.outputs.dagger) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
|
@ -85,12 +82,19 @@ jobs:
|
|||
- name: Extract version and SHA
|
||||
id: meta
|
||||
run: |
|
||||
VERSION=$(grep -m1 '^ARG CONTAINER_APP_VERSION=' \
|
||||
"containers/${{ matrix.container }}/Dockerfile" \
|
||||
| sed 's/^ARG CONTAINER_APP_VERSION=//')
|
||||
CONTAINER="${{ matrix.container }}"
|
||||
|
||||
# Try native Dagger pipeline (container.py) first, fall back to Dockerfile
|
||||
if [ -f "containers/$CONTAINER/container.py" ]; then
|
||||
VERSION=$(dagger call container-version --container-name="$CONTAINER")
|
||||
elif [ -f "containers/$CONTAINER/Dockerfile" ]; then
|
||||
VERSION=$(grep -m1 '^ARG CONTAINER_APP_VERSION=' \
|
||||
"containers/$CONTAINER/Dockerfile" \
|
||||
| sed 's/^ARG CONTAINER_APP_VERSION=//')
|
||||
fi
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "Error: No CONTAINER_APP_VERSION found in Dockerfile"
|
||||
echo "Error: Could not extract version for $CONTAINER"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
# CV Deploy Workflow
|
||||
#
|
||||
# Updates the CV deployment to a specific package version, commits
|
||||
# the change, and syncs via ArgoCD.
|
||||
# Bumps cv_version in ansible/roles/cv/defaults/main.yml and pushes the change.
|
||||
# Deployment to indri is manual (runner has no SSH access to indri):
|
||||
# mise run provision-indri -- --tags cv
|
||||
#
|
||||
# Usage:
|
||||
# 1. Release a new CV package from the cv repo first
|
||||
# 2. Go to Actions > Deploy CV > Run workflow
|
||||
# 3. Enter the version to deploy, or leave as "latest"
|
||||
# 4. Run the command above on gilbert to apply
|
||||
|
||||
name: Deploy CV
|
||||
|
||||
|
|
@ -60,18 +62,16 @@ jobs:
|
|||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Update CV deployment
|
||||
- name: Bump cv_version in ansible role
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
TARBALL="cv-${VERSION}.tar.gz"
|
||||
DEPLOYMENT_FILE="argocd/manifests/cv/deployment.yaml"
|
||||
RELEASE_URL="https://forge.eblu.me/api/packages/eblume/generic/cv/${VERSION}/${TARBALL}"
|
||||
DEFAULTS_FILE="ansible/roles/cv/defaults/main.yml"
|
||||
|
||||
echo "Updating $DEPLOYMENT_FILE with CV_RELEASE_URL..."
|
||||
yq -i "(.spec.template.spec.containers[0].env[] | select(.name == \"CV_RELEASE_URL\")).value = \"${RELEASE_URL}\"" "$DEPLOYMENT_FILE"
|
||||
echo "Bumping cv_version in $DEFAULTS_FILE to ${VERSION}..."
|
||||
yq -i ".cv_version = \"${VERSION}\"" "$DEFAULTS_FILE"
|
||||
|
||||
echo "Updated deployment:"
|
||||
grep -A1 "CV_RELEASE_URL" "$DEPLOYMENT_FILE"
|
||||
echo "Updated defaults:"
|
||||
grep -E "^cv_version:" "$DEFAULTS_FILE"
|
||||
|
||||
- name: Commit release changes
|
||||
env:
|
||||
|
|
@ -82,7 +82,7 @@ jobs:
|
|||
git config user.name "Forgejo Actions"
|
||||
git config user.email "actions@forge.ops.eblu.me"
|
||||
|
||||
git add argocd/manifests/cv/deployment.yaml
|
||||
git add ansible/roles/cv/defaults/main.yml
|
||||
|
||||
if git diff --cached --quiet; then
|
||||
echo "No changes to commit (already at $VERSION)"
|
||||
|
|
@ -94,38 +94,16 @@ jobs:
|
|||
echo "Changes committed and pushed"
|
||||
fi
|
||||
|
||||
- name: Deploy CV
|
||||
env:
|
||||
ARGOCD_AUTH_TOKEN: ${{ secrets.ARGOCD_AUTH_TOKEN }}
|
||||
run: |
|
||||
echo "Syncing CV app via ArgoCD..."
|
||||
|
||||
argocd app sync cv \
|
||||
--server argocd.ops.eblu.me \
|
||||
--grpc-web \
|
||||
--prune
|
||||
|
||||
argocd app wait cv \
|
||||
--server argocd.ops.eblu.me \
|
||||
--grpc-web \
|
||||
--timeout 120
|
||||
|
||||
echo "CV app synced successfully!"
|
||||
|
||||
- name: Purge Fly.io proxy cache
|
||||
env:
|
||||
FLY_API_TOKEN: ${{ secrets.FLY_DEPLOY_TOKEN }}
|
||||
run: |
|
||||
echo "Purging nginx cache on Fly.io proxy..."
|
||||
fly ssh console -a blumeops-proxy -C "sh -c 'rm -rf /tmp/cache && nginx -s reload'"
|
||||
echo "Cache purged"
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
echo "================================================"
|
||||
echo "CV Deployed: $VERSION"
|
||||
echo "CV version bumped: $VERSION"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
echo "CV should now be live at:"
|
||||
echo " https://cv.ops.eblu.me/"
|
||||
echo "To deploy on indri, run from gilbert:"
|
||||
echo " mise run provision-indri -- --tags cv"
|
||||
echo ""
|
||||
echo "Then purge the Fly.io proxy cache:"
|
||||
echo " fly ssh console -a blumeops-proxy -C \\"
|
||||
echo " \"sh -c 'rm -rf /tmp/cache && nginx -s reload'\""
|
||||
|
|
|
|||
0
.dagger/.gitattributes → .gitattributes
vendored
0
.dagger/.gitattributes → .gitattributes
vendored
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -1,5 +1,6 @@
|
|||
.claude/settings.local.json
|
||||
.claude/agent-memory/
|
||||
.claude/scheduled_tasks.lock
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
|
|
@ -7,5 +8,10 @@ __pycache__/
|
|||
*.pyo
|
||||
.venv/
|
||||
|
||||
# Dagger (auto-generated SDK)
|
||||
/sdk/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
/**/__pycache__
|
||||
/.env
|
||||
|
|
|
|||
171
AGENTS.md
Normal file
171
AGENTS.md
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
# AGENTS.md
|
||||
|
||||
Guidance for AI agents working in this repository. See also [[ai-assistance-guide]].
|
||||
|
||||
## Overview
|
||||
|
||||
blumeops is Erich Blume's GitOps repository for personal infrastructure, orchestrated via tailnet `tail8d86e.ts.net`.
|
||||
|
||||
**CRITICAL: Public repo at github.com/eblume/blumeops - never commit secrets!**
|
||||
|
||||
**Shell:** The user's interactive shell may differ from the current harness shell. Prefer repo-safe, non-interactive commands when possible, and match the user's shell conventions when giving interactive examples.
|
||||
|
||||
## Rules
|
||||
|
||||
1. **Always run `mise run ai-docs` at session start**
|
||||
This will refresh your context with important information you will be assumed to know and follow.
|
||||
**Read the full output** — never truncate, pipe to `head`/`tail`, or skip sections.
|
||||
For problems with a large surface area, ask the user if `mise run ai-sources` should also be run — it concatenates all non-doc source files (~270K tokens) for deep codebase context.
|
||||
2. **Always use `--context=minikube-indri` with kubectl** (or `--context=k3s-ringtail` for ringtail services) - work contexts must never be touched
|
||||
**NEVER run `minikube delete`** — it destroys all PVs, etcd, and cluster state. Use `minikube stop`/`minikube start` for restarts. If minikube is stuck, see [[restart-indri]]. Full rebuild from scratch requires the DR procedure in [[rebuild-minikube-cluster]].
|
||||
3. **Classify the change as C0/C1/C2 before starting** (see below) — this determines branching and PR requirements
|
||||
4. **Feature branches + PRs for C1/C2** - checkout main, pull, create branch, open PR via `tea pr create`. C0 goes direct to main.
|
||||
5. **Check PR comments with `mise run pr-comments <pr_number>`** before proceeding
|
||||
6. **Add changelog fragments (all change levels)** - `docs/changelog.d/<name>.<type>.md`
|
||||
Types: `feature`, `bugfix`, `infra`, `doc`, `ai`, `misc`
|
||||
Applies to C0, C1, and C2 whenever the change is user-visible or noteworthy.
|
||||
- **C1/C2:** Use branch name: `<branch>.<type>.md`
|
||||
- **C0:** Use orphan prefix: `+<descriptive-slug>.<type>.md` (avoids `main.*` collisions)
|
||||
7. **Test before applying** - dry runs (`--check --diff`), syntax checks, `ssh indri '...'`
|
||||
8. **Wait for user review before deploying** (C1/C2)
|
||||
9. **Never merge PRs or push to main without explicit request** (C0 commits to main are fine)
|
||||
10. **Verify deployments** - `mise run services-check`
|
||||
|
||||
## Change Classification
|
||||
|
||||
Before starting work, classify the change:
|
||||
|
||||
| Class | Name | When to use | Key trait |
|
||||
|-------|------|-------------|-----------|
|
||||
| **C0** | Quick Fix | Small, low-risk, fix-forward safe | Direct to main, no PR |
|
||||
| **C1** | Human Review | Moderate complexity or risk | Feature branch + PR, docs-first |
|
||||
| **C2** | Mikado Chain | Multi-phase, multi-session, high complexity | Mikado Branch Invariant |
|
||||
|
||||
**C0** — commit directly to main. No branch or PR needed. Fix forward if problems arise.
|
||||
|
||||
**C1** — feature branch with early PR. Search related docs first, write documentation changes before code, deploy from the unmerged branch (ArgoCD `--revision`, Ansible from checkout). Upgrade to C2 if complexity spirals.
|
||||
|
||||
**C2** — branch `mikado/<chain-stem>` governed by the Mikado Branch Invariant: all card commits first, then code progress, then card closures. Commits use `C2(<chain>): plan/impl/close/finalize` convention. Reset the branch when new prerequisites are discovered. Resume with `mise run docs-mikado --resume`.
|
||||
|
||||
See [[agent-change-process]] for the full methodology.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
./docs/ # documentation (Diataxis, Quartz)
|
||||
./docs/changelog.d/ # towncrier fragments
|
||||
./.dagger/ # dagger pipelines
|
||||
./.forgejo/ # forgejo-runner actions and workflows
|
||||
./mise-tasks/ # scripts via `mise run`
|
||||
./ansible/playbooks/ # ansible (indri.yml primary)
|
||||
./ansible/roles/ # indri service roles
|
||||
./argocd/apps/ # ArgoCD Application definitions
|
||||
./argocd/manifests/ # k8s manifests per service
|
||||
./fly/ # fly.io proxy for public routing
|
||||
./pulumi/ # Pulumi IaC (tailnet ACLs, dns, cloud)
|
||||
~/.config/{nvim,fish} # user's shell config, managed by chezmoi
|
||||
~/code/personal/ # user's projects
|
||||
~/code/personal/zk # user's zettelkasten (Obsidian-sync). Reference-data source; migrating into heph docs (hephaestus).
|
||||
~/code/3rd/ # mirrored external projects
|
||||
~/code/work # FORBIDDEN
|
||||
```
|
||||
Other code paths will be listed via ai-docs, this is just an overview. When you
|
||||
encounter wiki-links (`[[like-this]]`) it is referring to docs/ cards.
|
||||
|
||||
## Service Deployment
|
||||
|
||||
### Kubernetes (ArgoCD)
|
||||
|
||||
Most services run in minikube on indri via ArgoCD (app-of-apps, manual sync). GPU workloads (Frigate, ntfy) run on ringtail's k3s cluster, also managed by ArgoCD.
|
||||
|
||||
**PR workflow:**
|
||||
1. Create branch, modify `argocd/manifests/<service>/`
|
||||
2. Push. Sync 'apps' app if service definition changed (set --revision to branch).
|
||||
3. Test on branch: `argocd app set <service> --revision <branch> && argocd app sync <service>`
|
||||
4. After merge: `argocd app set <service> --revision main && argocd app sync <service>`
|
||||
|
||||
**Commands:** `argocd app list|get|diff|sync <app>`
|
||||
|
||||
**Login:** `argocd login argocd.ops.eblu.me --sso` (opens browser for Authentik SSO). Admin fallback for break-glass: `argocd login argocd.ops.eblu.me --username admin --password "$(op read 'op://vg6xf6vvfmoh5hqjjhlhbeoaie/srogeebssulhtb6tnqd7ls6qey/password')"`
|
||||
|
||||
### Indri (Ansible)
|
||||
|
||||
Native services: Forgejo, Zot, Caddy, Borgmatic, Alloy
|
||||
|
||||
```fish
|
||||
mise run provision-indri # full
|
||||
mise run provision-indri -- --tags <role> # specific
|
||||
mise run provision-indri -- --check --diff # dry run
|
||||
```
|
||||
|
||||
### Routing
|
||||
|
||||
| Domain | Mechanism | Reachable from |
|
||||
|--------|-----------|----------------|
|
||||
| `*.eblu.me` | Fly.io proxy (Tailscale tunnel) | public internet |
|
||||
| `*.ops.eblu.me` | Caddy on indri | k8s pods, containers, tailnet |
|
||||
| `*.tail8d86e.ts.net` | Tailscale MagicDNS | tailnet clients only |
|
||||
|
||||
Check tailscale serve: `ssh indri 'tailscale serve status --json'`
|
||||
|
||||
## Container Releases
|
||||
|
||||
```fish
|
||||
mise run container-list # show images/tags
|
||||
mise run container-release <name> <version> # tag and build
|
||||
```
|
||||
The goal is to eventually use only locally built containers in all cases, with
|
||||
full supply chain control via forge.ops.eblu.me repositories, mirroring source
|
||||
from upstream.
|
||||
|
||||
**After triggering a build** (manual dispatch or push to main), verify the
|
||||
workflow succeeded before proceeding:
|
||||
|
||||
```fish
|
||||
mise run runner-logs # find the run number
|
||||
mise run runner-logs <run#> # see jobs in the run
|
||||
mise run runner-logs <run#> -j <N> # fetch logs on failure
|
||||
```
|
||||
|
||||
This also works for other forge repos (`--repo eblume/hermes`).
|
||||
|
||||
## Third-Party Projects
|
||||
|
||||
Ask user to mirror on forge first, then clone to `~/code/3rd/<project>/`.
|
||||
|
||||
### Sporked Projects
|
||||
|
||||
Some mirrored projects are "sporked" — a floating-branch soft-fork strategy
|
||||
where local patches are continuously rebased on top of upstream. See
|
||||
[[spork-strategy]] and [[create-a-spork]] for the full methodology.
|
||||
|
||||
Sporked projects live in `~/code/3rd/<project>/` with three remotes:
|
||||
`origin` (eblume/ fork on forge), `mirror` (mirrors/ on forge), `upstream`
|
||||
(canonical). The `blumeops` branch is the default; `deploy` merges everything.
|
||||
|
||||
Create a new spork: `mise run spork-create <mirror-name>`
|
||||
|
||||
## Task Discovery
|
||||
|
||||
BlumeOps tasks live in [hephaestus](https://github.com/eblume/hephaestus) (`heph`),
|
||||
the user's self-hosted context/task system. Fetch them with the CLI:
|
||||
|
||||
```fish
|
||||
heph list --project Blumeops --json # outstanding Blumeops tasks as JSON
|
||||
```
|
||||
|
||||
(This replaced the retired `blumeops-tasks` mise task, which read from Todoist.)
|
||||
|
||||
Most operational scripts are stored in `./mise-tasks/`. For scripts with any logic or
|
||||
complexity, use uv run --script 's with explicit dependencies. Complex
|
||||
workflows with artifacts should become dagger pipelines. Mise tasks are for
|
||||
development processes and operations - tools for the user or the agent.
|
||||
|
||||
## Credentials
|
||||
|
||||
Root store is 1Password. Never grab directly - use existing patterns (ansible
|
||||
pre_tasks, external-secrets, scripts with `op` CLI). It's ok to use `op item
|
||||
get` without `--reveal` to explore what secrets are available, however.
|
||||
|
||||
Prefer `op read "op://vault/item/field"` over `op item get --fields` to avoid
|
||||
quoting issues with multi-line values.
|
||||
385
CHANGELOG.md
385
CHANGELOG.md
|
|
@ -12,6 +12,391 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
|
||||
<!-- towncrier release notes start -->
|
||||
|
||||
## [v1.17.0] - 2026-06-03
|
||||
|
||||
### Features
|
||||
|
||||
- Deploy the Adelaide / Heidi / Addie baby shower app — guest splash, raffle
|
||||
picker, and prize assignment console — on ringtail k3s with `shower.eblu.me`
|
||||
as the public entry and `shower.ops.eblu.me` as the tailnet admin host. App
|
||||
source: [`adelaide-baby-shower-app`](https://forge.eblu.me/eblume/adelaide-baby-shower-app).
|
||||
- Deploy adelaide-baby-shower-app v1.1.0 to ringtail k3s. Replaces the
|
||||
boolean lock with a four-phase `ShowerState` (`pre_event` → `party` →
|
||||
`prizes_locked` → `event_locked`), adds an append-only "guest memories"
|
||||
panel where guests can leave photos and comments for the baby, and
|
||||
polishes the admin and QR views. Three Django migrations
|
||||
(`0009_shower_phase`, `0010_guest_memories`, `0011_book_description`)
|
||||
run automatically in the entrypoint against the SQLite PV. No config
|
||||
or env-var changes.
|
||||
|
||||
Container build also gains a Forgejo-PyPI workaround: Forgejo's simple
|
||||
index returns absolute file URLs hardcoded to the public ROOT_URL
|
||||
(`forge.eblu.me`), which the Fly edge 403s on `/api/packages/*`. The
|
||||
wheel and sdist are now both pulled via direct `fetchurl` against
|
||||
`forge.ops.eblu.me` (tailnet-only) and the wheel is handed to pip as
|
||||
a local path.
|
||||
- `review-compliance-reports` now also fetches and summarizes the weekly Prowler container-image and IaC scans (previously only the K8s CIS in-cluster scan was processed). For each scan it shows status counts, severity breakdown, week-over-week delta, and — for the high-volume image/IaC scans — top-N tables grouped by check ID and resource instead of per-finding listings.
|
||||
- runner-logs now authenticates with Forgejo API token and auto-detects the repo from git remote. Job logs are fetched via SSH to indri (reading Forgejo's on-disk zstd log files) instead of the web endpoint, which doesn't support token auth for private repos.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix nightly borgmatic backups failing for 2 days. The shower SQLite
|
||||
dump hook referenced `kubectl --context=k3s-ringtail`, but indri's
|
||||
kubeconfig deliberately doesn't carry the ringtail credentials. The
|
||||
`before_backup` hook's failure aborted the entire run, taking out
|
||||
*both* the local sifaka repo and the BorgBase offsite. Replaced
|
||||
the inline-shell dump with a `~/bin/borgmatic-k8s-sqlite-dump`
|
||||
helper deployed by the ansible role. Each dump entry now declares a
|
||||
`target` of either `local:<context>` (mealie — kubectl uses indri's
|
||||
kubeconfig) or `ssh:<user@host>` (shower — ssh into ringtail and
|
||||
run `k3s kubectl` there, no indri-side kubeconfig needed; k3s.yaml
|
||||
on ringtail is mode 644 so no sudo required). Bytes stream back via
|
||||
`kubectl exec ... -- cat` rather than `kubectl cp`, since `kubectl
|
||||
cp` requires `tar` inside the pod and nix-built images like shower
|
||||
don't bundle it.
|
||||
- Shower app container now bakes the wheel + Python deps into the image
|
||||
at build time via `buildPythonPackage` instead of pip-installing on
|
||||
first boot. Boots are deterministic and don't depend on forge PyPI
|
||||
being reachable from the pod. The `wheelHash` in
|
||||
`containers/shower/default.nix` is the sha256 sourced from the
|
||||
[forge PyPI simple index](https://forge.eblu.me/api/packages/eblume/pypi/simple/adelaide-baby-shower-app/);
|
||||
bumping the version means bumping that hash too.
|
||||
|
||||
Borgmatic now covers the shower app: SQLite is dumped from the live
|
||||
pod via `kubectl exec` (mirroring the existing mealie entry, with
|
||||
`context: k3s-ringtail`), and the prize-photo media share is picked up
|
||||
through `/Volumes/shower` (sifaka SMB mount on indri, same pattern as
|
||||
`/Volumes/photos`).
|
||||
- Disabled adaptive sync (VRR) on ringtail's DP-1 output. The OMEN 27i IPS panel pumps brightness when its refresh rate swings into the low VRR range during low-framerate content (e.g. game cutscenes), producing a flicker that worsened over a session until a reboot. Pinning the panel to a fixed 165Hz eliminates it.
|
||||
- Fixed forge.eblu.me static assets (CSS, JS, images, fonts) not loading — the proxy's static asset cache block was missing the `Host` header, so Caddy couldn't route the requests.
|
||||
- Fixed homepage container EACCES on cold start: the nix-built image now chowns
|
||||
`/app/config` to uid 1000 at build time via `fakeRootCommands`, matching the
|
||||
behavior of the old Dockerfile. Without this, homepage couldn't seed missing
|
||||
skeleton configs (proxmox.yaml etc.) or create `/app/config/logs`, crashing on
|
||||
its first uncached request. Caught during the ringtail cutover.
|
||||
- Fixed sway keybindings on ringtail — the home-manager `keybindings` block was replacing the module's defaults entirely, leaving only explicit overrides (no workspace switching, focus, move, splits, resize mode, etc). Switched to `lib.mkOptionDefault` with `lib.mkForce` on the conflicting custom binds (`Mod+Return`, `Mod+d`, `Mod+space`, `Mod+l`) so defaults merge back in. Also added `Mod+F1` to show a filterable fuzzel list of current keybindings.
|
||||
|
||||
Fixed fuzzel config errors on launch — `border-radius` and `border-width` were under `[main]`, but fuzzel expects them as `radius`/`width` under a `[border]` section.
|
||||
- Pin the Quartz docs build to v4.5.2. The Dagger `build_docs` pipeline cloned Quartz from the default branch unpinned; Quartz v5.0.0 restructured its config layout (`.quartz/plugins`, `../quartz` imports) and broke the docs build against our existing `quartz.config.ts`/`quartz.layout.ts`.
|
||||
|
||||
### Infrastructure
|
||||
|
||||
- Wire the ringtail `blumeops-pg` cluster (which holds the wave-1-migrated
|
||||
paperless + teslamate databases) into backups and Grafana. Adds a Tailscale
|
||||
LoadBalancer Service (`blumeops-pg-ringtail.tail8d86e.ts.net`) and a Caddy L4
|
||||
route (`pg.ops.eblu.me:5434`), then repoints borgmatic's `teslamate` +
|
||||
`paperless` postgres dumps and the `mealie` SQLite dump at ringtail, and the
|
||||
Grafana TeslaMate datasource at the ringtail DB. Closes the backup gap that
|
||||
opened at cutover (the migrated live data was still being backed up from the
|
||||
now-frozen minikube copies) and unblocks the wave-1 decommission.
|
||||
- Migrated homepage dashboard from minikube (indri/arm64) to k3s (ringtail/amd64).
|
||||
The container is now built via nix (`containers/homepage/default.nix`), adapted
|
||||
from nixpkgs `homepage-dashboard` with the upstream Next.js cache patches and
|
||||
wrapped with `dockerTools.buildLayeredImage`. Autodiscovery shifts: services on
|
||||
minikube (ArgoCD, Immich, Kiwix, Mealie, Miniflux, Grafana, Prometheus,
|
||||
Navidrome, Paperless, TeslaMate, Transmission) become explicit static entries
|
||||
in `services.yaml`; ringtail services (Authentik, Frigate/NVR, Ntfy, Ollama)
|
||||
auto-populate via Ingress annotations.
|
||||
- Migrated CV (`cv.eblu.me`) and Docs (`docs.eblu.me`) from minikube Deployments to indri-native ansible roles. Caddy now serves the extracted release tarballs directly via a new `kind: static` service-block in the Caddy template — no daemon, no container — replacing the prior nginx-in-a-pod layer. Removes a network hop on every request and shrinks minikube's footprint. See [[cv-on-indri]] and [[docs-on-indri]]. Part of the broader minikube wind-down.
|
||||
- Migrated devpi (PyPI mirror at `pypi.ops.eblu.me`) from a minikube StatefulSet to a launchd-managed service on indri. devpi-server now runs in a uv-managed venv with pinned `devpi-server` and `devpi-web` versions, listens on `127.0.0.1:3141`, and is fronted by Caddy. The minikube StatefulSet was crash-looping under memory pressure (and breaking the Python toolchain everywhere); the new layout removes a layer of dependency on cluster health for critical-path tooling. See [[devpi-on-indri]].
|
||||
- Move the entire Immich stack — server, machine-learning, valkey,
|
||||
and the PostgreSQL+VectorChord cluster — off `minikube-indri` and
|
||||
onto `k3s-ringtail`. Postgres data migrated zero-loss via CNPG
|
||||
`pg_basebackup` (replica catch-up then promote); row counts on
|
||||
`asset`, `user`, `album`, `smart_search`, `activity`, `asset_face`
|
||||
verified equal between source and replica before cutover. The ML
|
||||
pod now uses ringtail's RTX 4080 via the nvidia-device-plugin
|
||||
(time-slicing bumped 2 → 4 to share with frigate + ollama). Caddy
|
||||
routing at `photos.ops.eblu.me` is unchanged (still
|
||||
`photos.tail8d86e.ts.net`, the device just lives on ringtail now).
|
||||
Borgmatic backups continue against the same `immich-pg` tailnet
|
||||
hostname. First concrete chain in the broader indri-k8s
|
||||
decommission effort.
|
||||
- Add local nix container build for `tailscale` (`containers/tailscale/default.nix`) so ringtail's tailscale-operator ProxyClass proxy pods pull from the forge mirror instead of `docker.io/tailscale/tailscale`. Pinned at v1.94.2 to match `service-versions.yaml`. Indri's tailscale-operator continues to use upstream during the k8s-to-ringtail migration.
|
||||
- Address the 6 critical Prowler IaC findings against `argocd/manifests/`. Prowler's IaC provider hardcodes `self._mutelist = None` and delegates filtering to Trivy, but doesn't plumb `--ignorefile` through — so the documented "use Trivy filtering" path is actually broken. Added a shim around `trivy` in the Prowler image that injects `--ignorefile $TRIVY_IGNOREFILE` for `trivy fs` invocations when the env var points at a real file. The IaC cronjob now mounts `mutelist/trivyignore.yaml` (Trivy's per-path schema) and sets the env var, muting the `external-secrets` and `kube-state-metrics` Secret-access findings (KSV-0041, KSV-0114). Separately, `grafana-clusterrole` is tightened to remove `secrets` access entirely: the dashboard sidecar already only consumes ConfigMap-labeled dashboards, so its `RESOURCE` env var is now `configmap` instead of `both`.
|
||||
- Pin ringtail's wired IP to `192.168.1.21` via NixOS scripted networking; NetworkManager no longer manages `enp5s0`. Removes DHCP lease renewal as a failure mode after a silent lease teardown took ringtail offline. Also explicitly enables `net.ipv4.ip_forward` (previously set implicitly by scripted-DHCP) so k3s pod networking and Tailscale routing continue to work with static networking.
|
||||
- Ripped out the compensating-controls (CC) framework: deleted `compensating-controls.yaml`, the `review-compensating-controls` mise task, and the associated how-to / explanation docs. Prowler and Kingfisher continue to run weekly and produce reports; the Prowler mutelist YAML files remain in place but no longer carry `CC: <id>` prefixes — each entry just keeps a free-form `Description` of why the finding is muted. The CC review cadence proved to be more overhead than this single-operator homelab needed.
|
||||
- Wire shower app for public exposure: fly nginx `shower.eblu.me` server
|
||||
block as a guest-only surface — splash page, `/prizes/<token>/`, static
|
||||
assets, media. Everything authenticated (`/admin/`, `/host/`,
|
||||
`/accounts/`) returns 403 with a "tailnet only" pointer. Staff hit
|
||||
`shower.ops.eblu.me` for the operator console + admin; the app's
|
||||
v1.0.1 `DJANGO_PUBLIC_URL_BASE` setting makes QR codes generated on
|
||||
the tailnet point back at the WAN host for guests. Plus a Caddy route
|
||||
on indri, Pulumi Gandi CNAME, and a Grafana APM dashboard tracking
|
||||
request rate, error rate, latency, bandwidth, and access logs.
|
||||
- Mirror Valkey 8.1 locally as `registry.ops.eblu.me/blumeops/valkey`. Replaces direct pulls of `docker.io/valkey/valkey:8.1-alpine` for paperless and immich sidecars. Built via native Dagger pipeline on Alpine 3.22. Stateless swap — no data migration. Authentik's nix-built Redis remains separate.
|
||||
- Add nix-built amd64 valkey for ringtail (`containers/valkey/default.nix`) so immich-ringtail can stop pulling the upstream multi-arch `docker.io/valkey/valkey` image. Existing `container.py` continues to build Alpine arm64 for paperless on indri. Both bump to valkey 8.1.7 (Alpine 3.22 8.1.7-r0 / nixpkgs 8.1.7).
|
||||
- Upgrade Grafana Alloy v1.14.0 → v1.16.0 across all four service deployments
|
||||
(alloy-k8s, alloy-ringtail, alloy-tracing-ringtail on k8s; alloy native on
|
||||
indri). Pulls in stable database observability (v1.15) and the OTel Collector
|
||||
v0.147.0 bump. Container build also migrated from Dockerfile to native Dagger
|
||||
`container.py` per the build-container-image migration playbook.
|
||||
- Upgraded Dagger from v0.20.1 to v0.20.6 (engine, CLI pin, and SDK regen) and migrated `runner-job-image` from a Debian-based Dockerfile to a native Dagger `container.py` on Alpine 3.23, reusing the shared `alpine_runtime` helper.
|
||||
- Decommission the wave-1 services on minikube-indri now that paperless,
|
||||
teslamate, and mealie run on ringtail with their data backed up. Removes the
|
||||
minikube `paperless`/`teslamate`/`mealie` manifest dirs + ArgoCD app
|
||||
definitions (pruning the parked Deployments, Services, and the redundant
|
||||
minikube mealie/paperless PVCs), and drops the `paperless`/`teslamate` roles
|
||||
from the minikube `blumeops-pg` cluster. The `paperless` and `teslamate`
|
||||
databases are dropped from indri's blumeops-pg as the finalization step.
|
||||
miniflux + authentik remain on the minikube cluster (later waves).
|
||||
- Upgraded the k8s Forgejo runner to the v12.8 line, switched it from first-boot registration to declarative `server.connections` credentials from 1Password, and consolidated the supporting runner how-to documentation.
|
||||
- Move paperless, teslamate, and mealie off `minikube-indri` onto
|
||||
`k3s-ringtail`, shedding ~1.1 GiB of resident load from the
|
||||
OOM-thrashing 8 GiB minikube node (the kernel OOM killer had been
|
||||
killing `kube-apiserver`/`dockerd`/argocd, flapping every
|
||||
minikube-hosted service at once). paperless + teslamate databases
|
||||
move into a fresh CNPG `blumeops-pg` cluster on ringtail via a cold
|
||||
`pg_dump`/`pg_restore` from the quiesced source — row counts verified
|
||||
equal before any routing flip; source DBs dropped only after the
|
||||
ringtail side serves traffic. mealie's SQLite PVC is copied as-is.
|
||||
paperless media stays on sifaka NFS. Downtime-tolerant cold cutover
|
||||
(no streaming replication); rollback is repoint-and-scale-up with the
|
||||
source untouched. Second chain in the indri-k8s decommission after
|
||||
[[migrate-immich-to-ringtail]].
|
||||
- Recurring maintenance batch:
|
||||
|
||||
- Ringtail flake inputs refreshed (`disko`, `home-manager`, `nixpkgs`).
|
||||
- Tooling deps bumped: prek hooks (trufflehog v3.95.3, kingfisher v1.101.0, ruff v0.15.14, `ansible-core` 2.21.0); fly proxy base images (nginx 1.30.1-alpine, alloy v1.16.1); `typer==0.26.2` in mise tasks.
|
||||
- Updated `nixos/ringtail/flake.lock` (weekly cadence): `disko`, `home-manager`, and `nixpkgs` inputs refreshed. `nixpkgs-services` skipped per overlay convention.
|
||||
- Reviewed `mealie` service version freshness; upstream is 5 minor versions ahead (v3.17.0 vs deployed v3.12.0). Marked reviewed; upgrade deferred.
|
||||
- Deploy shower v1.1.2 — bump container build to new app release.
|
||||
- Upgrade unpoller v2.34.0 → v3.2.0 and migrate container build from Dockerfile to native Dagger (container.py). v3.0.0 carries breaking UniFi API changes; v3.2.0 introduces a 60s background poll (cached scrapes) by default — set `interval = 0` in `up.conf` to restore on-demand polling.
|
||||
- Monthly tooling dependency refresh: prek hooks (trufflehog, kingfisher, ruff, shfmt, prettier, actionlint, ansible-lint), fly proxy base images (nginx 1.30.0, tailscale v1.94.2, alloy v1.16.0), normalize pyyaml lower bound in mise-tasks.
|
||||
- Add GE-Proton (`pkgs.proton-ge-bin`) to `programs.steam.extraCompatPackages`
|
||||
on ringtail. Subnautica 2 hangs at Mercuna plugin init under Proton
|
||||
Experimental + DXVK D3D12; GE-Proton is available as a Steam per-game
|
||||
compatibility option to work around it.
|
||||
- Add `sn2-prelaunch` Steam launch wrapper on ringtail that removes
|
||||
Subnautica 2's stale `Saved/running.dat` and `Saved/beforelobby.dat`
|
||||
lockfiles before each launch. SN2 pops up an invisible (0×0-sized)
|
||||
Error dialog when it detects an unclean exit, blocking GameThread
|
||||
forever; this is observable only as a black screen with a spinning
|
||||
loader. Use via Steam launch option: `sn2-prelaunch %command%`.
|
||||
- Add local nix container build for `frigate-notify` (`containers/frigate-notify/default.nix`) so the Frigate→ntfy bridge is rebuilt on ringtail from the forge mirror instead of pulled from `ghcr.io/0x2142/frigate-notify`.
|
||||
- Add resource limits to all ArgoCD pods to prevent unbounded resource consumption during node-wide pressure events.
|
||||
- Black-hole the `/mirrors/*` repositories at the Fly proxy edge (`return 403` → `forge.ops.eblu.me`). A surprise $29.60 Fly bill traced to ~1.24 TB/30d of egress on `forge.eblu.me`, 99.95% of all proxy egress — of which ~71% was AI scrapers (Meta `meta-externalagent`, OpenAI `GPTBot`, Amazonbot) crawling the near-infinite git-history URL space of the public mirror repos and timing out Forgejo in the process. Mirrors exist for supply-chain control and are consumed over the tailnet, so their public web UI had no legitimate audience. `robots.txt` already disallowed `/mirrors/`, but the offending agents ignore it. Tier-2 mitigations (user-agent denylist, Anubis proof-of-work gateway) are documented in `docs/explanation/ai-scraper-mitigation.md`.
|
||||
- Bump paperless and immich kustomizations to the main-SHA-built valkey tag (`v8.1.6-r0-fabca04`). Routine post-merge follow-up to keep production manifests pointing at images built from a commit on main.
|
||||
- Bump shower container to v1.1.1 (probe FOD hash).
|
||||
- Bumped shower app to v1.1.3 (wheel/sdist + FOD hashes probed on ringtail).
|
||||
- Cap systemd-coredump on ringtail (ProcessSizeMax/ExternalSizeMax 1G, MaxUse 2G) so multi-GB Wine/Proton game crash dumps no longer thrash the disk and lock up the desktop.
|
||||
- Deploy shower v1.1.1 to ringtail (kustomize newTag bump).
|
||||
- Deployed shower v1.1.3 to ringtail (image built and pushed from ringtail; runner bypassed due to indri overload).
|
||||
- Fix three follow-ups from the wave-1 decommission: grant the local
|
||||
break-glass `admin` account ArgoCD admin rights (`g, admin, role:admin` —
|
||||
previously only the Authentik `admins` group had access, so admin was
|
||||
locked out whenever its token expired), and repoint the alloy blackbox
|
||||
probe for teslamate from the deleted minikube service to
|
||||
`https://tesla.ops.eblu.me/` (through Caddy over Tailscale). The orphaned
|
||||
paperless/teslamate roles + ExternalSecrets left on the minikube
|
||||
blumeops-pg are also cleaned up.
|
||||
- Moved the Immich blackbox health probe from indri's alloy to ringtail's alloy. After the immich migration to ringtail, the probe still targeted `immich-server.immich.svc.cluster.local` on indri's cluster where the service no longer exists, causing a persistent `ServiceProbeFailure` alert.
|
||||
- Pin shower v1.1.1 FOD outputHash (probed locally on ringtail).
|
||||
- Rebuild Prowler container against main HEAD (v5.23.0-495e45d) after merging the IaC mutelist Dockerfile changes.
|
||||
- Rebuild and retag alloy v1.16.0 container images from the main-branch SHA
|
||||
following the squash-merge of #345, per the build-container-image
|
||||
squash-merge convention. Both images (`registry.ops.eblu.me/blumeops/alloy`)
|
||||
now reference `9564435` rather than the branch SHA `26a3ab5`, restoring
|
||||
source traceability after branch cleanup.
|
||||
- Rebuild shower from the post-merge commit on main so the container's
|
||||
SHA tag points at a commit that will still exist after the 30-day
|
||||
branch-cleanup window. Functionally identical to the branch-tag image
|
||||
already deployed, just preserves source traceability per
|
||||
[[build-container-image#Squash-merge and container tags]].
|
||||
- Rebuild unpoller container from squashed main commit so the image SHA tag matches a commit in main's history (was tagged with the pre-squash branch SHA).
|
||||
- Rebuild valkey container from squashed main commit (both arm64 dagger and amd64 nix variants), and update paperless + immich-ringtail kustomizations to the main-SHA tags `v8.1.7-ecded30` and `v8.1.7-ecded30-nix`.
|
||||
- Retired the `blumeops-tasks` mise task (Todoist API) in favor of `heph list --project Blumeops --json` from the self-hosted [hephaestus](https://github.com/eblume/hephaestus) system. Updated docs to point task discovery and rotation reminders at heph, and noted that the `~/code/personal/zk` zettelkasten is migrating into heph docs.
|
||||
- Switch the Fly proxy deploy strategy from `bluegreen` to `immediate` in `fly/fly.toml`. With a single proxy machine, bluegreen offers little benefit — the green machine routinely failed to reach "started" inside Fly's default 5-minute deploy timeout (the cold-start sequence of `tailscaled` → `tailscale up` → wait-for-MagicDNS → nginx startup eats most of the budget), and the failed deploys would roll back. `immediate` replaces the machine in place with a brief downtime (~5–10s) but actually completes.
|
||||
- Switch the ringtail provisioning playbook's blumeops clone URL from `forge.eblu.me` (public, via Fly proxy) to `forge.ops.eblu.me` (tailnet, direct via Caddy on indri). Ringtail is always on the tailnet, so the WAN round-trip is pure overhead — it also made `provision-ringtail` brittle whenever the Fly proxy was slow or down.
|
||||
- Switched Grafana's deployment strategy from `RollingUpdate` to `Recreate`. With an RWO PVC holding the SQLite database and Bleve search index, `RollingUpdate` reliably crashloops the new pod on the index lock until rollout timeout. `Recreate` terminates the old pod first so the new one acquires the lock cleanly.
|
||||
- Update `tailscale-operator-ringtail` ProxyClass to reference the `0108b68` main-SHA build of the tailscale container. Routine post-merge cleanup so the deployed image traces to a commit that survives PR branch cleanup.
|
||||
- Update the ringtail NixOS flake lockfile (`nixos/ringtail/flake.lock`): bump
|
||||
`nixpkgs` (b77b3de → 25f5383) and `disko` (5ba0c95 → 115e521) to latest.
|
||||
`nixpkgs-services` was intentionally left pinned (skipped by the
|
||||
`flake-update` pipeline). Routine recurring maintenance per [[manage-lockfile]].
|
||||
- Upgrade native macOS Alloy on indri to v1.16.0. Built on gilbert with Go
|
||||
1.26.2 + CGO (required for the macOS native DNS resolver, which Tailscale
|
||||
MagicDNS depends on), scp'd to `~/.local/bin/alloy` on indri, codesigned,
|
||||
and the LaunchAgent reloaded. Completes the v1.16.0 fleet upgrade started
|
||||
in #345 — all four Alloy services (alloy-k8s, alloy-ringtail,
|
||||
alloy-tracing-ringtail, alloy ansible) now run v1.16.0.
|
||||
- Upgraded zot on indri from v2.1.15 to v2.1.16 (security fixes: TLS verification on metrics client, CORS Allow-Credentials suppression on wildcard origins, manifest/API-key body size limits).
|
||||
|
||||
### Documentation
|
||||
|
||||
- Reviewed `replicating-blumeops` tutorial: fixed "BluemeOps" typos (also in `contributing.md`) and added `last-reviewed` frontmatter.
|
||||
- Reviewed [[indri]] reference card: added `devpi`, `cv`, and `docs` to the native-services list; widened the k8s note to reflect the growing set of apps now on ringtail and the planned indri-minikube decommission; added CPU/RAM specs.
|
||||
- New how-to: rotate-fly-deploy-token. Documents the 75-day rotation cadence, why we use `org`-scoped tokens (silences the cosmetic metrics-token warning on `fly status` with marginal blast-radius cost given the single-app personal org), and the procedure for rotation + Forgejo Actions secret sync.
|
||||
- Add `docs/explanation/ai-scraper-mitigation.md` — the egress-cost / AI-crawler threat model for the public Fly proxy, the tiered mitigation plan (Tier 1: mirror black-hole, shipped; Tier 2: user-agent denylist + Anubis; Tier 3: Cloudflare, rejected on principle), and the data behind it.
|
||||
- Fix manage-forgejo-mirrors verify step — sync button is on the repo settings page ("Synchronize now"), not the main repo page.
|
||||
- Fixed the `op item edit` invocation in the [[zot]] API-key rotation procedure: the previous `pbpaste | op item edit ... "field[password]=-"` stdin syntax is rejected by op 2.34 as "invalid JSON" (recent op versions treat piped input as a full JSON template, not a single field value). Procedure now reads the clipboard into a local fish variable and passes it as an inline assignment.
|
||||
- Fixed the export-filename step in [[run-1password-backup]]: 1Password's desktop app names the export `1PasswordExport-<account-uuid>-<timestamp>.1pux` automatically rather than letting you save to a fixed name, so the procedure now points the task at that glob instead of pretending the default name is `1Password-export.1pux`.
|
||||
- Refresh the contributing tutorial: add `last-reviewed`, include the `.ai.md` changelog fragment type, and clarify that `prek` is pinned via `mise`.
|
||||
- Review and refresh the Navidrome reference card: add `last-reviewed`, correct the scanner env var name, document the current image/version, and record routing and runtime details from the manifests.
|
||||
- Review and refresh the Ollama reference card: add `last-reviewed`, bump the documented image tag to 0.20.4, and add the two `qwen3.5` models now declared in `models.txt`.
|
||||
- Reviewed [[1password]] reference card: added the `blumeops` vs `Personal` vault split, noted that `onepassword-connect` runs on both indri and ringtail (not just one cluster), and pulled the `op read` vs `op item get --fields` guidance up from agent memory into the card.
|
||||
- Reviewed `index.md`; added ringtail to the infrastructure overview and stamped `last-reviewed`.
|
||||
- Reviewed transmission card: corrected storage layout (`/config/` is emptyDir, watch dir disabled) and noted the Prometheus exporter sidecar.
|
||||
- rotate-fly-deploy-token: combine mint+store into one command with both fish and bash forms; document the `op item edit` "Password item requires ps value" validator gotcha and the placeholder-password workaround.
|
||||
|
||||
### AI Assistance
|
||||
|
||||
- Adopt `AGENTS.md` as the canonical agent instruction file, keep `CLAUDE.md` as a compatibility shim, and update docs to reference the neutral file and the correct agent-change-process path.
|
||||
- CLAUDE.md now imports AGENTS.md via `@AGENTS.md` instead of telling agents to go read it. Claude Code only auto-loads CLAUDE.md, so the prose shim was easy to skip; the import inlines AGENTS.md into the session prompt unconditionally.
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
- Removed the dead minikube manifests, container builds, and tooling shims left behind after the cv + docs migration to indri-native (#342). Deletes `argocd/{apps,manifests}/{cv,docs}/`, `containers/{cv,quartz}/`, and the `quartz`→`docs` mapping in `mise-tasks/container-version-check`. Bumps `docs.current-version` to `v1.16.0` (the blumeops release tag) now that the legacy nginx-base version pin is gone.
|
||||
- Rebuild shower v1.1.0 container from main HEAD (`3c7967e`) and bump the
|
||||
kustomization tag to `v1.1.0-3c7967e-nix`. The PR was squash-merged, so
|
||||
the branch commit `444ff91` baked into the prior tag isn't reachable
|
||||
from main's history. The new tag points at a commit that exists on
|
||||
main; image content is byte-identical because the FOD output is content
|
||||
addressed and the inputs didn't change.
|
||||
- Rebuild shower v1.1.2 from main HEAD (a33fa47) and retag — PR #358 was squash-merged so the branch SHA baked into the prior image tag isn't reachable from main. FOD is content-addressed, so image bytes are identical; only provenance changes.
|
||||
- Remove the duplicate Homepage tiles for Mealie, Paperless, Immich, and
|
||||
TeslaMate. Homepage runs on ringtail and autodiscovers ringtail Ingresses via
|
||||
`gethomepage.dev/*` annotations; once these services migrated to ringtail they
|
||||
were discovered automatically, making their leftover static `services.yaml`
|
||||
entries (needed only while they lived on minikube) redundant.
|
||||
- Removed the now-unused `containers/devpi/` Dagger build artifact. Devpi runs natively on indri via uv venv; the container image is no longer referenced anywhere. Doc examples in `docs/reference/tools/dagger.md` updated to use `miniflux` as the example container name.
|
||||
- `container-build-and-release` now prints the specific `mise run runner-logs <N>` command after dispatching, polling the Forgejo API to resolve the run number for the commit it just triggered.
|
||||
- `mise run runner-logs <run> -j <n>` now reports a clear error when the log file doesn't exist on indri (e.g. a runner crash that left `action_task.log_in_storage = 0`). Previously it printed only the header and exited 0, because `zstdcat` exits 0 with a "can't stat … -- ignored" stderr message and ssh+fish on indri swallows the remote exit code.
|
||||
|
||||
|
||||
## [v1.16.0] - 2026-04-18
|
||||
|
||||
### Infrastructure
|
||||
|
||||
- Route Fly.io proxy through Caddy on indri with direct WireGuard peering, reducing public-facing latency from 20+ seconds (DERP relay) to sub-second. Fixed Beyla eBPF tracing on ringtail (memlock rlimit + BPF permissions). Restored trace collection to Tempo.
|
||||
|
||||
|
||||
## [v1.15.7] - 2026-04-18
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix borgmatic LaunchAgent failing silently due to macOS TCC permission dialogs. LaunchAgents now call borgmatic directly instead of routing through `mise x`, which triggered "wants to access Documents" dialogs that hung headless sessions. The ansible role now also manages borgmatic installation via `mise install`.
|
||||
|
||||
### Infrastructure
|
||||
|
||||
- Automate verification of Prowler MANUAL findings (kubelet file perms, kubelet config, etcd CA, RBAC cluster-admin) in `review-compliance-reports` and mute them with `node-config-automated-verification` compensating control.
|
||||
- Migrate transmission and transmission-exporter containers from Dockerfile to native Dagger builds (`container.py`). Updates base images to Alpine 3.23 and Python 3.14, pins uv to 0.11.6.
|
||||
- Switched Fly proxy to upstream keepalive pools, reducing forge.eblu.me latency from 35s+ p50 to sub-second. Added `mise run fly-reload` for DNS re-resolution without redeploy.
|
||||
- Upgrade Prowler from 5.22.0 to 5.23.0; remove init container workaround for broken `--registry` flag (upstream fix in PR #10470).
|
||||
- Added `robots.txt` to `forge.eblu.me` blocking crawlers from `/mirrors/` to reduce load from Facebook scraping.
|
||||
- Container builds are now manual-only via `mise run container-build-and-release`. Removed auto-trigger on push to main — shared Dagger helpers made path-based detection unreliable.
|
||||
- Migrate devpi container from Dockerfile to native Dagger build; bump devpi-server 6.19.1→6.19.3 and devpi-web 5.0.1→5.0.2.
|
||||
- Migrated kiwix-serve container from Dockerfile to native Dagger build, bumping Alpine base from 3.22 to 3.23.
|
||||
- Mitigated Forgejo archive endpoint DoS: redirect public archive requests to tailnet, expanded robots.txt, enabled archive cleanup cron, cached release downloads at proxy.
|
||||
- Refactored Dagger container pipelines: extended `go_build()` helper with `buildmode` and `extra_env` params, migrated miniflux and forgejo-runner to use it, and standardized all Alpine bases from 3.22 to 3.23.
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
- Review compensating control `sso-gated-admin-tools`: tightened scope to ArgoCD only, removed Grafana reference.
|
||||
- container-build-and-release now verifies the commit exists on the remote before dispatching a build.
|
||||
|
||||
|
||||
## [v1.15.6] - 2026-04-14
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Rotate ArgoCD workflow-bot token and admin password after DR rebuild invalidated signing keys, fixing build-blumeops workflow failures.
|
||||
|
||||
|
||||
## [v1.15.5] - 2026-04-14
|
||||
|
||||
### Features
|
||||
|
||||
- Deploy Paperless-ngx document management system at paperless.ops.eblu.me with OCR, Authentik SSO, and NFS storage on sifaka.
|
||||
- Add `ty` (Astral) Python typechecker to prek hooks, configured for Dagger SDK and container.py modules. Add `type: mise` to service-versions.yaml for tracking development tool versions (dagger, ansible-core, prek, pulumi, ty) through the standard service review process.
|
||||
- Upgrade grafana-sidecar from 1.28.0 to 2.6.0, adding health probes and porting build to native Dagger container.py.
|
||||
- Upgrade Navidrome to v0.61.1 — major artwork overhaul with per-disc cover art, rebuilt search engine (SQLite FTS5), server-managed transcoding, and WebP performance fix.
|
||||
- Add `mise run review-compliance-reports` task for weekly compliance report review with muted/unmuted distinction and week-over-week delta
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Add paperless database to borgmatic backup configuration. Previously the only service DB not included in nightly pg_dump backups.
|
||||
- Fix Fly.io proxy rate limiting to key on real client IP instead of Fly's internal proxy IP, so crawlers no longer consume the shared rate limit bucket for all clients.
|
||||
- Fix UnPoller (UniFi) Grafana dashboards failing to load due to UID exceeding Grafana 12's 40-character limit.
|
||||
- Fix blumeops-tasks swallowing wiki-link brackets in task descriptions (rich markup escaping)
|
||||
- Fix dagger flake-update pipeline: replace nonexistent `--exclude` flag with dynamic input discovery
|
||||
- Fix services-check to display all firing alerts for a given alert name, not just the first one.
|
||||
- Pin Fly.io proxy Tailscale to v1.94.1 — the `:stable` tag pulled v1.96.5 which has a MagicDNS regression (SERVFAIL on tailnet names), breaking all public routing through forge.eblu.me, docs.eblu.me, and cv.eblu.me.
|
||||
- Rewrite `mise run runner-logs` CLI: list runs by run number (not task ID), drill into jobs per run, fetch logs via Forgejo web API instead of SSH+filesystem. Fixes broken log retrieval caused by incorrect hex path calculation and stale data directory. Added `--repo` to query any forge repo (e.g. sporks) and `--limit`/`-n` to control listing size (0 for all).
|
||||
- Route Dagger build telemetry to Tempo, fixing OTEL metrics exporter warnings.
|
||||
- Switch paperless redis sidecar from amd64-only nix-built `authentik-redis` image to upstream `valkey:8.1-alpine` (multi-arch). The nix image was previously running under QEMU emulation on arm64 minikube.
|
||||
|
||||
### Infrastructure
|
||||
|
||||
- Build forgejo-runner container locally via native Dagger pipeline instead of pulling from upstream.
|
||||
- Build kube-state-metrics container locally (Dockerfile + nix) from forge mirror, replacing upstream registry.k8s.io image on both indri and ringtail.
|
||||
- Upgrade miniflux from 2.2.17 to 2.2.19 and migrate from Dockerfile to native Dagger container.py build (second container after navidrome). Refactor `alpine_runtime()` with `create_user` parameter to support Alpine's built-in nobody user. Pin all mise.toml tool versions to explicit versions instead of "latest".
|
||||
- Migrate Dagger module from .dagger/ to repo root (src/blumeops/) and replace docker_build() with native Dagger pipelines for container builds. Navidrome is the first container migrated, with full build error visibility.
|
||||
- Migrate teslamate container build from legacy Dockerfile to native Dagger container.py.
|
||||
- Add seccomp RuntimeDefault profiles to alloy-k8s and immich pods, resolving 4 unmuted Prowler findings
|
||||
- Full DR recovery from power loss and minikube cluster rebuild. Validated bootstrap procedure, identified circular dependencies (forge.eblu.me, Zot/Authentik OIDC), Tailscale device name collision issues, and documented recovery steps for restart-indri.
|
||||
- Set Frigate preview quality to CRF 8 (from default 1) to reduce preview file sizes and improve review timeline loading over NFS.
|
||||
- Track Fly.io proxy component versions (Tailscale, nginx, Alloy) in service-versions.yaml with new `fly` service type.
|
||||
- Upgrade ArgoCD from v3.3.2 to v3.3.6 (bug-fix patches), SHA-pin install manifest
|
||||
- Upgrade authentik 2026.2.0 → 2026.2.2 (bug-fix patch release)
|
||||
- Upgrade ollama from 0.17.5 to 0.20.4 (adds Gemma 4 support, benchmark tooling, Apple Silicon perf improvements)
|
||||
|
||||
### Documentation
|
||||
|
||||
- Delete outdated install-dagger-on-nix-runner card; add service-versions reference card; clean up zot.md and review-services.md links.
|
||||
- Enhanced the adding-a-service tutorial with kustomization setup, corrected Tailscale ingress format, updated ArgoCD repoURL, and added a step for creating service reference cards.
|
||||
- Review gandi.md: add missing forge.eblu.me CNAME, fix program description, stamp review date.
|
||||
|
||||
|
||||
## [v1.15.4] - 2026-04-06
|
||||
|
||||
### Infrastructure
|
||||
|
||||
- Migrate 1Password Connect from Helm to kustomize (1.8.1 → 1.8.2), completing the no-helm-policy migration.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Rewrite observability stack tutorial: replace Helm instructions with actual kustomize/ArgoCD patterns, fix typos, document Alloy as core component
|
||||
|
||||
|
||||
## [v1.15.3] - 2026-04-05
|
||||
|
||||
### Infrastructure
|
||||
|
||||
- Build Tempo container from source via forge mirror; bump 2.10.1 → 2.10.3
|
||||
- Pin NixOS service versions (forgejo-runner, snowflake, k3s) via `nixpkgs-services` overlay in ringtail flake, preventing silent upgrades from `nix flake update`. Add k3s and minikube to service-versions.yaml tracking. Fix stale nix-container-builder version (was 12.6.4, actually running 12.7.2).
|
||||
- Migrate Immich from Helm chart to kustomize manifests and upgrade from v2.5.6 to v2.6.3
|
||||
- Upgrade Grafana from 12.3.3 to 12.4.2 — patches 7 CVEs including an unauthenticated DoS (CVE-2026-27880).
|
||||
|
||||
### Documentation
|
||||
|
||||
- First compensating control review: verified `single-user-cluster` still in effect. Added aspirational how-to card for PCI DSS evidence collection.
|
||||
- Prowler `--registry` fix merged upstream (PR #10470); initContainer workaround documented as pending release.
|
||||
|
||||
|
||||
## [v1.15.2] - 2026-03-30
|
||||
|
||||
### Features
|
||||
|
||||
- Build custom Kingfisher container from sporked deploy branch, replacing upstream image with locally-built version including --clone-url-base patch.
|
||||
- Add Kingfisher secret scanner as a weekly CronJob scanning all Forgejo repos, with HTML and JSON reports written to sifaka NFS.
|
||||
- Add MongoDB Kingfisher secret scanner as a prek hook alongside TruffleHog for comparative coverage evaluation.
|
||||
- Add spork strategy: floating-branch soft-fork tooling (`mise run spork-create`) and documentation for maintaining local patches against upstream projects.
|
||||
|
||||
### Infrastructure
|
||||
|
||||
- Add compensating controls framework: tracking file, review mise task, and how-to doc. Map all Prowler mutelist entries to named controls with CC: prefixes.
|
||||
- Add Prowler mutelist to suppress expected findings from system components, operator-managed pods, and accepted operational needs. Fix missing seccomp profile on kube-state-metrics.
|
||||
- Borgmatic photos backup: restrict to library/ and upload/ (skip regenerable dirs), add SSH keepalives and checkpoint interval to prevent broken pipe failures on large initial syncs.
|
||||
- Upgrade forgejo-runner from 12.7.0 to 12.7.3 (bug fixes, security dep update). Add service reference card.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add service reference documentation for Kingfisher secret scanner.
|
||||
- Review and update Ansible reference doc: add missing roles, sibling playbooks, and clarify Ansible's role in the IaC stack.
|
||||
|
||||
|
||||
## [v1.15.1] - 2026-03-28
|
||||
|
||||
### Features
|
||||
|
|
|
|||
154
CLAUDE.md
154
CLAUDE.md
|
|
@ -1,153 +1 @@
|
|||
# CLAUDE.md
|
||||
|
||||
Guidance for Claude Code working in this repository. See also [[ai-assistance-guide]].
|
||||
|
||||
## Overview
|
||||
|
||||
blumeops is Erich Blume's GitOps repository for personal infrastructure, orchestrated via tailnet `tail8d86e.ts.net`.
|
||||
|
||||
**CRITICAL: Public repo at github.com/eblume/blumeops - never commit secrets!**
|
||||
|
||||
**Shell:** The user's shell is **fish**. Use `$status` not `$?` for exit codes. Use fish syntax in interactive examples.
|
||||
|
||||
## Rules
|
||||
|
||||
1. **Always run `mise run ai-docs` at session start**
|
||||
This will refresh your context with important information you will be assumed to know and follow.
|
||||
**Read the full output** — never truncate, pipe to `head`/`tail`, or skip sections.
|
||||
For problems with a large surface area, ask the user if `mise run ai-sources` should also be run — it concatenates all non-doc source files (~270K tokens) for deep codebase context.
|
||||
2. **Always use `--context=minikube-indri` with kubectl** (or `--context=k3s-ringtail` for ringtail services) - work contexts must never be touched
|
||||
3. **Classify the change as C0/C1/C2 before starting** (see below) — this determines branching and PR requirements
|
||||
4. **Feature branches + PRs for C1/C2** - checkout main, pull, create branch, open PR via `tea pr create`. C0 goes direct to main.
|
||||
5. **Check PR comments with `mise run pr-comments <pr_number>`** before proceeding
|
||||
6. **Add changelog fragments (all change levels)** - `docs/changelog.d/<name>.<type>.md`
|
||||
Types: `feature`, `bugfix`, `infra`, `doc`, `ai`, `misc`
|
||||
Applies to C0, C1, and C2 whenever the change is user-visible or noteworthy.
|
||||
- **C1/C2:** Use branch name: `<branch>.<type>.md`
|
||||
- **C0:** Use orphan prefix: `+<descriptive-slug>.<type>.md` (avoids `main.*` collisions)
|
||||
7. **Test before applying** - dry runs (`--check --diff`), syntax checks, `ssh indri '...'`
|
||||
8. **Wait for user review before deploying** (C1/C2)
|
||||
9. **Never merge PRs or push to main without explicit request** (C0 commits to main are fine)
|
||||
10. **Verify deployments** - `mise run services-check`
|
||||
|
||||
## Change Classification
|
||||
|
||||
Before starting work, classify the change:
|
||||
|
||||
| Class | Name | When to use | Key trait |
|
||||
|-------|------|-------------|-----------|
|
||||
| **C0** | Quick Fix | Small, low-risk, fix-forward safe | Direct to main, no PR |
|
||||
| **C1** | Human Review | Moderate complexity or risk | Feature branch + PR, docs-first |
|
||||
| **C2** | Mikado Chain | Multi-phase, multi-session, high complexity | Mikado Branch Invariant |
|
||||
|
||||
**C0** — commit directly to main. No branch or PR needed. Fix forward if problems arise.
|
||||
|
||||
**C1** — feature branch with early PR. Search related docs first, write documentation changes before code, deploy from the unmerged branch (ArgoCD `--revision`, Ansible from checkout). Upgrade to C2 if complexity spirals.
|
||||
|
||||
**C2** — branch `mikado/<chain-stem>` governed by the Mikado Branch Invariant: all card commits first, then code progress, then card closures. Commits use `C2(<chain>): plan/impl/close/finalize` convention. Reset the branch when new prerequisites are discovered. Resume with `mise run docs-mikado --resume`.
|
||||
|
||||
See [[agent-change-process]] for the full methodology.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
./docs/ # documentation (Diataxis, Quartz)
|
||||
./docs/changelog.d/ # towncrier fragments
|
||||
./.dagger/ # dagger pipelines
|
||||
./.forgejo/ # forgejo-runner actions and workflows
|
||||
./mise-tasks/ # scripts via `mise run`
|
||||
./ansible/playbooks/ # ansible (indri.yml primary)
|
||||
./ansible/roles/ # indri service roles
|
||||
./argocd/apps/ # ArgoCD Application definitions
|
||||
./argocd/manifests/ # k8s manifests per service
|
||||
./fly/ # fly.io proxy for public routing
|
||||
./pulumi/ # Pulumi IaC (tailnet ACLs, dns, cloud)
|
||||
~/.config/{nvim,fish} # user's shell config, managed by chezmoi
|
||||
~/code/personal/ # user's projects
|
||||
~/code/personal/zk # user's Obsidian-sync managed zettelkasten. Potential source for reference data.
|
||||
~/code/3rd/ # mirrored external projects
|
||||
~/code/work # FORBIDDEN
|
||||
```
|
||||
Other code paths will be listed via ai-docs, this is just an overview. When you
|
||||
encounter wiki-links (`[[like-this]]`) it is referring to docs/ cards.
|
||||
|
||||
## Service Deployment
|
||||
|
||||
### Kubernetes (ArgoCD)
|
||||
|
||||
Most services run in minikube on indri via ArgoCD (app-of-apps, manual sync). GPU workloads (Frigate, ntfy) run on ringtail's k3s cluster, also managed by ArgoCD.
|
||||
|
||||
**PR workflow:**
|
||||
1. Create branch, modify `argocd/manifests/<service>/`
|
||||
2. Push. Sync 'apps' app if service definition changed (set --revision to branch).
|
||||
3. Test on branch: `argocd app set <service> --revision <branch> && argocd app sync <service>`
|
||||
4. After merge: `argocd app set <service> --revision main && argocd app sync <service>`
|
||||
|
||||
**Commands:** `argocd app list|get|diff|sync <app>`
|
||||
|
||||
**Login:** `argocd login argocd.ops.eblu.me --username admin --password "$(op read 'op://vg6xf6vvfmoh5hqjjhlhbeoaie/srogeebssulhtb6tnqd7ls6qey/password')"`
|
||||
|
||||
### Indri (Ansible)
|
||||
|
||||
Native services: Forgejo, Zot, Caddy, Borgmatic, Alloy
|
||||
|
||||
```fish
|
||||
mise run provision-indri # full
|
||||
mise run provision-indri -- --tags <role> # specific
|
||||
mise run provision-indri -- --check --diff # dry run
|
||||
```
|
||||
|
||||
### Routing
|
||||
|
||||
| Domain | Mechanism | Reachable from |
|
||||
|--------|-----------|----------------|
|
||||
| `*.eblu.me` | Fly.io proxy (Tailscale tunnel) | public internet |
|
||||
| `*.ops.eblu.me` | Caddy on indri | k8s pods, containers, tailnet |
|
||||
| `*.tail8d86e.ts.net` | Tailscale MagicDNS | tailnet clients only |
|
||||
|
||||
Check tailscale serve: `ssh indri 'tailscale serve status --json'`
|
||||
|
||||
## Container Releases
|
||||
|
||||
```fish
|
||||
mise run container-list # show images/tags
|
||||
mise run container-release <name> <version> # tag and build
|
||||
```
|
||||
The goal is to eventually use only locally built containers in all cases, with
|
||||
full supply chain control via forge.ops.eblu.me repositories, mirroring source
|
||||
from upstream.
|
||||
|
||||
## Third-Party Projects
|
||||
|
||||
Ask user to mirror on forge first, then clone to `~/code/3rd/<project>/`.
|
||||
|
||||
### Sporked Projects
|
||||
|
||||
Some mirrored projects are "sporked" — a floating-branch soft-fork strategy
|
||||
where local patches are continuously rebased on top of upstream. See
|
||||
[[spork-strategy]] and [[create-a-spork]] for the full methodology.
|
||||
|
||||
Sporked projects live in `~/code/3rd/<project>/` with three remotes:
|
||||
`origin` (eblume/ fork on forge), `mirror` (mirrors/ on forge), `upstream`
|
||||
(canonical). The `blumeops` branch is the default; `deploy` merges everything.
|
||||
|
||||
Create a new spork: `mise run spork-create <mirror-name>`
|
||||
|
||||
## Task Discovery
|
||||
|
||||
```fish
|
||||
mise run blumeops-tasks # fetch from Todoist, sorted by priority
|
||||
```
|
||||
Most tasks are stored in `./mise-tasks/`. For scripts with any logic or
|
||||
complexity, use uv run --script 's with explicit dependencies. Complex
|
||||
workflows with artifacts should become dagger pipelines. Mise tasks are for
|
||||
development processes and operations - tools for the user or the agent.
|
||||
|
||||
## Credentials
|
||||
|
||||
Root store is 1Password. Never grab directly - use existing patterns (ansible
|
||||
pre_tasks, external-secrets, scripts with `op` CLI). It's ok to use `op item
|
||||
get` without `--reveal` to explore what secrets are available, however.
|
||||
|
||||
Prefer `op read "op://vault/item/field"` over `op item get --fields` to avoid
|
||||
quoting issues with multi-line values.
|
||||
@AGENTS.md
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ Tools and configuration for Erich Blume's personal infrastructure, orchestrated
|
|||
across a Tailscale tailnet.
|
||||
|
||||
This is a homelab, but it's also a testing ground for AI-assisted
|
||||
infrastructure development. Much of this codebase was co-authored with [Claude
|
||||
infrastructure development. Much of this codebase was initially co-authored with [Claude
|
||||
Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview),
|
||||
and the repo places heavy emphasis on documentation, process, and change
|
||||
classification to make that collaboration work well. I don't know entirely how
|
||||
|
|
@ -77,7 +77,7 @@ mise run container-list # list tracked container images
|
|||
## AI-assisted development
|
||||
|
||||
This repo is designed to be worked on by both humans and AI agents. The
|
||||
[`CLAUDE.md`](CLAUDE.md) file provides instructions for Claude Code, and the
|
||||
[`AGENTS.md`](AGENTS.md) file provides shared instructions for agentic tools, and the
|
||||
[`docs/tutorials/ai-assistance-guide.md`](docs/tutorials/ai-assistance-guide.md)
|
||||
explains the full workflow.
|
||||
|
||||
|
|
@ -87,7 +87,7 @@ Changes are classified before starting work:
|
|||
- **C1** - feature branch + PR, documentation written before code
|
||||
- **C2** - multi-phase work using the Mikado method for dependency tracking
|
||||
|
||||
See the [agent change process](docs/how-to/agent-change-process.md) for
|
||||
See the [agent change process](docs/explanation/agent-change-process.md) for
|
||||
details.
|
||||
|
||||
## License
|
||||
|
|
|
|||
|
|
@ -212,6 +212,23 @@
|
|||
no_log: true
|
||||
tags: [forgejo_metrics]
|
||||
|
||||
# Devpi root password (PyPI mirror admin)
|
||||
- name: Fetch devpi root password
|
||||
ansible.builtin.command:
|
||||
cmd: op read "op://vg6xf6vvfmoh5hqjjhlhbeoaie/kyhzfifryqnuk7jeyibmmjvxxm/add more/root password"
|
||||
delegate_to: localhost
|
||||
register: _devpi_root_password
|
||||
changed_when: false
|
||||
no_log: true
|
||||
check_mode: false
|
||||
tags: [devpi]
|
||||
|
||||
- name: Set devpi root password fact
|
||||
ansible.builtin.set_fact:
|
||||
devpi_root_password: "{{ _devpi_root_password.stdout }}"
|
||||
no_log: true
|
||||
tags: [devpi]
|
||||
|
||||
roles:
|
||||
- role: alloy
|
||||
tags: alloy
|
||||
|
|
@ -227,6 +244,8 @@
|
|||
tags: zot
|
||||
- role: zot_metrics
|
||||
tags: zot_metrics
|
||||
- role: devpi
|
||||
tags: devpi
|
||||
- role: minikube
|
||||
tags: minikube
|
||||
- role: minikube_metrics
|
||||
|
|
@ -237,5 +256,11 @@
|
|||
tags: jellyfin_metrics
|
||||
- role: forgejo_metrics
|
||||
tags: forgejo_metrics
|
||||
- role: cv
|
||||
tags: cv
|
||||
- role: docs
|
||||
tags: docs
|
||||
- role: heph
|
||||
tags: heph
|
||||
- role: caddy
|
||||
tags: caddy
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@
|
|||
tasks:
|
||||
- name: Ensure blumeops repo is present
|
||||
ansible.builtin.git:
|
||||
repo: "https://forge.eblu.me/eblume/blumeops.git"
|
||||
repo: "https://forge.ops.eblu.me/eblume/blumeops.git"
|
||||
dest: /etc/blumeops
|
||||
version: "{{ ringtail_commit | default('main') }}"
|
||||
force: true
|
||||
|
|
|
|||
|
|
@ -101,6 +101,10 @@ alloy_op_vault: vg6xf6vvfmoh5hqjjhlhbeoaie
|
|||
alloy_op_postgres_item: guxu3j7ajhjyey6xxl2ovsl2ui
|
||||
alloy_op_postgres_field: alloy-user-pw
|
||||
|
||||
# Forgejo metrics collection
|
||||
alloy_collect_forgejo: true
|
||||
alloy_forgejo_port: 3001
|
||||
|
||||
# macOS power metrics collection (via powermetrics, requires root)
|
||||
alloy_collect_power_metrics: true
|
||||
alloy_power_metrics_script: /usr/local/bin/macos-power-metrics
|
||||
|
|
|
|||
|
|
@ -74,6 +74,18 @@ prometheus.scrape "zot" {
|
|||
}
|
||||
{% endif %}
|
||||
|
||||
{% if alloy_collect_forgejo | default(false) %}
|
||||
// ============== FORGEJO METRICS ==============
|
||||
|
||||
// Scrape Forgejo's native metrics endpoint
|
||||
prometheus.scrape "forgejo" {
|
||||
targets = [{"__address__" = "localhost:{{ alloy_forgejo_port }}"}]
|
||||
metrics_path = "/metrics"
|
||||
forward_to = [prometheus.relabel.instance.receiver]
|
||||
scrape_interval = "{{ alloy_scrape_interval }}"
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
{% if alloy_collect_logs %}
|
||||
// ============== LOG COLLECTION ==============
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,16 @@ borgmatic_log_dir: /Users/erichblume/Library/Logs
|
|||
# Full path to borg binary since LaunchAgent doesn't have homebrew in PATH
|
||||
borgmatic_local_path: /opt/homebrew/bin/borg
|
||||
|
||||
# Borgmatic version — keep in sync with mise.toml in the repo root.
|
||||
# Ansible installs this via `mise install` so indri doesn't need the repo cloned.
|
||||
borgmatic_version: "2.1.4"
|
||||
|
||||
# Full path to borgmatic binary — called directly by LaunchAgents to avoid
|
||||
# routing through mise, which triggers macOS TCC permission dialogs for
|
||||
# protected folders (e.g. ~/Documents) that hang headless LaunchAgent sessions.
|
||||
# Uses mise's "latest" symlink so version bumps don't break the LaunchAgent path.
|
||||
borgmatic_bin: /Users/erichblume/.local/share/mise/installs/pipx-borgmatic/latest/bin/borgmatic
|
||||
|
||||
# Schedule: runs daily at 2:00 AM
|
||||
borgmatic_schedule_hour: 2
|
||||
borgmatic_schedule_minute: 0
|
||||
|
|
@ -17,6 +27,9 @@ borgmatic_source_directories:
|
|||
- /Users/erichblume/.config/borgmatic
|
||||
- /Users/erichblume/Documents
|
||||
- /Users/erichblume/.local/share/borgmatic/k8s-dumps
|
||||
# Shower app prize-photo uploads (sifaka SMB mount). Mounted manually
|
||||
# on indri via Finder — see docs/how-to/operations/shower-app.md.
|
||||
- /Volumes/shower
|
||||
|
||||
# Backup repositories
|
||||
borgmatic_repositories:
|
||||
|
|
@ -43,7 +56,17 @@ borgmatic_k8s_sqlite_dumps:
|
|||
namespace: mealie
|
||||
label_selector: app=mealie
|
||||
db_path: /app/data/mealie.db
|
||||
context: minikube
|
||||
# migrated to ringtail (wave-1); ssh to ringtail and run k3s kubectl
|
||||
# there, same as shower below.
|
||||
target: ssh:eblume@ringtail
|
||||
- name: shower
|
||||
namespace: shower
|
||||
label_selector: app=shower
|
||||
db_path: /app/data/db.sqlite3
|
||||
# ssh to ringtail and run k3s kubectl there — avoids needing a
|
||||
# ringtail kubeconfig on indri. k3s.yaml on ringtail is
|
||||
# world-readable (mode 644), so no sudo required.
|
||||
target: ssh:eblume@ringtail
|
||||
|
||||
# Exclude patterns
|
||||
borgmatic_exclude_patterns: []
|
||||
|
|
@ -80,14 +103,19 @@ borgmatic_postgresql_databases:
|
|||
hostname: pg.ops.eblu.me
|
||||
port: 5432
|
||||
username: borgmatic
|
||||
- name: teslamate
|
||||
hostname: pg.ops.eblu.me
|
||||
port: 5432
|
||||
username: borgmatic
|
||||
- name: authentik
|
||||
hostname: pg.ops.eblu.me
|
||||
port: 5432
|
||||
username: borgmatic
|
||||
# migrated to ringtail blumeops-pg (wave-1); port 5434 = Caddy L4 route
|
||||
- name: teslamate
|
||||
hostname: pg.ops.eblu.me
|
||||
port: 5434
|
||||
username: borgmatic
|
||||
- name: paperless
|
||||
hostname: pg.ops.eblu.me
|
||||
port: 5434
|
||||
username: borgmatic
|
||||
# immich-pg cluster (VectorChord) via Caddy L4 on port 5433
|
||||
- name: immich
|
||||
hostname: pg.ops.eblu.me
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
---
|
||||
# Note: borgmatic is installed via mise (pipx), not managed here.
|
||||
# This role manages the config file and scheduled LaunchAgent.
|
||||
# Borgmatic is installed via mise (pipx) and called directly by LaunchAgents.
|
||||
# This role manages installation, config, and the scheduled LaunchAgents.
|
||||
|
||||
- name: Install borgmatic via mise
|
||||
ansible.builtin.command: mise install pipx:borgmatic@{{ borgmatic_version }}
|
||||
register: borgmatic_install
|
||||
changed_when: "'installed' in borgmatic_install.stderr"
|
||||
|
||||
- name: Ensure borgmatic config directory exists
|
||||
ansible.builtin.file:
|
||||
|
|
@ -14,8 +19,10 @@
|
|||
ansible.builtin.copy:
|
||||
content: |
|
||||
# Managed by ansible (borgmatic role) - k8s PostgreSQL backup credentials
|
||||
# 5432 = minikube blumeops-pg, 5433 = immich-pg, 5434 = ringtail blumeops-pg
|
||||
pg.ops.eblu.me:5432:*:borgmatic:{{ borgmatic_db_password }}
|
||||
pg.ops.eblu.me:5433:*:borgmatic:{{ borgmatic_db_password }}
|
||||
pg.ops.eblu.me:5434:*:borgmatic:{{ borgmatic_db_password }}
|
||||
dest: ~/.pgpass
|
||||
mode: '0600'
|
||||
no_log: true
|
||||
|
|
@ -44,6 +51,20 @@
|
|||
mode: '0700'
|
||||
when: borgmatic_k8s_sqlite_dumps | length > 0
|
||||
|
||||
- name: Ensure ~/bin exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ ansible_env.HOME }}/bin"
|
||||
state: directory
|
||||
mode: '0755'
|
||||
when: borgmatic_k8s_sqlite_dumps | length > 0
|
||||
|
||||
- name: Deploy k8s SQLite dump helper script
|
||||
ansible.builtin.template:
|
||||
src: k8s-sqlite-dump.sh.j2
|
||||
dest: "{{ ansible_env.HOME }}/bin/borgmatic-k8s-sqlite-dump"
|
||||
mode: '0755'
|
||||
when: borgmatic_k8s_sqlite_dumps | length > 0
|
||||
|
||||
- name: Deploy borgmatic configuration
|
||||
ansible.builtin.template:
|
||||
src: config.yaml.j2
|
||||
|
|
|
|||
|
|
@ -14,10 +14,7 @@
|
|||
</dict>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/opt/homebrew/opt/mise/bin/mise</string>
|
||||
<string>x</string>
|
||||
<string>--</string>
|
||||
<string>borgmatic</string>
|
||||
<string>{{ borgmatic_bin }}</string>
|
||||
<string>--config</string>
|
||||
<string>{{ borgmatic_photos_config }}</string>
|
||||
<string>create</string>
|
||||
|
|
|
|||
|
|
@ -14,10 +14,7 @@
|
|||
</dict>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/opt/homebrew/opt/mise/bin/mise</string>
|
||||
<string>x</string>
|
||||
<string>--</string>
|
||||
<string>borgmatic</string>
|
||||
<string>{{ borgmatic_bin }}</string>
|
||||
<string>--config</string>
|
||||
<string>{{ borgmatic_config }}</string>
|
||||
<string>create</string>
|
||||
|
|
|
|||
|
|
@ -32,12 +32,20 @@ exclude_patterns:
|
|||
encryption_passcommand: {{ borgmatic_encryption_passcommand }}
|
||||
|
||||
{% if borgmatic_k8s_sqlite_dumps %}
|
||||
# Pre-backup: dump SQLite databases from k8s pods
|
||||
# Uses sqlite3 .backup for a safe, consistent copy (no corruption from concurrent writes)
|
||||
# Pre-backup: dump SQLite databases from k8s pods.
|
||||
# Uses sqlite3.backup() for a safe, consistent copy.
|
||||
#
|
||||
# Quoting/escaping is delegated to ~/bin/borgmatic-k8s-sqlite-dump
|
||||
# (deployed by the borgmatic ansible role). Each entry's `target`
|
||||
# is either:
|
||||
# - local:<context> -> local kubectl with --context (mealie etc.)
|
||||
# - ssh:<user@host> -> ssh + k3s kubectl on the cluster host,
|
||||
# used for ringtail since indri's kubeconfig
|
||||
# deliberately doesn't carry that context.
|
||||
before_backup:
|
||||
- mkdir -p {{ borgmatic_k8s_dump_dir }}
|
||||
{% for db in borgmatic_k8s_sqlite_dumps %}
|
||||
- /opt/homebrew/bin/kubectl --context={{ db.context }} exec -n {{ db.namespace }} deploy/{{ db.name }} -- python3 -c "import sqlite3; sqlite3.connect('{{ db.db_path }}').backup(sqlite3.connect('/tmp/{{ db.name }}-backup.db'))" && /opt/homebrew/bin/kubectl --context={{ db.context }} cp {{ db.namespace }}/$(/opt/homebrew/bin/kubectl --context={{ db.context }} get pod -n {{ db.namespace }} -l {{ db.label_selector }} -o jsonpath='{.items[0].metadata.name}'):/tmp/{{ db.name }}-backup.db {{ borgmatic_k8s_dump_dir }}/{{ db.name }}.db
|
||||
- {{ ansible_env.HOME }}/bin/borgmatic-k8s-sqlite-dump {{ db.target }} {{ db.namespace }} {{ db.label_selector }} {{ db.db_path }} {{ db.name }} {{ borgmatic_k8s_dump_dir }}/{{ db.name }}.db
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
|
|
|
|||
73
ansible/roles/borgmatic/templates/k8s-sqlite-dump.sh.j2
Normal file
73
ansible/roles/borgmatic/templates/k8s-sqlite-dump.sh.j2
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
#!/usr/bin/env bash
|
||||
# {{ ansible_managed }}
|
||||
#
|
||||
# Helper script invoked by borgmatic's before_backup hook to capture a
|
||||
# k8s pod's SQLite database. Keeps the borgmatic config readable by
|
||||
# pulling all the quoting out of YAML.
|
||||
#
|
||||
# Usage:
|
||||
# borgmatic-k8s-sqlite-dump <target> <namespace> <selector> \
|
||||
# <db_path> <name> <dump_target>
|
||||
#
|
||||
# <target> is one of:
|
||||
# local:<context> - run local kubectl with --context=<context>
|
||||
# ssh:<user@host> - ssh to host and run k3s kubectl there
|
||||
# (no indri-side kubeconfig needed)
|
||||
#
|
||||
# <namespace> - k8s namespace of the pod
|
||||
# <selector> - label selector to find the pod (e.g. app=shower)
|
||||
# <db_path> - absolute path inside the pod to the SQLite DB
|
||||
# <name> - short name used for temp filenames
|
||||
# <dump_target> - file on this host to receive the dump
|
||||
set -euo pipefail
|
||||
|
||||
target=${1:?missing target}
|
||||
namespace=${2:?missing namespace}
|
||||
selector=${3:?missing selector}
|
||||
db_path=${4:?missing db path}
|
||||
name=${5:?missing name}
|
||||
dump_target=${6:?missing dump target}
|
||||
|
||||
# Stage the backup next to the source DB (a guaranteed-writable volume);
|
||||
# minimal nix images (e.g. mealie) have no /tmp.
|
||||
pod_tmp="$(dirname "$db_path")/.borgmatic-backup-${name}.db"
|
||||
|
||||
python_backup='import sqlite3; sqlite3.connect("'"$db_path"'").backup(sqlite3.connect("'"$pod_tmp"'"))'
|
||||
|
||||
mode=${target%%:*}
|
||||
ref=${target#*:}
|
||||
|
||||
case "$mode" in
|
||||
local)
|
||||
# Pulls dump bytes out via "kubectl exec -- cat" rather than
|
||||
# "kubectl cp", which would otherwise need tar inside the pod
|
||||
# (nix-built images like shower don't bundle tar).
|
||||
context=$ref
|
||||
kubectl="/opt/homebrew/bin/kubectl --context=$context -n $namespace"
|
||||
pod=$($kubectl get pod -l "$selector" \
|
||||
-o jsonpath='{.items[0].metadata.name}')
|
||||
$kubectl exec "$pod" -- python3 -c "$python_backup"
|
||||
$kubectl exec "$pod" -- cat "$pod_tmp" > "$dump_target"
|
||||
$kubectl exec "$pod" -- rm -f "$pod_tmp"
|
||||
;;
|
||||
ssh)
|
||||
host=$ref
|
||||
# Force bash on the remote (user's login shell on ringtail is
|
||||
# fish). Pipe the script via stdin to dodge nested quoting.
|
||||
# The dump bytes come back over the ssh stdout stream — no
|
||||
# intermediate scp, no tar requirement in the pod.
|
||||
ssh "$host" bash <<EOF > "$dump_target"
|
||||
set -euo pipefail
|
||||
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
|
||||
pod=\$(k3s kubectl -n "$namespace" get pod -l "$selector" -o jsonpath='{.items[0].metadata.name}')
|
||||
k3s kubectl -n "$namespace" exec "\$pod" -- python3 -c '$python_backup' 1>&2
|
||||
k3s kubectl -n "$namespace" exec "\$pod" -- cat "$pod_tmp"
|
||||
k3s kubectl -n "$namespace" exec "\$pod" -- rm -f "$pod_tmp" 1>&2
|
||||
EOF
|
||||
;;
|
||||
*)
|
||||
echo "borgmatic-k8s-sqlite-dump: unknown target mode: $mode" >&2
|
||||
echo " expected local:<context> or ssh:<user@host>" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
|
@ -51,7 +51,10 @@ caddy_services:
|
|||
backend: "https://feed.tail8d86e.ts.net"
|
||||
- name: devpi
|
||||
host: "pypi.{{ caddy_domain }}"
|
||||
backend: "https://pypi.tail8d86e.ts.net"
|
||||
backend: "http://localhost:3141"
|
||||
- name: heph
|
||||
host: "heph.{{ caddy_domain }}"
|
||||
backend: "http://localhost:8787" # hephaestus hub (server mode) + PWA shell
|
||||
- name: kiwix
|
||||
host: "kiwix.{{ caddy_domain }}"
|
||||
backend: "https://kiwix.tail8d86e.ts.net"
|
||||
|
|
@ -72,16 +75,23 @@ caddy_services:
|
|||
backend: "https://go.tail8d86e.ts.net"
|
||||
- name: docs
|
||||
host: "docs.{{ caddy_domain }}"
|
||||
backend: "https://docs.tail8d86e.ts.net"
|
||||
kind: static
|
||||
root: "{{ docs_content_dir }}"
|
||||
try_html: true # Quartz: path → path/ → path.html → 404.html
|
||||
- name: cv
|
||||
host: "cv.{{ caddy_domain }}"
|
||||
backend: "https://cv.tail8d86e.ts.net"
|
||||
kind: static
|
||||
root: "{{ cv_content_dir }}"
|
||||
download_paths:
|
||||
- path: /resume.pdf
|
||||
filename: erich-blume-resume.pdf
|
||||
- name: nvr
|
||||
host: "nvr.{{ caddy_domain }}"
|
||||
backend: "https://nvr.tail8d86e.ts.net"
|
||||
- name: authentik
|
||||
host: "authentik.{{ caddy_domain }}"
|
||||
backend: "https://authentik.tail8d86e.ts.net"
|
||||
cache_policy: spa
|
||||
- name: ntfy
|
||||
host: "ntfy.{{ caddy_domain }}"
|
||||
backend: "https://ntfy.tail8d86e.ts.net"
|
||||
|
|
@ -91,6 +101,12 @@ caddy_services:
|
|||
- name: mealie
|
||||
host: "meals.{{ caddy_domain }}"
|
||||
backend: "https://meals.tail8d86e.ts.net"
|
||||
- name: paperless
|
||||
host: "paperless.{{ caddy_domain }}"
|
||||
backend: "https://paperless.tail8d86e.ts.net"
|
||||
- name: shower
|
||||
host: "shower.{{ caddy_domain }}"
|
||||
backend: "https://shower.tail8d86e.ts.net"
|
||||
- name: sifaka
|
||||
host: "nas.{{ caddy_domain }}"
|
||||
backend: "http://sifaka:5000"
|
||||
|
|
@ -104,6 +120,8 @@ caddy_tcp_services:
|
|||
backend: "pg.tail8d86e.ts.net:5432" # PostgreSQL (blumeops-pg)
|
||||
- port: 5433
|
||||
backend: "immich-pg.tail8d86e.ts.net:5432" # PostgreSQL (immich-pg)
|
||||
- port: 5434
|
||||
backend: "blumeops-pg-ringtail.tail8d86e.ts.net:5432" # PostgreSQL (blumeops-pg on ringtail)
|
||||
- port: "{{ sifaka_node_exporter_port }}"
|
||||
backend: "sifaka:{{ sifaka_node_exporter_port }}" # Sifaka node_exporter
|
||||
- port: "{{ sifaka_smartctl_exporter_port }}"
|
||||
|
|
|
|||
|
|
@ -31,6 +31,33 @@
|
|||
{% for service in caddy_services %}
|
||||
@{{ service.name }} host {{ service.host }}
|
||||
handle @{{ service.name }} {
|
||||
{% if service.kind | default('proxy') == 'static' %}
|
||||
root * {{ service.root }}
|
||||
encode gzip
|
||||
# Long-cache fingerprinted assets; everything else stays default.
|
||||
@{{ service.name }}_assets path_regexp \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2)$
|
||||
header @{{ service.name }}_assets Cache-Control "public, max-age=31536000, immutable"
|
||||
{% for dl in service.download_paths | default([]) %}
|
||||
@{{ service.name }}_dl{{ loop.index }} path {{ dl.path }}
|
||||
header @{{ service.name }}_dl{{ loop.index }} Content-Disposition `attachment; filename="{{ dl.filename }}"`
|
||||
{% endfor %}
|
||||
{% if service.try_html | default(false) %}
|
||||
# Quartz clean URLs: path → path/ → path.html → /404.html (200).
|
||||
# Caddy's handle_errors is a top-level directive and can't live in
|
||||
# this nested handle, so the 404 page rides as the final try_files
|
||||
# candidate (served with 200 — acceptable for a human-facing 404).
|
||||
try_files {path} {path}/ {path}.html /404.html
|
||||
{% endif %}
|
||||
file_server
|
||||
{% else %}
|
||||
{% if service.cache_policy | default('') == 'spa' %}
|
||||
# SPA cache policy: hashed static assets are immutable, HTML must revalidate.
|
||||
# Prevents stale HTML from referencing chunk hashes that no longer exist.
|
||||
@{{ service.name }}_static path /static/dist/*
|
||||
header @{{ service.name }}_static Cache-Control "public, max-age=31536000, immutable"
|
||||
@{{ service.name }}_html path /if/*
|
||||
header @{{ service.name }}_html Cache-Control "no-cache"
|
||||
{% endif %}
|
||||
{% if service.backend.startswith('https://') %}
|
||||
reverse_proxy {{ service.backend }} {
|
||||
# Caddy v2.11+ rewrites Host to upstream for HTTPS backends.
|
||||
|
|
@ -39,6 +66,7 @@
|
|||
}
|
||||
{% else %}
|
||||
reverse_proxy {{ service.backend }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
}
|
||||
|
||||
|
|
|
|||
10
ansible/roles/cv/defaults/main.yml
Normal file
10
ansible/roles/cv/defaults/main.yml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
# CV / resume static site (native, replaces minikube Deployment)
|
||||
# Caddy serves cv_content_dir directly via the static-kind service block.
|
||||
|
||||
cv_version: "v1.0.3"
|
||||
cv_release_url: "https://forge.ops.eblu.me/api/packages/eblume/generic/cv/{{ cv_version }}/cv-{{ cv_version }}.tar.gz"
|
||||
|
||||
cv_home: /Users/erichblume/blumeops/cv
|
||||
cv_content_dir: "{{ cv_home }}/content"
|
||||
cv_version_sentinel: "{{ cv_home }}/.installed-version"
|
||||
57
ansible/roles/cv/tasks/main.yml
Normal file
57
ansible/roles/cv/tasks/main.yml
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
---
|
||||
# cv role — download and extract the CV release tarball into cv_content_dir.
|
||||
# Caddy serves the directory directly; there is no daemon to manage.
|
||||
#
|
||||
# Idempotency: a sentinel file records the installed cv_version. The
|
||||
# download/extract steps only run when the sentinel doesn't match cv_version.
|
||||
#
|
||||
# We use curl rather than ansible.builtin.get_url because the forge generic-
|
||||
# packages endpoint returns 405 on HEAD requests, which get_url issues before
|
||||
# downloading.
|
||||
|
||||
- name: Ensure cv home exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ cv_home }}"
|
||||
state: directory
|
||||
mode: '0755'
|
||||
|
||||
- name: Read installed cv version sentinel
|
||||
ansible.builtin.slurp:
|
||||
src: "{{ cv_version_sentinel }}"
|
||||
register: cv_installed_raw
|
||||
failed_when: false
|
||||
changed_when: false
|
||||
|
||||
- name: Set installed cv version fact
|
||||
ansible.builtin.set_fact:
|
||||
cv_installed_version: >-
|
||||
{{ (cv_installed_raw.content | b64decode).strip()
|
||||
if (cv_installed_raw.content is defined) else '' }}
|
||||
|
||||
- name: Recreate cv content dir
|
||||
ansible.builtin.file:
|
||||
path: "{{ cv_content_dir }}"
|
||||
state: "{{ item }}"
|
||||
mode: '0755'
|
||||
loop:
|
||||
- absent
|
||||
- directory
|
||||
when: cv_installed_version != cv_version
|
||||
|
||||
- name: Download and extract cv release tarball
|
||||
ansible.builtin.shell:
|
||||
cmd: >-
|
||||
set -euo pipefail;
|
||||
curl -fsSL {{ cv_release_url | quote }} -o {{ cv_home }}/cv.tar.gz &&
|
||||
tar -xzf {{ cv_home }}/cv.tar.gz -C {{ cv_content_dir }} &&
|
||||
rm -f {{ cv_home }}/cv.tar.gz
|
||||
executable: /bin/bash
|
||||
when: cv_installed_version != cv_version
|
||||
changed_when: true
|
||||
|
||||
- name: Write cv version sentinel
|
||||
ansible.builtin.copy:
|
||||
content: "{{ cv_version }}\n"
|
||||
dest: "{{ cv_version_sentinel }}"
|
||||
mode: '0644'
|
||||
when: cv_installed_version != cv_version
|
||||
21
ansible/roles/devpi/defaults/main.yml
Normal file
21
ansible/roles/devpi/defaults/main.yml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
---
|
||||
# devpi PyPI caching mirror (native launchd, replaces minikube StatefulSet)
|
||||
|
||||
devpi_home: /Users/erichblume/devpi
|
||||
devpi_venv: "{{ devpi_home }}/venv"
|
||||
devpi_server_dir: "{{ devpi_home }}/server-dir"
|
||||
devpi_binary: "{{ devpi_venv }}/bin/devpi-server"
|
||||
devpi_init_binary: "{{ devpi_venv }}/bin/devpi-init"
|
||||
|
||||
devpi_python_version: "3.12"
|
||||
devpi_server_version: "6.19.3"
|
||||
devpi_web_version: "5.0.2"
|
||||
|
||||
devpi_host: 127.0.0.1
|
||||
devpi_port: 3141
|
||||
devpi_outside_url: "https://pypi.ops.eblu.me"
|
||||
|
||||
devpi_log_dir: /Users/erichblume/Library/Logs
|
||||
|
||||
# uv binary on indri — mise shim so version bumps via `mise upgrade uv` flow through transparently
|
||||
devpi_uv_binary: /Users/erichblume/.local/share/mise/shims/uv
|
||||
6
ansible/roles/devpi/handlers/main.yml
Normal file
6
ansible/roles/devpi/handlers/main.yml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
- name: Restart devpi
|
||||
ansible.builtin.shell: |
|
||||
launchctl unload ~/Library/LaunchAgents/mcquack.eblume.devpi.plist 2>/dev/null || true
|
||||
launchctl load ~/Library/LaunchAgents/mcquack.eblume.devpi.plist
|
||||
changed_when: true
|
||||
71
ansible/roles/devpi/tasks/main.yml
Normal file
71
ansible/roles/devpi/tasks/main.yml
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
---
|
||||
# devpi role — devpi-server in a uv-managed venv, run via LaunchAgent.
|
||||
# Replaces the prior minikube StatefulSet; see [[devpi-on-indri]].
|
||||
#
|
||||
# The root password is fetched in the indri.yml playbook pre_tasks and
|
||||
# exposed as `devpi_root_password`.
|
||||
|
||||
- name: Ensure devpi home exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ devpi_home }}"
|
||||
state: directory
|
||||
mode: '0755'
|
||||
|
||||
- name: Ensure devpi server-dir exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ devpi_server_dir }}"
|
||||
state: directory
|
||||
mode: '0700'
|
||||
|
||||
- name: Create devpi venv if missing
|
||||
ansible.builtin.command:
|
||||
cmd: "{{ devpi_uv_binary }} venv --python {{ devpi_python_version }} {{ devpi_venv }}"
|
||||
creates: "{{ devpi_venv }}/bin/python"
|
||||
|
||||
- name: Install devpi-server and devpi-web into venv
|
||||
# Always bootstrap from upstream PyPI — devpi is the index it would otherwise resolve through,
|
||||
# and that's a circular dependency (devpi cannot install itself from itself).
|
||||
ansible.builtin.command:
|
||||
cmd: >-
|
||||
{{ devpi_uv_binary }} pip install
|
||||
--python {{ devpi_venv }}/bin/python
|
||||
--index-url https://pypi.org/simple/
|
||||
devpi-server=={{ devpi_server_version }}
|
||||
devpi-web=={{ devpi_web_version }}
|
||||
register: devpi_pip_install
|
||||
changed_when: "'Installed' in devpi_pip_install.stdout or 'Uninstalled' in devpi_pip_install.stdout"
|
||||
notify: Restart devpi
|
||||
|
||||
- name: Check if devpi server-dir is initialized
|
||||
ansible.builtin.stat:
|
||||
path: "{{ devpi_server_dir }}/.serverversion"
|
||||
register: devpi_serverversion
|
||||
|
||||
- name: Initialize devpi server-dir
|
||||
ansible.builtin.command:
|
||||
cmd: >-
|
||||
{{ devpi_init_binary }}
|
||||
--serverdir {{ devpi_server_dir }}
|
||||
--root-passwd {{ devpi_root_password }}
|
||||
when: not devpi_serverversion.stat.exists
|
||||
changed_when: true
|
||||
no_log: true
|
||||
|
||||
- name: Deploy devpi LaunchAgent plist
|
||||
ansible.builtin.template:
|
||||
src: devpi.plist.j2
|
||||
dest: ~/Library/LaunchAgents/mcquack.eblume.devpi.plist
|
||||
mode: '0644'
|
||||
notify: Restart devpi
|
||||
|
||||
- name: Check if devpi LaunchAgent is loaded
|
||||
ansible.builtin.command: launchctl list mcquack.eblume.devpi
|
||||
register: devpi_launchctl_check
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Load devpi LaunchAgent if not loaded
|
||||
ansible.builtin.command: launchctl load ~/Library/LaunchAgents/mcquack.eblume.devpi.plist
|
||||
when: devpi_launchctl_check.rc != 0
|
||||
changed_when: true
|
||||
failed_when: false
|
||||
34
ansible/roles/devpi/templates/devpi.plist.j2
Normal file
34
ansible/roles/devpi/templates/devpi.plist.j2
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- {{ ansible_managed }} -->
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>mcquack.eblume.devpi</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>{{ devpi_binary }}</string>
|
||||
<string>--serverdir</string>
|
||||
<string>{{ devpi_server_dir }}</string>
|
||||
<string>--host</string>
|
||||
<string>{{ devpi_host }}</string>
|
||||
<string>--port</string>
|
||||
<string>{{ devpi_port }}</string>
|
||||
<string>--outside-url</string>
|
||||
<string>{{ devpi_outside_url }}</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>PATH</key>
|
||||
<string>{{ devpi_venv }}/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
||||
</dict>
|
||||
<key>StandardOutPath</key>
|
||||
<string>{{ devpi_log_dir }}/mcquack.devpi.out.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>{{ devpi_log_dir }}/mcquack.devpi.err.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
10
ansible/roles/docs/defaults/main.yml
Normal file
10
ansible/roles/docs/defaults/main.yml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
# Docs (Quartz-built static site) — replaces minikube Deployment.
|
||||
# Caddy serves docs_content_dir directly via the static-kind service block,
|
||||
# with Quartz-style try_files (path → path/ → path.html → 404).
|
||||
|
||||
docs_version: "v1.17.0"
|
||||
docs_release_url: "https://forge.eblu.me/eblume/blumeops/releases/download/{{ docs_version }}/docs-{{ docs_version }}.tar.gz"
|
||||
docs_home: /Users/erichblume/blumeops/docs
|
||||
docs_content_dir: "{{ docs_home }}/content"
|
||||
docs_version_sentinel: "{{ docs_home }}/.installed-version"
|
||||
57
ansible/roles/docs/tasks/main.yml
Normal file
57
ansible/roles/docs/tasks/main.yml
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
---
|
||||
# docs role — download and extract the Quartz-built docs tarball into
|
||||
# docs_content_dir. Caddy serves the directory directly with Quartz-style
|
||||
# try_files; there is no daemon to manage.
|
||||
#
|
||||
# Idempotency: a sentinel file records the installed docs_version. The
|
||||
# download/extract steps only run when the sentinel doesn't match docs_version.
|
||||
#
|
||||
# Mirrors the cv role's curl-based download for consistency, even though the
|
||||
# forge releases endpoint here does support HEAD.
|
||||
|
||||
- name: Ensure docs home exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ docs_home }}"
|
||||
state: directory
|
||||
mode: '0755'
|
||||
|
||||
- name: Read installed docs version sentinel
|
||||
ansible.builtin.slurp:
|
||||
src: "{{ docs_version_sentinel }}"
|
||||
register: docs_installed_raw
|
||||
failed_when: false
|
||||
changed_when: false
|
||||
|
||||
- name: Set installed docs version fact
|
||||
ansible.builtin.set_fact:
|
||||
docs_installed_version: >-
|
||||
{{ (docs_installed_raw.content | b64decode).strip()
|
||||
if (docs_installed_raw.content is defined) else '' }}
|
||||
|
||||
- name: Recreate docs content dir
|
||||
ansible.builtin.file:
|
||||
path: "{{ docs_content_dir }}"
|
||||
state: "{{ item }}"
|
||||
mode: '0755'
|
||||
loop:
|
||||
- absent
|
||||
- directory
|
||||
when: docs_installed_version != docs_version
|
||||
|
||||
- name: Download and extract docs release tarball
|
||||
ansible.builtin.shell:
|
||||
cmd: >-
|
||||
set -euo pipefail;
|
||||
curl -fsSL {{ docs_release_url | quote }} -o {{ docs_home }}/docs.tar.gz &&
|
||||
tar -xzf {{ docs_home }}/docs.tar.gz -C {{ docs_content_dir }} &&
|
||||
rm -f {{ docs_home }}/docs.tar.gz
|
||||
executable: /bin/bash
|
||||
when: docs_installed_version != docs_version
|
||||
changed_when: true
|
||||
|
||||
- name: Write docs version sentinel
|
||||
ansible.builtin.copy:
|
||||
content: "{{ docs_version }}\n"
|
||||
dest: "{{ docs_version_sentinel }}"
|
||||
mode: '0644'
|
||||
when: docs_installed_version != docs_version
|
||||
|
|
@ -61,6 +61,12 @@ MIN_INTERVAL = 10m
|
|||
[cron.update_checker]
|
||||
ENABLED = false
|
||||
|
||||
[cron.archive_cleanup]
|
||||
ENABLED = true
|
||||
RUN_AT_START = true
|
||||
SCHEDULE = @midnight
|
||||
OLDER_THAN = 2h
|
||||
|
||||
[session]
|
||||
PROVIDER = {{ forgejo_session_provider }}
|
||||
|
||||
|
|
@ -89,6 +95,11 @@ ACCOUNT_LINKING = login
|
|||
USERNAME = nickname
|
||||
REGISTER_EMAIL_CONFIRM = false
|
||||
|
||||
[metrics]
|
||||
ENABLED = true
|
||||
ENABLED_ISSUE_BY_LABEL = false
|
||||
ENABLED_ISSUE_BY_REPOSITORY = false
|
||||
|
||||
[actions]
|
||||
ENABLED = {{ forgejo_actions_enabled | lower }}
|
||||
DEFAULT_ACTIONS_URL = {{ forgejo_actions_default_url }}
|
||||
|
|
|
|||
49
ansible/roles/heph/defaults/main.yml
Normal file
49
ansible/roles/heph/defaults/main.yml
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
---
|
||||
# hephaestus hub — the canonical heph replica (server mode) on indri.
|
||||
# Other devices (e.g. gilbert) are spokes that sync against this hub.
|
||||
# See [[set-up-sync-hub]] and [[host-heph-pwa]] in the hephaestus repo.
|
||||
|
||||
# Pinned release used for the initial `cargo install` and the PWA shell.
|
||||
# After bootstrap, hephd's own --self-update keeps the binary current; this
|
||||
# pin only governs the first install and the bundled PWA shell version.
|
||||
heph_version: v1.2.1
|
||||
|
||||
# Anonymous public HTTPS clone — matches hephd's INSTALL_GIT_URL so the initial
|
||||
# install and unattended self-update build from the same source (no ssh-agent).
|
||||
heph_repo_url: https://forge.eblu.me/eblume/hephaestus.git
|
||||
|
||||
heph_bin_dir: /Users/erichblume/.cargo/bin
|
||||
heph_binary: "{{ heph_bin_dir }}/hephd"
|
||||
|
||||
# rustc/cargo here are rustup shims. The bare (non-mise) environment that the
|
||||
# launchagent and ansible run in falls back to rustup's *default* toolchain,
|
||||
# which can lag behind heph's rust-version floor (Cargo.toml: 1.89). Pin the
|
||||
# channel explicitly so both the bootstrap build and unattended self-update
|
||||
# always use a current toolchain regardless of the host's rustup default.
|
||||
heph_rust_toolchain: stable
|
||||
|
||||
heph_data_dir: /Users/erichblume/.local/share/heph
|
||||
heph_db: "{{ heph_data_dir }}/heph.db"
|
||||
heph_socket: "{{ heph_data_dir }}/hephd.sock"
|
||||
heph_log_dir: /Users/erichblume/Library/Logs
|
||||
|
||||
# Version-pinned source checkout; the PWA static shell is served directly from
|
||||
# its heph-pwa/ subdir (no copy), keeping shell and hub in lockstep at heph_version.
|
||||
heph_pwa_src_dir: /Users/erichblume/.cache/heph-pwa-src
|
||||
heph_web_root: "{{ heph_pwa_src_dir }}/heph-pwa"
|
||||
|
||||
# Hub listens on all interfaces so tailnet spokes can reach it directly
|
||||
# (http://indri.tail8d86e.ts.net:8787) and Caddy can proxy heph.ops.eblu.me.
|
||||
# Access is gated by Authentik OIDC regardless — tailnet reachability is not
|
||||
# enough (this is the owner's most sensitive data).
|
||||
heph_http_addr: 0.0.0.0:8787
|
||||
heph_port: 8787
|
||||
heph_external_url: https://heph.ops.eblu.me
|
||||
|
||||
# Authentik OIDC — issuer + audience together turn hub auth on. The audience is
|
||||
# the device-code client id (see argocd/manifests/authentik heph blueprint).
|
||||
heph_oidc_issuer: https://authentik.ops.eblu.me/application/o/heph/
|
||||
heph_oidc_audience: heph
|
||||
|
||||
# Self-update poll interval (seconds). 10 minutes.
|
||||
heph_self_update_interval_secs: 600
|
||||
6
ansible/roles/heph/handlers/main.yml
Normal file
6
ansible/roles/heph/handlers/main.yml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
- name: Restart heph
|
||||
ansible.builtin.shell: |
|
||||
launchctl unload ~/Library/LaunchAgents/mcquack.eblume.heph.plist 2>/dev/null || true
|
||||
launchctl load ~/Library/LaunchAgents/mcquack.eblume.heph.plist
|
||||
changed_when: true
|
||||
82
ansible/roles/heph/tasks/main.yml
Normal file
82
ansible/roles/heph/tasks/main.yml
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
---
|
||||
# hephaestus hub (server mode) on indri.
|
||||
#
|
||||
# DATA SEEDING (one-time, Path A — do this BEFORE the first provision so the hub
|
||||
# adopts gilbert's existing data instead of being born empty):
|
||||
#
|
||||
# 1. On the seed device (gilbert): heph daemon stop
|
||||
# 2. Copy its store to indri: scp ~/.local/share/heph/heph.db \
|
||||
# indri:~/.local/share/heph/heph.db
|
||||
# 3. On indri, give the hub its OWN device origin (keeps gilbert's owner_id +
|
||||
# data; hephd regenerates a fresh origin on next start when it is missing):
|
||||
# sqlite3 ~/.local/share/heph/heph.db "DELETE FROM meta WHERE key='origin';"
|
||||
# 4. Run this role (installs hephd, stages the PWA, loads the launchagent).
|
||||
#
|
||||
# hephd auto-creates an empty store on first start if none exists, so seeding is
|
||||
# optional — skip it only if you intend a fresh, empty hub.
|
||||
|
||||
- name: Ensure heph data directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ heph_data_dir }}"
|
||||
state: directory
|
||||
mode: '0700'
|
||||
|
||||
- name: Check for installed hephd binary
|
||||
ansible.builtin.stat:
|
||||
path: "{{ heph_binary }}"
|
||||
register: heph_binary_stat
|
||||
|
||||
# Bootstrap install only when hephd is absent. Thereafter hephd's own
|
||||
# --self-update keeps it current; ansible must not fight (or downgrade) it.
|
||||
# This builds from source and can take several minutes on a cold cargo cache.
|
||||
- name: Bootstrap-install heph + hephd from the forge ({{ heph_version }})
|
||||
ansible.builtin.command:
|
||||
cmd: >-
|
||||
{{ heph_bin_dir }}/cargo install --locked
|
||||
--git {{ heph_repo_url }}
|
||||
--tag {{ heph_version }}
|
||||
heph hephd
|
||||
environment:
|
||||
PATH: "{{ heph_bin_dir }}:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
|
||||
RUSTUP_TOOLCHAIN: "{{ heph_rust_toolchain }}"
|
||||
when: not heph_binary_stat.stat.exists
|
||||
changed_when: true
|
||||
notify: Restart heph
|
||||
|
||||
# Checkout provides the PWA shell at {{ heph_web_root }} (heph-pwa/ subdir),
|
||||
# served directly by hephd. Static files are read from disk per request, so a
|
||||
# version bump needs no restart; the service worker (CACHE = "heph-pwa-vN")
|
||||
# evicts stale assets on next load.
|
||||
- name: Ensure heph cache parent directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ heph_pwa_src_dir | dirname }}"
|
||||
state: directory
|
||||
mode: '0755'
|
||||
|
||||
- name: Stage heph-pwa source at {{ heph_version }}
|
||||
ansible.builtin.git:
|
||||
repo: "{{ heph_repo_url }}"
|
||||
dest: "{{ heph_pwa_src_dir }}"
|
||||
version: "{{ heph_version }}"
|
||||
depth: 1
|
||||
single_branch: true
|
||||
force: true
|
||||
|
||||
- name: Deploy heph LaunchAgent plist
|
||||
ansible.builtin.template:
|
||||
src: heph.plist.j2
|
||||
dest: ~/Library/LaunchAgents/mcquack.eblume.heph.plist
|
||||
mode: '0644'
|
||||
notify: Restart heph
|
||||
|
||||
- name: Check if heph LaunchAgent is loaded
|
||||
ansible.builtin.command: launchctl list mcquack.eblume.heph
|
||||
register: heph_launchctl_check
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Load heph LaunchAgent if not loaded
|
||||
ansible.builtin.command: launchctl load ~/Library/LaunchAgents/mcquack.eblume.heph.plist
|
||||
when: heph_launchctl_check.rc != 0
|
||||
changed_when: true
|
||||
failed_when: false
|
||||
50
ansible/roles/heph/templates/heph.plist.j2
Normal file
50
ansible/roles/heph/templates/heph.plist.j2
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- {{ ansible_managed }} -->
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>mcquack.eblume.heph</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>{{ heph_binary }}</string>
|
||||
<string>--mode</string>
|
||||
<string>server</string>
|
||||
<string>--http-addr</string>
|
||||
<string>{{ heph_http_addr }}</string>
|
||||
<string>--db</string>
|
||||
<string>{{ heph_db }}</string>
|
||||
<string>--socket</string>
|
||||
<string>{{ heph_socket }}</string>
|
||||
<string>--web-root</string>
|
||||
<string>{{ heph_web_root }}</string>
|
||||
<string>--oidc-issuer</string>
|
||||
<string>{{ heph_oidc_issuer }}</string>
|
||||
<string>--oidc-audience</string>
|
||||
<string>{{ heph_oidc_audience }}</string>
|
||||
<string>--self-update</string>
|
||||
<string>--self-update-interval-secs</string>
|
||||
<string>{{ heph_self_update_interval_secs }}</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<!-- cargo + toolchain on PATH so --self-update can run `cargo install`. -->
|
||||
<key>PATH</key>
|
||||
<string>{{ heph_bin_dir }}:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
||||
<key>HOME</key>
|
||||
<string>/Users/erichblume</string>
|
||||
<!-- Pin the rustup channel: the launchagent runs without mise, so a bare
|
||||
cargo shim would otherwise use rustup's (stale) default toolchain. -->
|
||||
<key>RUSTUP_TOOLCHAIN</key>
|
||||
<string>{{ heph_rust_toolchain }}</string>
|
||||
</dict>
|
||||
<key>StandardOutPath</key>
|
||||
<string>{{ heph_log_dir }}/mcquack.heph.out.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>{{ heph_log_dir }}/mcquack.heph.err.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -77,6 +77,37 @@
|
|||
msg: "WARNING: minikube may not have started properly. Run 'minikube start' manually on indri if needed. Status: {{ minikube_final_status.stdout | default('unknown') }}"
|
||||
when: minikube_final_status.rc != 0
|
||||
|
||||
# The storage-provisioner is a bare Pod (no controller). If the node restarts
|
||||
# via Docker Desktop rather than `minikube start`, kubelet brings back static
|
||||
# pods (apiserver, etcd) but bare pods like storage-provisioner are lost.
|
||||
# `minikube start` on a running cluster is safe and re-applies all addons.
|
||||
- name: Check storage-provisioner pod is running
|
||||
ansible.builtin.command:
|
||||
cmd: kubectl -n kube-system get pod storage-provisioner -o jsonpath='{.status.phase}'
|
||||
register: minikube_storage_provisioner
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
when: minikube_final_status.rc == 0
|
||||
|
||||
- name: Re-run minikube start to restore addons
|
||||
ansible.builtin.command:
|
||||
cmd: >
|
||||
minikube start
|
||||
--driver={{ minikube_driver }}
|
||||
--container-runtime={{ minikube_container_runtime }}
|
||||
--cpus={{ minikube_cpus }}
|
||||
--memory={{ minikube_memory }}
|
||||
--disk-size={{ minikube_disk_size }}
|
||||
{% for name in minikube_apiserver_names %}
|
||||
--apiserver-names={{ name }}
|
||||
{% endfor %}
|
||||
--apiserver-port={{ minikube_apiserver_port }}
|
||||
--listen-address={{ minikube_listen_address }}
|
||||
when:
|
||||
- minikube_final_status.rc == 0
|
||||
- minikube_storage_provisioner.stdout | default('') != 'Running'
|
||||
changed_when: true
|
||||
|
||||
# Configure containerd to use zot registry as pull-through cache
|
||||
# With docker driver, use host.minikube.internal to reach the host
|
||||
# Zot runs on indri:5050 and caches images from docker.io, ghcr.io, quay.io
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# 1Password Connect for ringtail k3s cluster
|
||||
# Same chart/values as indri, different destination
|
||||
# Same manifests as indri, different destination
|
||||
#
|
||||
# Prerequisites:
|
||||
# 1. Bootstrap secrets via ansible (provision-ringtail creates 1password namespace,
|
||||
|
|
@ -13,17 +13,10 @@ metadata:
|
|||
namespace: argocd
|
||||
spec:
|
||||
project: default
|
||||
sources:
|
||||
- repoURL: ssh://forgejo@forge.ops.eblu.me:2222/mirrors/connect-helm-charts.git
|
||||
targetRevision: connect-2.3.0
|
||||
path: charts/connect
|
||||
helm:
|
||||
releaseName: onepassword-connect
|
||||
valueFiles:
|
||||
- $values/argocd/manifests/1password-connect/values.yaml
|
||||
- repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
|
||||
targetRevision: main
|
||||
ref: values
|
||||
source:
|
||||
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
|
||||
targetRevision: main
|
||||
path: argocd/manifests/1password-connect
|
||||
destination:
|
||||
server: https://ringtail.tail8d86e.ts.net:6443
|
||||
namespace: 1password
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# 1Password Connect - Secrets Automation Server
|
||||
# Provides REST API access to 1Password vault items for External Secrets Operator
|
||||
#
|
||||
# Chart mirrored from https://github.com/1Password/connect-helm-charts
|
||||
# Manifests rendered from connect-helm-charts v2.4.1, maintained as plain kustomize.
|
||||
#
|
||||
# Prerequisites (one-time setup):
|
||||
# 1. Create Connect server: op connect server create blumeops --vaults blumeops
|
||||
|
|
@ -19,17 +19,10 @@ metadata:
|
|||
namespace: argocd
|
||||
spec:
|
||||
project: default
|
||||
sources:
|
||||
- repoURL: ssh://forgejo@forge.ops.eblu.me:2222/mirrors/connect-helm-charts.git
|
||||
targetRevision: connect-2.3.0
|
||||
path: charts/connect
|
||||
helm:
|
||||
releaseName: onepassword-connect
|
||||
valueFiles:
|
||||
- $values/argocd/manifests/1password-connect/values.yaml
|
||||
- repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
|
||||
targetRevision: main
|
||||
ref: values
|
||||
source:
|
||||
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
|
||||
targetRevision: main
|
||||
path: argocd/manifests/1password-connect
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: 1password
|
||||
|
|
|
|||
27
argocd/apps/cloudnative-pg-ringtail.yaml
Normal file
27
argocd/apps/cloudnative-pg-ringtail.yaml
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# CloudNativePG Operator for ringtail k3s cluster
|
||||
# Deploys the operator only; PostgreSQL clusters are created separately
|
||||
#
|
||||
# Sibling of cloudnative-pg.yaml (minikube). Same mirror, same release,
|
||||
# different destination. Both apps will coexist during the immich
|
||||
# migration; the minikube one is removed at the end of the broader
|
||||
# indri-k8s decommission.
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: cloudnative-pg-ringtail
|
||||
namespace: argocd
|
||||
spec:
|
||||
project: default
|
||||
source:
|
||||
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/mirrors/cloudnative-pg.git
|
||||
targetRevision: v1.27.1
|
||||
path: releases
|
||||
directory:
|
||||
include: 'cnpg-1.27.1.yaml'
|
||||
destination:
|
||||
server: https://ringtail.tail8d86e.ts.net:6443
|
||||
namespace: cnpg-system
|
||||
syncPolicy:
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
- ServerSideApply=true # Required for large CRDs that exceed annotation size limit
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
---
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: cv
|
||||
namespace: argocd
|
||||
spec:
|
||||
project: default
|
||||
source:
|
||||
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
|
||||
targetRevision: main
|
||||
path: argocd/manifests/cv
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: cv
|
||||
syncPolicy:
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
26
argocd/apps/databases-ringtail.yaml
Normal file
26
argocd/apps/databases-ringtail.yaml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# Databases on ringtail k3s.
|
||||
#
|
||||
# Today: only immich-pg (CNPG Cluster) + its borgmatic ExternalSecret.
|
||||
# More databases may move here as the indri-k8s decommission proceeds.
|
||||
#
|
||||
# Prerequisites:
|
||||
# - cloudnative-pg-ringtail (operator must exist before the Cluster CR)
|
||||
# - external-secrets-ringtail + 1password-connect-ringtail (for the
|
||||
# immich-pg-borgmatic ExternalSecret to sync)
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: databases-ringtail
|
||||
namespace: argocd
|
||||
spec:
|
||||
project: default
|
||||
source:
|
||||
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
|
||||
targetRevision: main
|
||||
path: argocd/manifests/databases-ringtail
|
||||
destination:
|
||||
server: https://ringtail.tail8d86e.ts.net:6443
|
||||
namespace: databases
|
||||
syncPolicy:
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
# devpi PyPI Caching Proxy
|
||||
# Provides PyPI cache and private package hosting
|
||||
#
|
||||
# After first deployment, initialize devpi:
|
||||
# kubectl -n devpi exec -it devpi-0 -- devpi-init --serverdir /devpi --root-passwd <password>
|
||||
# kubectl -n devpi rollout restart statefulset devpi
|
||||
#
|
||||
# Then create user/index:
|
||||
# uvx devpi use https://pypi.tail8d86e.ts.net
|
||||
# uvx devpi login root
|
||||
# uvx devpi user -c eblume email=blume.erich@gmail.com
|
||||
# uvx devpi index -c eblume/dev bases=root/pypi
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: devpi
|
||||
namespace: argocd
|
||||
spec:
|
||||
project: default
|
||||
source:
|
||||
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
|
||||
targetRevision: main
|
||||
path: argocd/manifests/devpi
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: devpi
|
||||
syncPolicy:
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
---
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: docs
|
||||
namespace: argocd
|
||||
spec:
|
||||
project: default
|
||||
source:
|
||||
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
|
||||
targetRevision: main
|
||||
path: argocd/manifests/docs
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: docs
|
||||
syncPolicy:
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
|
|
@ -15,7 +15,7 @@ spec:
|
|||
source:
|
||||
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
|
||||
targetRevision: main
|
||||
path: argocd/manifests/external-secrets
|
||||
path: argocd/manifests/external-secrets-ringtail
|
||||
destination:
|
||||
server: https://ringtail.tail8d86e.ts.net:6443
|
||||
namespace: external-secrets
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ spec:
|
|||
targetRevision: main
|
||||
path: argocd/manifests/homepage
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
server: https://ringtail.tail8d86e.ts.net:6443
|
||||
namespace: homepage
|
||||
syncPolicy:
|
||||
syncOptions:
|
||||
|
|
|
|||
31
argocd/apps/immich-ringtail.yaml
Normal file
31
argocd/apps/immich-ringtail.yaml
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# Immich on ringtail k3s.
|
||||
#
|
||||
# Staging deployment; the minikube `immich` app remains in parallel
|
||||
# until cutover. See [[immich-cutover-and-decommission]] for the
|
||||
# routing flip + minikube cleanup.
|
||||
#
|
||||
# Prerequisites:
|
||||
# - cnpg-on-ringtail + databases-ringtail (postgres)
|
||||
# - 1password-connect-ringtail + external-secrets-ringtail (not used
|
||||
# by this app today — immich-db Secret is created manually,
|
||||
# matching the minikube pattern)
|
||||
# - The immich-db Secret in the immich namespace, holding the
|
||||
# password for the `immich` postgres role (copied from the source
|
||||
# immich-pg-app Secret at migration time).
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: immich-ringtail
|
||||
namespace: argocd
|
||||
spec:
|
||||
project: default
|
||||
source:
|
||||
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
|
||||
targetRevision: main
|
||||
path: argocd/manifests/immich-ringtail
|
||||
destination:
|
||||
server: https://ringtail.tail8d86e.ts.net:6443
|
||||
namespace: immich
|
||||
syncPolicy:
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
# Immich Storage - PersistentVolume and PVC for photo library
|
||||
# Must be synced BEFORE the main immich app
|
||||
#
|
||||
# Prerequisites:
|
||||
# 1. NFS share on sifaka at /volume1/photos with permissions for indri
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: immich-storage
|
||||
namespace: argocd
|
||||
spec:
|
||||
project: default
|
||||
source:
|
||||
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
|
||||
targetRevision: main
|
||||
path: argocd/manifests/immich
|
||||
# Only deploy storage resources (PV/PVC/Ingress), not Helm values.yaml
|
||||
directory:
|
||||
include: "{pv-nfs.yaml,pvc.yaml,ingress-tailscale.yaml}"
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: immich
|
||||
syncPolicy:
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
# Immich - Self-hosted photo and video management
|
||||
# High-performance Google Photos/iCloud alternative with AI features
|
||||
#
|
||||
# Chart mirrored from https://github.com/immich-app/immich-charts to forge
|
||||
#
|
||||
# Prerequisites:
|
||||
# 1. Mirror immich-charts to forge: https://github.com/immich-app/immich-charts
|
||||
# 2. Create immich namespace and secrets:
|
||||
# kubectl create namespace immich
|
||||
# op inject -i argocd/manifests/immich/secret-db.yaml.tpl | kubectl apply -f -
|
||||
# 3. Create immich-pg database and user (see immich-pg app)
|
||||
# 4. Mount photos directory from indri to minikube
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: immich
|
||||
namespace: argocd
|
||||
spec:
|
||||
project: default
|
||||
sources:
|
||||
# Helm chart from forge mirror (SSH via egress)
|
||||
- repoURL: ssh://forgejo@forge.ops.eblu.me:2222/mirrors/immich-charts.git
|
||||
targetRevision: immich-0.10.3
|
||||
path: charts/immich
|
||||
helm:
|
||||
releaseName: immich
|
||||
valueFiles:
|
||||
- $values/argocd/manifests/immich/values.yaml
|
||||
- repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
|
||||
targetRevision: main
|
||||
ref: values
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: immich
|
||||
syncPolicy:
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
26
argocd/apps/mealie-ringtail.yaml
Normal file
26
argocd/apps/mealie-ringtail.yaml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# Mealie on ringtail k3s.
|
||||
#
|
||||
# Wave-1 indri-k8s decommission. Staging deployment; the minikube `mealie`
|
||||
# app stays in parallel until cutover (copy SQLite PVC, drop the minikube
|
||||
# tailscale ingress, flip Caddy). See [[migrate-wave1-ringtail]].
|
||||
#
|
||||
# Prerequisites:
|
||||
# - external-secrets-ringtail (onepassword-blumeops ClusterSecretStore)
|
||||
# - mealie-data PVC contents copied from minikube at cutover
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: mealie-ringtail
|
||||
namespace: argocd
|
||||
spec:
|
||||
project: default
|
||||
source:
|
||||
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
|
||||
targetRevision: main
|
||||
path: argocd/manifests/mealie-ringtail
|
||||
destination:
|
||||
server: https://ringtail.tail8d86e.ts.net:6443
|
||||
namespace: mealie
|
||||
syncPolicy:
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: mealie
|
||||
namespace: argocd
|
||||
spec:
|
||||
project: default
|
||||
source:
|
||||
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
|
||||
targetRevision: main
|
||||
path: argocd/manifests/mealie
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: mealie
|
||||
syncPolicy:
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
28
argocd/apps/paperless-ringtail.yaml
Normal file
28
argocd/apps/paperless-ringtail.yaml
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# Paperless-ngx on ringtail k3s.
|
||||
#
|
||||
# Wave-1 indri-k8s decommission. Staging deployment; the minikube
|
||||
# `paperless` app stays in parallel until cutover (drop the minikube
|
||||
# tailscale ingress to free the name, then flip Caddy). See
|
||||
# [[migrate-wave1-ringtail]].
|
||||
#
|
||||
# Prerequisites:
|
||||
# - databases-ringtail blumeops-pg (paperless database + role)
|
||||
# - external-secrets-ringtail (onepassword-blumeops ClusterSecretStore)
|
||||
# - sifaka NFS rule granting ringtail access to /volume1/paperless
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: paperless-ringtail
|
||||
namespace: argocd
|
||||
spec:
|
||||
project: default
|
||||
source:
|
||||
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
|
||||
targetRevision: main
|
||||
path: argocd/manifests/paperless-ringtail
|
||||
destination:
|
||||
server: https://ringtail.tail8d86e.ts.net:6443
|
||||
namespace: paperless
|
||||
syncPolicy:
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
20
argocd/apps/shower.yaml
Normal file
20
argocd/apps/shower.yaml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Adelaide / Heidi / Addie baby shower app — Django guest/raffle/prize system.
|
||||
# Public landing page at shower.eblu.me (via fly proxy), staff console + admin
|
||||
# at shower.ops.eblu.me (tailnet only). Built from forge PyPI wheel.
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: shower
|
||||
namespace: argocd
|
||||
spec:
|
||||
project: default
|
||||
source:
|
||||
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
|
||||
targetRevision: main
|
||||
path: argocd/manifests/shower
|
||||
destination:
|
||||
server: https://ringtail.tail8d86e.ts.net:6443
|
||||
namespace: shower
|
||||
syncPolicy:
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
28
argocd/apps/teslamate-ringtail.yaml
Normal file
28
argocd/apps/teslamate-ringtail.yaml
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# TeslaMate on ringtail k3s.
|
||||
#
|
||||
# Wave-1 indri-k8s decommission. Staging deployment; the minikube
|
||||
# `teslamate` app stays in parallel until cutover (migrate the teslamate
|
||||
# database, drop the minikube tailscale ingress, flip Caddy). See
|
||||
# [[migrate-wave1-ringtail]].
|
||||
#
|
||||
# Prerequisites:
|
||||
# - databases-ringtail blumeops-pg (teslamate database + role; cube +
|
||||
# earthdistance extensions created by superuser at cutover)
|
||||
# - external-secrets-ringtail (onepassword-blumeops ClusterSecretStore)
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: teslamate-ringtail
|
||||
namespace: argocd
|
||||
spec:
|
||||
project: default
|
||||
source:
|
||||
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
|
||||
targetRevision: main
|
||||
path: argocd/manifests/teslamate-ringtail
|
||||
destination:
|
||||
server: https://ringtail.tail8d86e.ts.net:6443
|
||||
namespace: teslamate
|
||||
syncPolicy:
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
# TeslaMate Tesla Data Logger
|
||||
# Requires: CloudNativePG PostgreSQL cluster and manual secret setup
|
||||
#
|
||||
# Before syncing, create the namespace and secrets:
|
||||
# kubectl create namespace teslamate
|
||||
# op inject -i argocd/manifests/databases/secret-teslamate.yaml.tpl | kubectl apply -f -
|
||||
# op inject -i argocd/manifests/teslamate/secret-encryption-key.yaml.tpl | kubectl apply -f -
|
||||
# op inject -i argocd/manifests/teslamate/secret-db.yaml.tpl | kubectl apply -f -
|
||||
#
|
||||
# Then create the database:
|
||||
# PGPASSWORD=$(op read "op://blumeops/postgres/password") \
|
||||
# psql -h pg.ops.eblu.me -U eblume -c "CREATE DATABASE teslamate OWNER teslamate;"
|
||||
#
|
||||
# After syncing, access the TeslaMate UI at https://tesla.tail8d86e.ts.net to complete
|
||||
# Tesla API authentication via OAuth flow.
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: teslamate
|
||||
namespace: argocd
|
||||
spec:
|
||||
project: default
|
||||
source:
|
||||
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
|
||||
targetRevision: main
|
||||
path: argocd/manifests/teslamate
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: teslamate
|
||||
syncPolicy:
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
|
|
@ -55,6 +55,15 @@ op inject -i argocd/manifests/1password-connect/secret-credentials.yaml.tpl | \
|
|||
kubectl --context=minikube-indri apply -f -
|
||||
```
|
||||
|
||||
## Version Management
|
||||
|
||||
Image versions are pinned in `kustomization.yaml` via `images[].newTag`. To upgrade:
|
||||
|
||||
1. Update `newTag` for both `1password/connect-api` and `1password/connect-sync`
|
||||
2. Sync via ArgoCD
|
||||
|
||||
The manifests were rendered from `connect-helm-charts v2.4.1` and are maintained as plain kustomize.
|
||||
|
||||
## Deployment
|
||||
|
||||
```bash
|
||||
|
|
|
|||
131
argocd/manifests/1password-connect/deployment.yaml
Normal file
131
argocd/manifests/1password-connect/deployment.yaml
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
# Rendered from connect-helm-charts v2.4.1 with blumeops values, then de-Helmed.
|
||||
# Image tags managed by kustomization.yaml images[] — do not edit here.
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: onepassword-connect
|
||||
namespace: 1password
|
||||
labels:
|
||||
app.kubernetes.io/component: connect
|
||||
app.kubernetes.io/name: connect
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: onepassword-connect
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: onepassword-connect
|
||||
app.kubernetes.io/component: connect
|
||||
spec:
|
||||
securityContext:
|
||||
fsGroup: 999
|
||||
runAsGroup: 999
|
||||
runAsNonRoot: true
|
||||
runAsUser: 999
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
volumes:
|
||||
- name: shared-data
|
||||
emptyDir: {}
|
||||
- name: credentials
|
||||
secret:
|
||||
secretName: op-credentials
|
||||
items:
|
||||
- key: 1password-credentials.json
|
||||
path: 1password-credentials.json
|
||||
containers:
|
||||
- name: connect-api
|
||||
image: 1password/connect-api:kustomized
|
||||
imagePullPolicy: IfNotPresent
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
readOnlyRootFilesystem: true
|
||||
resources:
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 256Mi
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
env:
|
||||
- name: OP_SESSION
|
||||
value: /home/opuser/.op/1password-credentials.json
|
||||
- name: OP_BUS_PORT
|
||||
value: "11220"
|
||||
- name: OP_BUS_PEERS
|
||||
value: localhost:11221
|
||||
- name: OP_HTTP_PORT
|
||||
value: "8080"
|
||||
- name: OP_LOG_LEVEL
|
||||
value: "info"
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
scheme: HTTP
|
||||
port: 8080
|
||||
initialDelaySeconds: 15
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /heartbeat
|
||||
scheme: HTTP
|
||||
port: 8080
|
||||
failureThreshold: 3
|
||||
periodSeconds: 30
|
||||
initialDelaySeconds: 15
|
||||
volumeMounts:
|
||||
- mountPath: /home/opuser/.op/data
|
||||
name: shared-data
|
||||
- name: credentials
|
||||
mountPath: /home/opuser/.op/1password-credentials.json
|
||||
subPath: 1password-credentials.json
|
||||
- name: connect-sync
|
||||
image: 1password/connect-sync:kustomized
|
||||
imagePullPolicy: IfNotPresent
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
readOnlyRootFilesystem: true
|
||||
resources:
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 256Mi
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
env:
|
||||
- name: OP_HTTP_PORT
|
||||
value: "8081"
|
||||
- name: OP_SESSION
|
||||
value: /home/opuser/.op/1password-credentials.json
|
||||
- name: OP_BUS_PORT
|
||||
value: "11221"
|
||||
- name: OP_BUS_PEERS
|
||||
value: localhost:11220
|
||||
- name: OP_LOG_LEVEL
|
||||
value: "info"
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8081
|
||||
initialDelaySeconds: 15
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /heartbeat
|
||||
port: 8081
|
||||
scheme: HTTP
|
||||
failureThreshold: 3
|
||||
periodSeconds: 30
|
||||
initialDelaySeconds: 15
|
||||
volumeMounts:
|
||||
- mountPath: /home/opuser/.op/data
|
||||
name: shared-data
|
||||
- name: credentials
|
||||
mountPath: /home/opuser/.op/1password-credentials.json
|
||||
subPath: 1password-credentials.json
|
||||
15
argocd/manifests/1password-connect/kustomization.yaml
Normal file
15
argocd/manifests/1password-connect/kustomization.yaml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
namespace: 1password
|
||||
|
||||
resources:
|
||||
- deployment.yaml
|
||||
- service.yaml
|
||||
|
||||
images:
|
||||
- name: 1password/connect-api
|
||||
newTag: "1.8.2"
|
||||
- name: 1password/connect-sync
|
||||
newTag: "1.8.2"
|
||||
18
argocd/manifests/1password-connect/service.yaml
Normal file
18
argocd/manifests/1password-connect/service.yaml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Rendered from connect-helm-charts v2.4.1, then de-Helmed.
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: onepassword-connect
|
||||
namespace: 1password
|
||||
labels:
|
||||
app.kubernetes.io/component: connect
|
||||
app.kubernetes.io/name: connect
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: onepassword-connect
|
||||
ports:
|
||||
- port: 8081
|
||||
name: connect-sync
|
||||
- port: 8080
|
||||
name: connect-api
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
# 1Password Connect Helm values for blumeops
|
||||
# Chart: https://github.com/1Password/connect-helm-charts
|
||||
#
|
||||
# The credentials are bootstrapped manually via secret-credentials.yaml.tpl
|
||||
# before deploying this chart.
|
||||
|
||||
connect:
|
||||
# Use pre-created credentials secret (from bootstrap)
|
||||
credentialsKey: 1password-credentials.json
|
||||
credentialsName: op-credentials
|
||||
|
||||
# Resource limits for minikube
|
||||
api:
|
||||
resources:
|
||||
requests:
|
||||
memory: "64Mi"
|
||||
cpu: "50m"
|
||||
limits:
|
||||
memory: "256Mi"
|
||||
cpu: "200m"
|
||||
|
||||
sync:
|
||||
resources:
|
||||
requests:
|
||||
memory: "64Mi"
|
||||
cpu: "50m"
|
||||
limits:
|
||||
memory: "256Mi"
|
||||
cpu: "200m"
|
||||
|
||||
# We don't use the 1Password Operator (using External Secrets instead)
|
||||
operator:
|
||||
create: false
|
||||
|
|
@ -159,8 +159,10 @@ prometheus.exporter.blackbox "services" {
|
|||
}
|
||||
|
||||
target {
|
||||
// devpi runs natively on indri (LaunchAgent), not in-cluster.
|
||||
// We probe through Caddy (https://pypi.ops.eblu.me) which the cluster can reach via Tailscale.
|
||||
name = "devpi"
|
||||
address = "http://devpi.devpi.svc.cluster.local:3141/+api"
|
||||
address = "https://pypi.ops.eblu.me/+api"
|
||||
module = "http_2xx"
|
||||
}
|
||||
|
||||
|
|
@ -189,14 +191,9 @@ prometheus.exporter.blackbox "services" {
|
|||
}
|
||||
|
||||
target {
|
||||
// Migrated to ringtail (wave-1); probe through Caddy over Tailscale.
|
||||
name = "teslamate"
|
||||
address = "http://teslamate.teslamate.svc.cluster.local:4000/"
|
||||
module = "http_2xx"
|
||||
}
|
||||
|
||||
target {
|
||||
name = "immich"
|
||||
address = "http://immich-server.immich.svc.cluster.local:2283/api/server/ping"
|
||||
address = "https://tesla.ops.eblu.me/"
|
||||
module = "http_2xx"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ spec:
|
|||
serviceAccountName: alloy
|
||||
securityContext:
|
||||
fsGroup: 473 # alloy user group
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: alloy
|
||||
image: registry.ops.eblu.me/blumeops/alloy:kustomized
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ resources:
|
|||
|
||||
images:
|
||||
- name: registry.ops.eblu.me/blumeops/alloy
|
||||
newTag: v1.14.0-fd0bebb
|
||||
newTag: v1.16.0-9564435
|
||||
|
||||
configMapGenerator:
|
||||
- name: alloy-config
|
||||
|
|
|
|||
|
|
@ -45,6 +45,26 @@ prometheus.scrape "kube_state_metrics" {
|
|||
forward_to = [prometheus.remote_write.prometheus.receiver]
|
||||
}
|
||||
|
||||
// ============== SERVICE HEALTH PROBES ==============
|
||||
|
||||
// Blackbox-style HTTP probes for in-cluster services on ringtail
|
||||
prometheus.exporter.blackbox "services" {
|
||||
config = "{ modules: { http_2xx: { prober: http, timeout: 5s } } }"
|
||||
|
||||
target {
|
||||
name = "immich"
|
||||
address = "http://immich-server.immich.svc.cluster.local:2283/api/server/ping"
|
||||
module = "http_2xx"
|
||||
}
|
||||
}
|
||||
|
||||
// Scrape blackbox probe results
|
||||
prometheus.scrape "blackbox" {
|
||||
targets = prometheus.exporter.blackbox.services.targets
|
||||
scrape_interval = "30s"
|
||||
forward_to = [prometheus.remote_write.prometheus.receiver]
|
||||
}
|
||||
|
||||
// Push metrics to indri Prometheus
|
||||
prometheus.remote_write "prometheus" {
|
||||
external_labels = { cluster = "ringtail" }
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ resources:
|
|||
|
||||
images:
|
||||
- name: registry.ops.eblu.me/blumeops/alloy
|
||||
newTag: v1.14.0-fd0bebb-nix
|
||||
newTag: v1.16.0-9564435-nix
|
||||
|
||||
configMapGenerator:
|
||||
- name: alloy-config
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ spec:
|
|||
mountPath: /var/lib/alloy/data
|
||||
securityContext:
|
||||
privileged: true
|
||||
runAsUser: 0
|
||||
tolerations:
|
||||
- operator: Exists
|
||||
volumes:
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ resources:
|
|||
|
||||
images:
|
||||
- name: registry.ops.eblu.me/blumeops/alloy
|
||||
newTag: v1.14.0-fd0bebb-nix
|
||||
newTag: v1.16.0-9564435-nix
|
||||
|
||||
configMapGenerator:
|
||||
- name: alloy-tracing-config
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ kubectl wait --for=condition=available deployment/argocd-server -n argocd --time
|
|||
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d && echo
|
||||
|
||||
# 5. Login and change password
|
||||
argocd login argocd.tail8d86e.ts.net --username admin --grpc-web
|
||||
argocd login argocd.tail8d86e.ts.net --username admin
|
||||
argocd account update-password
|
||||
|
||||
# 6. Apply repo-creds-forge credential template for SSH access to all forge repos
|
||||
|
|
@ -114,4 +114,4 @@ spec:
|
|||
Future improvement: integrate with a secrets operator (e.g., External Secrets).
|
||||
- The credential template (`repo-creds`) uses a URL prefix to match all repos on forge.
|
||||
- ArgoCD uses Tailscale Ingress with Let's Encrypt for TLS termination.
|
||||
- The `--grpc-web` flag is required for CLI access through the Tailscale ingress.
|
||||
- After Authentik is up, prefer `argocd login argocd.ops.eblu.me --sso` over the admin password login above; admin is only needed during bootstrap or as break-glass.
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ data:
|
|||
name: Authentik
|
||||
issuer: https://authentik.ops.eblu.me/application/o/argocd/
|
||||
clientID: argocd
|
||||
clientSecret: $argocd-oidc-authentik:client-secret
|
||||
requestedScopes:
|
||||
- openid
|
||||
- profile
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@
|
|||
#
|
||||
# - workflow-bot: minimal CI/CD permissions (sync, get)
|
||||
# - admins: Authentik admins group mapped to ArgoCD admin role
|
||||
# - admin: local break-glass account — keeps ArgoCD admin rights for when
|
||||
# Authentik SSO is unavailable (without this it has no permissions, since
|
||||
# policy.default is unset)
|
||||
#
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
|
|
@ -14,3 +17,4 @@ data:
|
|||
p, role:workflow-bot, applications, get, *, allow
|
||||
g, workflow-bot, role:workflow-bot
|
||||
g, admins, role:admin
|
||||
g, admin, role:admin
|
||||
|
|
|
|||
118
argocd/manifests/argocd/argocd-resources-patch.yaml
Normal file
118
argocd/manifests/argocd/argocd-resources-patch.yaml
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: argocd-server
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: argocd-server
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: argocd-repo-server
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: argocd-repo-server
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: argocd-application-controller
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: argocd-application-controller
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: "1"
|
||||
memory: 1Gi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: argocd-applicationset-controller
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: argocd-applicationset-controller
|
||||
resources:
|
||||
requests:
|
||||
cpu: 25m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 250m
|
||||
memory: 256Mi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: argocd-dex-server
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: dex
|
||||
resources:
|
||||
requests:
|
||||
cpu: 25m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 250m
|
||||
memory: 256Mi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: argocd-redis
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: redis
|
||||
resources:
|
||||
requests:
|
||||
cpu: 25m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 250m
|
||||
memory: 256Mi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: argocd-notifications-controller
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: argocd-notifications-controller
|
||||
resources:
|
||||
requests:
|
||||
cpu: 25m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 250m
|
||||
memory: 256Mi
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
# ExternalSecret for ArgoCD OIDC client secret (Authentik)
|
||||
#
|
||||
# Referenced from argocd-cm as $argocd-oidc-authentik:client-secret
|
||||
# Must have app.kubernetes.io/part-of: argocd label for ArgoCD to read it
|
||||
#
|
||||
---
|
||||
apiVersion: external-secrets.io/v1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
name: argocd-oidc-authentik
|
||||
namespace: argocd
|
||||
spec:
|
||||
refreshInterval: 1h
|
||||
secretStoreRef:
|
||||
kind: ClusterSecretStore
|
||||
name: onepassword-blumeops
|
||||
target:
|
||||
name: argocd-oidc-authentik
|
||||
creationPolicy: Owner
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/part-of: argocd
|
||||
data:
|
||||
- secretKey: client-secret
|
||||
remoteRef:
|
||||
conversionStrategy: Default
|
||||
decodingStrategy: None
|
||||
key: "Authentik (blumeops)"
|
||||
metadataPolicy: None
|
||||
property: argocd-client-secret
|
||||
|
|
@ -5,13 +5,14 @@ namespace: argocd
|
|||
|
||||
resources:
|
||||
# Pin to specific version for intentional upgrades
|
||||
- https://raw.githubusercontent.com/argoproj/argo-cd/v3.3.2/manifests/install.yaml
|
||||
# ArgoCD v3.3.6
|
||||
- https://raw.githubusercontent.com/argoproj/argo-cd/998fb59dc355653c0657908a6ea2f87136e022d1/manifests/install.yaml
|
||||
- ingress-tailscale.yaml
|
||||
- external-secret-repo-forge.yaml
|
||||
- external-secret-oidc-authentik.yaml
|
||||
|
||||
patches:
|
||||
- path: argocd-cmd-params-cm.yaml
|
||||
- path: argocd-ssh-known-hosts-cm.yaml
|
||||
- path: argocd-cm-patch.yaml
|
||||
- path: argocd-rbac-cm-patch.yaml
|
||||
- path: argocd-resources-patch.yaml
|
||||
|
|
|
|||
|
|
@ -262,14 +262,15 @@ data:
|
|||
name: ArgoCD
|
||||
authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]]
|
||||
invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]]
|
||||
client_type: confidential
|
||||
client_type: public
|
||||
client_id: argocd
|
||||
client_secret: !Env AUTHENTIK_ARGOCD_CLIENT_SECRET
|
||||
redirect_uris:
|
||||
- matching_mode: strict
|
||||
url: https://argocd.ops.eblu.me/auth/callback
|
||||
- matching_mode: strict
|
||||
url: https://argocd.tail8d86e.ts.net/auth/callback
|
||||
- matching_mode: strict
|
||||
url: http://localhost:8085/auth/callback
|
||||
signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]]
|
||||
property_mappings:
|
||||
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]]
|
||||
|
|
@ -346,6 +347,50 @@ data:
|
|||
meta_launch_url: https://jellyfin.ops.eblu.me
|
||||
policy_engine_mode: all
|
||||
|
||||
paperless.yaml: |
|
||||
version: 1
|
||||
metadata:
|
||||
name: BlumeOps Paperless SSO
|
||||
labels:
|
||||
blueprints.goauthentik.io/description: "Paperless-ngx OIDC provider and application"
|
||||
entries:
|
||||
# OAuth2 provider for Paperless-ngx (confidential client)
|
||||
- model: authentik_providers_oauth2.oauth2provider
|
||||
id: paperless-provider
|
||||
identifiers:
|
||||
name: Paperless
|
||||
attrs:
|
||||
name: Paperless
|
||||
authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]]
|
||||
invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]]
|
||||
client_type: confidential
|
||||
client_id: paperless
|
||||
client_secret: !Env AUTHENTIK_PAPERLESS_CLIENT_SECRET
|
||||
redirect_uris:
|
||||
- matching_mode: strict
|
||||
url: https://paperless.ops.eblu.me/accounts/oidc/authentik/login/callback/
|
||||
- matching_mode: strict
|
||||
url: https://paperless.tail8d86e.ts.net/accounts/oidc/authentik/login/callback/
|
||||
signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]]
|
||||
property_mappings:
|
||||
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]]
|
||||
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, email]]
|
||||
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, profile]]
|
||||
sub_mode: hashed_user_id
|
||||
include_claims_in_id_token: true
|
||||
|
||||
# Paperless application — all authenticated users allowed
|
||||
- model: authentik_core.application
|
||||
id: paperless-app
|
||||
identifiers:
|
||||
slug: paperless
|
||||
attrs:
|
||||
name: Paperless
|
||||
slug: paperless
|
||||
provider: !KeyOf paperless-provider
|
||||
meta_launch_url: https://paperless.ops.eblu.me
|
||||
policy_engine_mode: all
|
||||
|
||||
mealie.yaml: |
|
||||
version: 1
|
||||
metadata:
|
||||
|
|
@ -389,3 +434,93 @@ data:
|
|||
provider: !KeyOf mealie-provider
|
||||
meta_launch_url: https://meals.ops.eblu.me
|
||||
policy_engine_mode: all
|
||||
|
||||
heph.yaml: |
|
||||
version: 1
|
||||
metadata:
|
||||
name: BlumeOps Heph SSO
|
||||
labels:
|
||||
blueprints.goauthentik.io/description: "Hephaestus hub OIDC (device-code) provider, application, and device-code flow"
|
||||
entries:
|
||||
# Device-code flow (RFC 8628). authentik ships no default for this, so we
|
||||
# create one and bind it to the brand below. An empty stage_configuration
|
||||
# flow is sufficient: the already-authenticated user just confirms the code.
|
||||
- model: authentik_flows.flow
|
||||
id: device-code-flow
|
||||
identifiers:
|
||||
slug: default-device-code-flow
|
||||
attrs:
|
||||
name: Device code flow
|
||||
title: Device code flow
|
||||
slug: default-device-code-flow
|
||||
designation: stage_configuration
|
||||
authentication: require_authenticated
|
||||
|
||||
# Enable the device-code grant globally by binding the flow to the default
|
||||
# brand (domain authentik-default). Partial update — only sets this field.
|
||||
- model: authentik_brands.brand
|
||||
identifiers:
|
||||
domain: authentik-default
|
||||
attrs:
|
||||
flow_device_code: !KeyOf device-code-flow
|
||||
|
||||
# OAuth2 provider for heph — PUBLIC client (device-code + PKCE, no secret).
|
||||
# client_id doubles as the token audience the hub verifies (--oidc-audience heph),
|
||||
# and the app slug 'heph' is the issuer path (/application/o/heph/).
|
||||
- model: authentik_providers_oauth2.oauth2provider
|
||||
id: heph-provider
|
||||
identifiers:
|
||||
name: Heph
|
||||
attrs:
|
||||
name: Heph
|
||||
authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]]
|
||||
invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]]
|
||||
client_type: public
|
||||
client_id: heph
|
||||
# CLI/TUI use the device-code grant (no redirect). The heph-pwa browser
|
||||
# login uses Authorization Code + PKCE, which DOES redirect back to the
|
||||
# app's origin — register those here (Authentik also keys token-endpoint
|
||||
# CORS off these origins). Trailing slash matters: the PWA's redirect_uri
|
||||
# is its base dir, e.g. https://heph.ops.eblu.me/.
|
||||
redirect_uris:
|
||||
- matching_mode: strict
|
||||
url: https://heph.ops.eblu.me/
|
||||
- matching_mode: strict
|
||||
url: http://localhost:8787/ # local dev (hephd --web-root)
|
||||
signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]]
|
||||
property_mappings:
|
||||
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]]
|
||||
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, email]]
|
||||
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, profile]]
|
||||
# offline_access: heph CLI requests "openid offline_access"; without
|
||||
# this mapping the refresh token is session-bound and hephd's
|
||||
# refresh_token grant 400s once the session lapses (spoke sync dies).
|
||||
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, offline_access]]
|
||||
sub_mode: hashed_user_id
|
||||
include_claims_in_id_token: true
|
||||
|
||||
# Heph application — linked to the OAuth2 provider
|
||||
- model: authentik_core.application
|
||||
id: heph-app
|
||||
identifiers:
|
||||
slug: heph
|
||||
attrs:
|
||||
name: Hephaestus
|
||||
slug: heph
|
||||
provider: !KeyOf heph-provider
|
||||
meta_launch_url: https://heph.ops.eblu.me
|
||||
policy_engine_mode: any
|
||||
|
||||
# Policy binding — restrict heph to admins group (single-owner, sensitive data)
|
||||
- model: authentik_policies.policybinding
|
||||
identifiers:
|
||||
order: 0
|
||||
target: !KeyOf heph-app
|
||||
group: !Find [authentik_core.group, [name, admins]]
|
||||
attrs:
|
||||
target: !KeyOf heph-app
|
||||
group: !Find [authentik_core.group, [name, admins]]
|
||||
order: 0
|
||||
enabled: true
|
||||
negate: false
|
||||
timeout: 30
|
||||
|
|
|
|||
|
|
@ -75,26 +75,26 @@ spec:
|
|||
secretKeyRef:
|
||||
name: authentik-config
|
||||
key: jellyfin-client-secret
|
||||
- name: AUTHENTIK_ARGOCD_CLIENT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: authentik-config
|
||||
key: argocd-client-secret
|
||||
- name: AUTHENTIK_MEALIE_CLIENT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: authentik-config
|
||||
key: mealie-client-secret
|
||||
- name: AUTHENTIK_PAPERLESS_CLIENT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: authentik-config
|
||||
key: paperless-client-secret
|
||||
volumeMounts:
|
||||
- name: blueprints
|
||||
mountPath: /blueprints/custom
|
||||
readOnly: true
|
||||
resources:
|
||||
requests:
|
||||
memory: "256Mi"
|
||||
memory: "512Mi"
|
||||
cpu: "100m"
|
||||
limits:
|
||||
memory: "1Gi"
|
||||
memory: "2Gi"
|
||||
cpu: "1000m"
|
||||
volumes:
|
||||
- name: blueprints
|
||||
|
|
|
|||
|
|
@ -53,11 +53,11 @@ spec:
|
|||
remoteRef:
|
||||
key: "Authentik (blumeops)"
|
||||
property: jellyfin-client-secret
|
||||
- secretKey: argocd-client-secret
|
||||
remoteRef:
|
||||
key: "Authentik (blumeops)"
|
||||
property: argocd-client-secret
|
||||
- secretKey: mealie-client-secret
|
||||
remoteRef:
|
||||
key: "Authentik (blumeops)"
|
||||
property: mealie-client-secret
|
||||
- secretKey: paperless-client-secret
|
||||
remoteRef:
|
||||
key: "Authentik (blumeops)"
|
||||
property: paperless-client-secret
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ resources:
|
|||
- ingress-tailscale.yaml
|
||||
images:
|
||||
- name: registry.ops.eblu.me/blumeops/authentik
|
||||
newTag: v2026.2.0-fd0bebb-nix
|
||||
newTag: v2026.2.2-2eb2830-nix
|
||||
- name: docker.io/library/redis
|
||||
newName: registry.ops.eblu.me/blumeops/authentik-redis
|
||||
newTag: v8.2.3-fd0bebb-nix
|
||||
|
|
|
|||
|
|
@ -1,51 +0,0 @@
|
|||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: cv
|
||||
namespace: cv
|
||||
spec:
|
||||
replicas: 2
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxUnavailable: 0
|
||||
maxSurge: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: cv
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: cv
|
||||
spec:
|
||||
securityContext:
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: cv
|
||||
image: registry.ops.eblu.me/blumeops/cv:kustomized
|
||||
ports:
|
||||
- containerPort: 80
|
||||
name: http
|
||||
env:
|
||||
- name: CV_RELEASE_URL
|
||||
value: "https://forge.eblu.me/api/packages/eblume/generic/cv/v1.0.3/cv-v1.0.3.tar.gz"
|
||||
resources:
|
||||
requests:
|
||||
memory: "64Mi"
|
||||
cpu: "10m"
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 80
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 30
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 80
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: cv-tailscale
|
||||
namespace: cv
|
||||
annotations:
|
||||
tailscale.com/proxy-class: "default"
|
||||
tailscale.com/proxy-group: "ingress"
|
||||
tailscale.com/tags: "tag:k8s,tag:flyio-target"
|
||||
gethomepage.dev/enabled: "true"
|
||||
gethomepage.dev/name: "CV"
|
||||
gethomepage.dev/group: "Services"
|
||||
gethomepage.dev/icon: "mdi-file-document"
|
||||
gethomepage.dev/description: "Resume / CV"
|
||||
gethomepage.dev/href: "https://cv.eblu.me"
|
||||
gethomepage.dev/pod-selector: "app=cv"
|
||||
spec:
|
||||
ingressClassName: tailscale
|
||||
defaultBackend:
|
||||
service:
|
||||
name: cv
|
||||
port:
|
||||
number: 80
|
||||
tls:
|
||||
- hosts:
|
||||
- cv
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
---
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
namespace: cv
|
||||
resources:
|
||||
- deployment.yaml
|
||||
- service.yaml
|
||||
- ingress-tailscale.yaml
|
||||
- pdb.yaml
|
||||
images:
|
||||
- name: registry.ops.eblu.me/blumeops/cv
|
||||
newTag: v1.0.3-613f05d
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
---
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: cv
|
||||
spec:
|
||||
minAvailable: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: cv
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: cv
|
||||
namespace: cv
|
||||
spec:
|
||||
selector:
|
||||
app: cv
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: 80
|
||||
97
argocd/manifests/databases-ringtail/blumeops-pg.yaml
Normal file
97
argocd/manifests/databases-ringtail/blumeops-pg.yaml
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
# PostgreSQL Cluster for blumeops services on ringtail k3s.
|
||||
#
|
||||
# Wave-1 indri-k8s decommission target (see [[migrate-wave1-ringtail]]).
|
||||
# Holds the paperless and teslamate databases migrated off the minikube
|
||||
# blumeops-pg via cold pg_dump/pg_restore at cutover. miniflux + authentik
|
||||
# stay where they are for now (later waves), so this cluster only carries
|
||||
# the wave-1 roles.
|
||||
#
|
||||
# Apps reach this in-cluster at blumeops-pg-rw.databases.svc.cluster.local
|
||||
# — the same name they used on minikube, so teslamate's DATABASE_HOST is
|
||||
# unchanged.
|
||||
#
|
||||
# Database creation is deferred to cutover, mirroring the minikube cluster
|
||||
# (where only the bootstrap database is declared and the rest were created
|
||||
# out-of-band):
|
||||
# - paperless: the bootstrap database below (restored into at cutover).
|
||||
# - teslamate: created at its cutover by the eblume superuser, because the
|
||||
# dump's `earthdistance` extension is untrusted and CREATE EXTENSION
|
||||
# needs superuser. (cube + earthdistance ownership then transferred to
|
||||
# the teslamate role so it can ALTER EXTENSION UPDATE.)
|
||||
apiVersion: postgresql.cnpg.io/v1
|
||||
kind: Cluster
|
||||
metadata:
|
||||
name: blumeops-pg
|
||||
namespace: databases
|
||||
spec:
|
||||
instances: 1
|
||||
imageName: ghcr.io/cloudnative-pg/postgresql:18.3
|
||||
|
||||
storage:
|
||||
size: 10Gi
|
||||
storageClass: local-path
|
||||
|
||||
bootstrap:
|
||||
initdb:
|
||||
database: paperless
|
||||
owner: paperless
|
||||
|
||||
managed:
|
||||
roles:
|
||||
# eblume superuser for admin + privileged restore steps (extensions)
|
||||
- name: eblume
|
||||
login: true
|
||||
superuser: true
|
||||
createdb: true
|
||||
createrole: true
|
||||
connectionLimit: -1
|
||||
ensure: present
|
||||
inherit: true
|
||||
passwordSecret:
|
||||
name: blumeops-pg-eblume
|
||||
# borgmatic read-only user for backups
|
||||
- name: borgmatic
|
||||
login: true
|
||||
connectionLimit: -1
|
||||
ensure: present
|
||||
inherit: true
|
||||
inRoles:
|
||||
- pg_read_all_data
|
||||
passwordSecret:
|
||||
name: blumeops-pg-borgmatic
|
||||
# paperless user (also the bootstrap database owner above; the
|
||||
# managed role sets its password from the 1Password-backed secret)
|
||||
- name: paperless
|
||||
login: true
|
||||
connectionLimit: -1
|
||||
ensure: present
|
||||
inherit: true
|
||||
passwordSecret:
|
||||
name: blumeops-pg-paperless
|
||||
# teslamate user. Extension ownership (cube, earthdistance) is
|
||||
# transferred to this role at cutover so it can ALTER EXTENSION UPDATE.
|
||||
- name: teslamate
|
||||
login: true
|
||||
connectionLimit: -1
|
||||
ensure: present
|
||||
inherit: true
|
||||
passwordSecret:
|
||||
name: blumeops-pg-teslamate
|
||||
|
||||
resources:
|
||||
requests:
|
||||
memory: "256Mi"
|
||||
cpu: "100m"
|
||||
limits:
|
||||
memory: "1Gi"
|
||||
cpu: "500m"
|
||||
|
||||
postgresql:
|
||||
parameters:
|
||||
max_connections: "50"
|
||||
shared_buffers: "128MB"
|
||||
password_encryption: "scram-sha-256"
|
||||
pg_hba:
|
||||
# Password auth from anywhere; network security is via Tailscale.
|
||||
- host all all 0.0.0.0/0 scram-sha-256
|
||||
- host all all ::/0 scram-sha-256
|
||||
|
|
@ -1,13 +1,14 @@
|
|||
# ExternalSecret for borgmatic backup user password on immich-pg cluster
|
||||
# ExternalSecret for borgmatic backup user password
|
||||
#
|
||||
# Replaces the manual op inject workflow from secret-borgmatic.yaml.tpl
|
||||
#
|
||||
# Reuses the same 1Password item as blumeops-pg-borgmatic.
|
||||
# 1Password item: "borgmatic" in blumeops vault
|
||||
# Field: "db-password"
|
||||
#
|
||||
apiVersion: external-secrets.io/v1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
name: immich-pg-borgmatic
|
||||
name: blumeops-pg-borgmatic
|
||||
namespace: databases
|
||||
spec:
|
||||
refreshInterval: 1h
|
||||
|
|
@ -15,7 +16,7 @@ spec:
|
|||
kind: ClusterSecretStore
|
||||
name: onepassword-blumeops
|
||||
target:
|
||||
name: immich-pg-borgmatic
|
||||
name: blumeops-pg-borgmatic
|
||||
creationPolicy: Owner
|
||||
template:
|
||||
type: kubernetes.io/basic-auth
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
# ExternalSecret for eblume superuser password
|
||||
#
|
||||
# Replaces the manual op inject workflow from secret-eblume.yaml.tpl
|
||||
#
|
||||
# 1Password item: "postgres" in blumeops vault
|
||||
# Field: "password"
|
||||
#
|
||||
apiVersion: external-secrets.io/v1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
name: blumeops-pg-eblume
|
||||
namespace: databases
|
||||
spec:
|
||||
refreshInterval: 1h
|
||||
secretStoreRef:
|
||||
kind: ClusterSecretStore
|
||||
name: onepassword-blumeops
|
||||
target:
|
||||
name: blumeops-pg-eblume
|
||||
creationPolicy: Owner
|
||||
template:
|
||||
type: kubernetes.io/basic-auth
|
||||
data:
|
||||
username: eblume
|
||||
password: "{{ .password }}"
|
||||
data:
|
||||
- secretKey: password
|
||||
remoteRef:
|
||||
key: postgres
|
||||
property: password
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
# ExternalSecret for borgmatic backup user password on immich-pg cluster
|
||||
# (ringtail k3s).
|
||||
#
|
||||
# Mirror of argocd/manifests/databases/external-secret-immich-borgmatic.yaml.
|
||||
# The onepassword-blumeops ClusterSecretStore exists on ringtail via the
|
||||
# external-secrets-ringtail app.
|
||||
#
|
||||
# 1Password item: "borgmatic" in blumeops vault
|
||||
# Field: "db-password"
|
||||
apiVersion: external-secrets.io/v1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
name: immich-pg-borgmatic
|
||||
namespace: databases
|
||||
spec:
|
||||
refreshInterval: 1h
|
||||
secretStoreRef:
|
||||
kind: ClusterSecretStore
|
||||
name: onepassword-blumeops
|
||||
target:
|
||||
name: immich-pg-borgmatic
|
||||
creationPolicy: Owner
|
||||
template:
|
||||
type: kubernetes.io/basic-auth
|
||||
data:
|
||||
username: borgmatic
|
||||
password: "{{ .password }}"
|
||||
data:
|
||||
- secretKey: password
|
||||
remoteRef:
|
||||
key: borgmatic
|
||||
property: db-password
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
# ExternalSecret for Paperless database user password
|
||||
#
|
||||
# 1Password item: "Paperless (blumeops)" in blumeops vault
|
||||
# Field: "postgresql-password"
|
||||
#
|
||||
apiVersion: external-secrets.io/v1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
name: blumeops-pg-paperless
|
||||
namespace: databases
|
||||
spec:
|
||||
refreshInterval: 1h
|
||||
secretStoreRef:
|
||||
kind: ClusterSecretStore
|
||||
name: onepassword-blumeops
|
||||
target:
|
||||
name: blumeops-pg-paperless
|
||||
creationPolicy: Owner
|
||||
template:
|
||||
type: kubernetes.io/basic-auth
|
||||
data:
|
||||
username: paperless
|
||||
password: "{{ .password }}"
|
||||
data:
|
||||
- secretKey: password
|
||||
remoteRef:
|
||||
key: Paperless (blumeops)
|
||||
property: postgresql-password
|
||||
53
argocd/manifests/databases-ringtail/immich-pg.yaml
Normal file
53
argocd/manifests/databases-ringtail/immich-pg.yaml
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# PostgreSQL Cluster for Immich on ringtail k3s.
|
||||
#
|
||||
# Initially bootstrapped via CNPG pg_basebackup from the minikube
|
||||
# immich-pg cluster on 2026-05-13, then promoted to primary. The
|
||||
# externalClusters + bootstrap.pg_basebackup blocks have been pruned
|
||||
# from this manifest now that the migration is complete — leaving
|
||||
# them around is a footgun (re-enabling replica.enabled=true would
|
||||
# try to demote this cluster against a stale source). See
|
||||
# [[immich-pg-data-migration]] for the procedure used.
|
||||
apiVersion: postgresql.cnpg.io/v1
|
||||
kind: Cluster
|
||||
metadata:
|
||||
name: immich-pg
|
||||
namespace: databases
|
||||
spec:
|
||||
instances: 1
|
||||
imageName: ghcr.io/tensorchord/cloudnative-vectorchord:17-0.5.0
|
||||
|
||||
storage:
|
||||
size: 10Gi
|
||||
storageClass: local-path
|
||||
|
||||
# Managed roles
|
||||
managed:
|
||||
roles:
|
||||
- name: borgmatic
|
||||
login: true
|
||||
connectionLimit: -1
|
||||
ensure: present
|
||||
inherit: true
|
||||
inRoles:
|
||||
- pg_read_all_data
|
||||
passwordSecret:
|
||||
name: immich-pg-borgmatic
|
||||
|
||||
resources:
|
||||
requests:
|
||||
memory: "256Mi"
|
||||
cpu: "100m"
|
||||
limits:
|
||||
memory: "1Gi"
|
||||
cpu: "500m"
|
||||
|
||||
postgresql:
|
||||
shared_preload_libraries:
|
||||
- "vchord.so"
|
||||
parameters:
|
||||
max_connections: "50"
|
||||
shared_buffers: "128MB"
|
||||
password_encryption: "scram-sha-256"
|
||||
pg_hba:
|
||||
- host all all 0.0.0.0/0 scram-sha-256
|
||||
- host all all ::/0 scram-sha-256
|
||||
16
argocd/manifests/databases-ringtail/kustomization.yaml
Normal file
16
argocd/manifests/databases-ringtail/kustomization.yaml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
namespace: databases
|
||||
|
||||
resources:
|
||||
- immich-pg.yaml
|
||||
- external-secret-immich-borgmatic.yaml
|
||||
- service-immich-pg-tailscale.yaml
|
||||
# wave-1 indri-k8s decommission: blumeops-pg (paperless + teslamate)
|
||||
- blumeops-pg.yaml
|
||||
- service-blumeops-pg-tailscale.yaml
|
||||
- external-secret-eblume.yaml
|
||||
- external-secret-borgmatic.yaml
|
||||
- external-secret-paperless.yaml
|
||||
- external-secret-teslamate.yaml
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# Tailscale LoadBalancer for the ringtail blumeops-pg cluster.
|
||||
# Canonical hostname: blumeops-pg-ringtail.tail8d86e.ts.net (distinct from
|
||||
# the minikube blumeops-pg, which still owns pg.tail8d86e.ts.net until the
|
||||
# wave-1 decommission). Borgmatic on indri and the Grafana TeslaMate
|
||||
# datasource reach it via the Caddy L4 route pg.ops.eblu.me:5434.
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: blumeops-pg-tailscale
|
||||
namespace: databases
|
||||
annotations:
|
||||
tailscale.com/hostname: "blumeops-pg-ringtail"
|
||||
tailscale.com/proxy-class: "default"
|
||||
spec:
|
||||
type: LoadBalancer
|
||||
loadBalancerClass: tailscale
|
||||
selector:
|
||||
cnpg.io/cluster: blumeops-pg
|
||||
role: primary
|
||||
ports:
|
||||
- name: postgresql
|
||||
port: 5432
|
||||
targetPort: 5432
|
||||
protocol: TCP
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
# Tailscale LoadBalancer for immich-pg PostgreSQL access
|
||||
# Canonical hostname: immich-pg.tail8d86e.ts.net
|
||||
# Caddy L4 proxies pg.ops.eblu.me:5433 → this service for borgmatic backups
|
||||
# Tailscale LoadBalancer for immich-pg PostgreSQL access on ringtail.
|
||||
# Canonical hostname: immich-pg.tail8d86e.ts.net (claimed from the
|
||||
# minikube side after the minikube service was removed during the
|
||||
# immich-to-ringtail migration). Borgmatic on indri uses this
|
||||
# hostname for nightly backups.
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
|
|
@ -44,17 +44,9 @@ spec:
|
|||
- pg_read_all_data
|
||||
passwordSecret:
|
||||
name: blumeops-pg-borgmatic
|
||||
# teslamate user for TeslaMate Tesla data logger
|
||||
# Note: superuser required for extension management during migrations
|
||||
- name: teslamate
|
||||
login: true
|
||||
superuser: true
|
||||
connectionLimit: -1
|
||||
ensure: present
|
||||
inherit: true
|
||||
createdb: true
|
||||
passwordSecret:
|
||||
name: blumeops-pg-teslamate
|
||||
# teslamate + paperless roles removed: migrated to ringtail blumeops-pg
|
||||
# (wave-1 decommission). Their databases were dropped from this cluster
|
||||
# after the cutover was verified and backed up.
|
||||
# authentik user for Authentik identity provider (runs on ringtail)
|
||||
- name: authentik
|
||||
login: true
|
||||
|
|
|
|||
|
|
@ -1,69 +0,0 @@
|
|||
# PostgreSQL Cluster for Immich
|
||||
# Uses VectorChord (successor to pgvecto.rs) for AI-powered vector search
|
||||
# See: https://github.com/immich-app/immich/discussions/9060
|
||||
# Managed by CloudNativePG operator
|
||||
apiVersion: postgresql.cnpg.io/v1
|
||||
kind: Cluster
|
||||
metadata:
|
||||
name: immich-pg
|
||||
namespace: databases
|
||||
spec:
|
||||
instances: 1
|
||||
# VectorChord image for PostgreSQL 17 with VectorChord 0.5.0
|
||||
# Immich v2.4.1 requires VectorChord >=0.3 <0.6
|
||||
# See: https://github.com/tensorchord/VectorChord
|
||||
imageName: ghcr.io/tensorchord/cloudnative-vectorchord:17-0.5.0
|
||||
|
||||
storage:
|
||||
size: 10Gi
|
||||
storageClass: standard
|
||||
|
||||
# Bootstrap creates initial database and owner
|
||||
bootstrap:
|
||||
initdb:
|
||||
database: immich
|
||||
owner: immich
|
||||
postInitSQL:
|
||||
# Extensions required by Immich
|
||||
- CREATE EXTENSION IF NOT EXISTS vector;
|
||||
- CREATE EXTENSION IF NOT EXISTS vchord CASCADE;
|
||||
- CREATE EXTENSION IF NOT EXISTS cube CASCADE;
|
||||
- CREATE EXTENSION IF NOT EXISTS earthdistance CASCADE;
|
||||
|
||||
# Managed roles
|
||||
# Note: connectionLimit, ensure, inherit are CNPG defaults added to prevent ArgoCD drift
|
||||
managed:
|
||||
roles:
|
||||
# borgmatic read-only user for backups
|
||||
- name: borgmatic
|
||||
login: true
|
||||
connectionLimit: -1
|
||||
ensure: present
|
||||
inherit: true
|
||||
inRoles:
|
||||
- pg_read_all_data
|
||||
passwordSecret:
|
||||
name: immich-pg-borgmatic
|
||||
|
||||
# Resource limits for minikube environment
|
||||
resources:
|
||||
requests:
|
||||
memory: "256Mi"
|
||||
cpu: "100m"
|
||||
limits:
|
||||
memory: "1Gi"
|
||||
cpu: "500m"
|
||||
|
||||
# PostgreSQL configuration
|
||||
postgresql:
|
||||
# VectorChord requires vchord.so in shared_preload_libraries
|
||||
shared_preload_libraries:
|
||||
- "vchord.so"
|
||||
parameters:
|
||||
max_connections: "50"
|
||||
shared_buffers: "128MB"
|
||||
password_encryption: "scram-sha-256"
|
||||
pg_hba:
|
||||
# Allow connections from k8s pods
|
||||
- host all all 0.0.0.0/0 scram-sha-256
|
||||
- host all all ::/0 scram-sha-256
|
||||
|
|
@ -5,12 +5,8 @@ namespace: databases
|
|||
|
||||
resources:
|
||||
- blumeops-pg.yaml
|
||||
- immich-pg.yaml
|
||||
- service-tailscale.yaml
|
||||
- service-immich-pg-tailscale.yaml
|
||||
- service-metrics-tailscale.yaml
|
||||
- external-secret-eblume.yaml
|
||||
- external-secret-borgmatic.yaml
|
||||
- external-secret-immich-borgmatic.yaml
|
||||
- external-secret-teslamate.yaml
|
||||
- external-secret-authentik.yaml
|
||||
|
|
|
|||
|
|
@ -1,72 +0,0 @@
|
|||
# devpi PyPI Caching Proxy
|
||||
|
||||
devpi-server running in Kubernetes, providing:
|
||||
- PyPI caching proxy at `root/pypi`
|
||||
- Private package hosting at `eblume/dev`
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Create the root password secret
|
||||
|
||||
```fish
|
||||
kubectl create namespace devpi
|
||||
op inject -i argocd/manifests/devpi/secret-root.yaml.tpl | kubectl apply -f -
|
||||
```
|
||||
|
||||
### 2. Deploy via ArgoCD
|
||||
|
||||
```fish
|
||||
argocd app sync apps
|
||||
argocd app sync devpi
|
||||
```
|
||||
|
||||
The container will auto-initialize on first startup using the root password from the secret.
|
||||
|
||||
### 3. Create user and index (first time only)
|
||||
|
||||
After the pod is running:
|
||||
|
||||
```fish
|
||||
# Login to devpi as root
|
||||
uvx --from devpi-client devpi use https://pypi.tail8d86e.ts.net
|
||||
uvx --from devpi-client devpi login root
|
||||
# Enter root password when prompted
|
||||
|
||||
# Create eblume user (prompts for password - use the one from 1Password)
|
||||
uvx --from devpi-client devpi user -c eblume email=blume.erich@gmail.com
|
||||
|
||||
# Create private index inheriting from PyPI
|
||||
uvx --from devpi-client devpi index -c eblume/dev bases=root/pypi
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### As pip index (caching proxy)
|
||||
|
||||
Configure `~/.config/pip/pip.conf`:
|
||||
|
||||
```ini
|
||||
[global]
|
||||
index-url = https://pypi.tail8d86e.ts.net/root/pypi/+simple/
|
||||
trusted-host = pypi.tail8d86e.ts.net
|
||||
```
|
||||
|
||||
### Upload private packages
|
||||
|
||||
```fish
|
||||
cd ~/code/personal/your-package
|
||||
uv build
|
||||
uv publish --publish-url https://pypi.tail8d86e.ts.net/eblume/dev/
|
||||
```
|
||||
|
||||
## URLs
|
||||
|
||||
- Web UI: https://pypi.tail8d86e.ts.net
|
||||
- PyPI cache: https://pypi.tail8d86e.ts.net/root/pypi/+simple/
|
||||
- Private index: https://pypi.tail8d86e.ts.net/eblume/dev/+simple/
|
||||
|
||||
## Credentials
|
||||
|
||||
Stored in 1Password vault `blumeops`, item `kyhzfifryqnuk7jeyibmmjvxxm`:
|
||||
- `root password` - devpi root user
|
||||
- `password` - eblume user password
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
# ExternalSecret for devpi root password
|
||||
#
|
||||
# Replaces the manual op inject workflow from secret-root.yaml.tpl
|
||||
#
|
||||
# 1Password item: "devpi" in blumeops vault
|
||||
# Field: "root password"
|
||||
#
|
||||
apiVersion: external-secrets.io/v1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
name: devpi-root
|
||||
namespace: devpi
|
||||
spec:
|
||||
refreshInterval: 1h
|
||||
secretStoreRef:
|
||||
kind: ClusterSecretStore
|
||||
name: onepassword-blumeops
|
||||
target:
|
||||
name: devpi-root
|
||||
creationPolicy: Owner
|
||||
data:
|
||||
- secretKey: password
|
||||
remoteRef:
|
||||
key: devpi
|
||||
property: root password
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: devpi-tailscale
|
||||
namespace: devpi
|
||||
annotations:
|
||||
tailscale.com/proxy-class: "default"
|
||||
tailscale.com/proxy-group: "ingress"
|
||||
gethomepage.dev/enabled: "true"
|
||||
gethomepage.dev/name: "PyPI"
|
||||
gethomepage.dev/group: "Infrastructure"
|
||||
gethomepage.dev/icon: "pypi.png"
|
||||
gethomepage.dev/description: "PyPI cache"
|
||||
gethomepage.dev/href: "https://pypi.ops.eblu.me"
|
||||
gethomepage.dev/pod-selector: "app=devpi"
|
||||
spec:
|
||||
ingressClassName: tailscale
|
||||
defaultBackend:
|
||||
service:
|
||||
name: devpi
|
||||
port:
|
||||
number: 3141
|
||||
tls:
|
||||
- hosts:
|
||||
- pypi
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
namespace: devpi
|
||||
|
||||
resources:
|
||||
- statefulset.yaml
|
||||
- service.yaml
|
||||
- ingress-tailscale.yaml
|
||||
- external-secret.yaml
|
||||
|
||||
images:
|
||||
- name: registry.ops.eblu.me/blumeops/devpi
|
||||
newTag: v6.19.1-613f05d
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue