diff --git a/.flox/env/manifest.lock b/.flox/env/manifest.lock index 06955e343..cb9049bd2 100644 --- a/.flox/env/manifest.lock +++ b/.flox/env/manifest.lock @@ -143,27 +143,27 @@ { "attr_path": "rust-analyzer", "broken": false, - "derivation": "/nix/store/5yp1z5rpignnnyd5j6wllwd7zn03fqiq-rust-analyzer-2025-10-28.drv", - "description": "Modular compiler frontend for the Rust language", + "derivation": "/nix/store/4d5zm2za90kxmjyl6g02hp18cfb7j7jd-rust-analyzer-2026-02-16.drv", + "description": "Language server for the Rust language", "install_id": "rust-analyzer", "license": "[ MIT, Apache-2.0 ]", - "locked_url": "https://github.com/flox/nixpkgs?rev=b3d51a0365f6695e7dd5cdf3e180604530ed33b4", - "name": "rust-analyzer-2025-10-28", + "locked_url": "https://github.com/flox/nixpkgs?rev=2fc6539b481e1d2569f25f8799236694180c0993", + "name": "rust-analyzer-2026-02-16", "pname": "rust-analyzer", - "rev": "b3d51a0365f6695e7dd5cdf3e180604530ed33b4", - "rev_count": 888552, - "rev_date": "2025-11-02T19:18:41Z", - "scrape_date": "2025-11-04T00:36:47.279901Z", + "rev": "2fc6539b481e1d2569f25f8799236694180c0993", + "rev_count": 953160, + "rev_date": "2026-02-23T12:05:20Z", + "scrape_date": "2026-02-25T04:47:31.324691Z", "stabilities": [ "unstable" ], "unfree": false, - "version": "2025-10-28", + "version": "2026-02-16", "outputs_to_install": [ "out" ], "outputs": { - "out": "/nix/store/z63nrc6qyz8d6cj2aknh62w8sxbfgl57-rust-analyzer-2025-10-28" + "out": "/nix/store/nz6l7l5kgf7dmgiq35b3pr00smcd39aj-rust-analyzer-2026-02-16" }, "system": "aarch64-darwin", "group": "rust-analyzer", @@ -172,27 +172,27 @@ { "attr_path": "rust-analyzer", "broken": false, - "derivation": "/nix/store/89whiiy4mf70fma6s4fbmrxm6qj2fzfk-rust-analyzer-2025-10-28.drv", - "description": "Modular compiler frontend for the Rust language", + "derivation": "/nix/store/gx09p3kw0ql9lasbk56nkym7n3dynxvz-rust-analyzer-2026-02-16.drv", + "description": "Language server for the Rust language", "install_id": "rust-analyzer", "license": "[ MIT, Apache-2.0 ]", - "locked_url": "https://github.com/flox/nixpkgs?rev=b3d51a0365f6695e7dd5cdf3e180604530ed33b4", - "name": "rust-analyzer-2025-10-28", + "locked_url": "https://github.com/flox/nixpkgs?rev=2fc6539b481e1d2569f25f8799236694180c0993", + "name": "rust-analyzer-2026-02-16", "pname": "rust-analyzer", - "rev": "b3d51a0365f6695e7dd5cdf3e180604530ed33b4", - "rev_count": 888552, - "rev_date": "2025-11-02T19:18:41Z", - "scrape_date": "2025-11-05T16:31:02.704172Z", + "rev": "2fc6539b481e1d2569f25f8799236694180c0993", + "rev_count": 953160, + "rev_date": "2026-02-23T12:05:20Z", + "scrape_date": "2026-02-25T05:25:58.570749Z", "stabilities": [ "unstable" ], "unfree": false, - "version": "2025-10-28", + "version": "2026-02-16", "outputs_to_install": [ "out" ], "outputs": { - "out": "/nix/store/2yasv8af8g67phhwr2gnmys06bjj53ax-rust-analyzer-2025-10-28" + "out": "/nix/store/8wl18z22vf7y7kj77k4a3y4a63kzrlpq-rust-analyzer-2026-02-16" }, "system": "aarch64-linux", "group": "rust-analyzer", @@ -201,27 +201,27 @@ { "attr_path": "rust-analyzer", "broken": false, - "derivation": "/nix/store/8s5q839liwggqa7qghfwgpzg4lfx702m-rust-analyzer-2025-10-28.drv", - "description": "Modular compiler frontend for the Rust language", + "derivation": "/nix/store/jjc37n1lvlgnx40fvyc86svadp8l3vgh-rust-analyzer-2026-02-16.drv", + "description": "Language server for the Rust language", "install_id": "rust-analyzer", "license": "[ MIT, Apache-2.0 ]", - "locked_url": "https://github.com/flox/nixpkgs?rev=b3d51a0365f6695e7dd5cdf3e180604530ed33b4", - "name": "rust-analyzer-2025-10-28", + "locked_url": "https://github.com/flox/nixpkgs?rev=2fc6539b481e1d2569f25f8799236694180c0993", + "name": "rust-analyzer-2026-02-16", "pname": "rust-analyzer", - "rev": "b3d51a0365f6695e7dd5cdf3e180604530ed33b4", - "rev_count": 888552, - "rev_date": "2025-11-02T19:18:41Z", - "scrape_date": "2025-11-05T16:56:23.553783Z", + "rev": "2fc6539b481e1d2569f25f8799236694180c0993", + "rev_count": 953160, + "rev_date": "2026-02-23T12:05:20Z", + "scrape_date": "2026-02-25T06:10:37.981612Z", "stabilities": [ "unstable" ], "unfree": false, - "version": "2025-10-28", + "version": "2026-02-16", "outputs_to_install": [ "out" ], "outputs": { - "out": "/nix/store/r1afkswg4jc0kdrfag27ghn27xgf7cy6-rust-analyzer-2025-10-28" + "out": "/nix/store/7zxbyg0cr4ajg1cnfdfahwhl37jl5jdg-rust-analyzer-2026-02-16" }, "system": "x86_64-darwin", "group": "rust-analyzer", @@ -230,27 +230,27 @@ { "attr_path": "rust-analyzer", "broken": false, - "derivation": "/nix/store/gmal103ypiy314y75w08xclg6kbg600h-rust-analyzer-2025-10-28.drv", - "description": "Modular compiler frontend for the Rust language", + "derivation": "/nix/store/krap4ws4gg5ph5qq19vilzjijqr44s1b-rust-analyzer-2026-02-16.drv", + "description": "Language server for the Rust language", "install_id": "rust-analyzer", "license": "[ MIT, Apache-2.0 ]", - "locked_url": "https://github.com/flox/nixpkgs?rev=b3d51a0365f6695e7dd5cdf3e180604530ed33b4", - "name": "rust-analyzer-2025-10-28", + "locked_url": "https://github.com/flox/nixpkgs?rev=2fc6539b481e1d2569f25f8799236694180c0993", + "name": "rust-analyzer-2026-02-16", "pname": "rust-analyzer", - "rev": "b3d51a0365f6695e7dd5cdf3e180604530ed33b4", - "rev_count": 888552, - "rev_date": "2025-11-02T19:18:41Z", - "scrape_date": "2025-11-05T17:23:04.056087Z", + "rev": "2fc6539b481e1d2569f25f8799236694180c0993", + "rev_count": 953160, + "rev_date": "2026-02-23T12:05:20Z", + "scrape_date": "2026-02-25T07:02:20.175088Z", "stabilities": [ "unstable" ], "unfree": false, - "version": "2025-10-28", + "version": "2026-02-16", "outputs_to_install": [ "out" ], "outputs": { - "out": "/nix/store/pflbq4iyh3rpbanba11lp3s9y859i55y-rust-analyzer-2025-10-28" + "out": "/nix/store/3h49id8siw8mlpkk47n8bxbkhiqwwc15-rust-analyzer-2026-02-16" }, "system": "x86_64-linux", "group": "rust-analyzer", @@ -259,28 +259,27 @@ { "attr_path": "cargo", "broken": false, - "derivation": "/nix/store/smqakq8ly0wv3q2jvhvcihx2qd4bmi9i-cargo-1.92.0.drv", + "derivation": "/nix/store/s5ydixqkqxq4lmmmapb1zzbrsfpm21wd-cargo-1.93.0.drv", "description": "Downloads your Rust project's dependencies and builds your project", "install_id": "cargo", "license": "[ MIT, Apache-2.0 ]", - "locked_url": "https://github.com/flox/nixpkgs?rev=00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "name": "cargo-1.92.0", + "locked_url": "https://github.com/flox/nixpkgs?rev=2fc6539b481e1d2569f25f8799236694180c0993", + "name": "cargo-1.93.0", "pname": "cargo", - "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "rev_count": 940249, - "rev_date": "2026-02-04T09:32:58Z", - "scrape_date": "2026-02-06T04:35:54.555765Z", + "rev": "2fc6539b481e1d2569f25f8799236694180c0993", + "rev_count": 953160, + "rev_date": "2026-02-23T12:05:20Z", + "scrape_date": "2026-02-25T04:46:01.680663Z", "stabilities": [ - "staging", "unstable" ], "unfree": false, - "version": "1.92.0", + "version": "1.93.0", "outputs_to_install": [ "out" ], "outputs": { - "out": "/nix/store/v4bvnkm0p5x41fhybskr0cf2zvkgyrvv-cargo-1.92.0" + "out": "/nix/store/v21x5yl04z0l303iz6ir5aqy9jzzrn76-cargo-1.93.0" }, "system": "aarch64-darwin", "group": "rust-toolchain", @@ -289,28 +288,27 @@ { "attr_path": "cargo", "broken": false, - "derivation": "/nix/store/y2jp95g3j7vprw96lllsxliklvxh4icv-cargo-1.92.0.drv", + "derivation": "/nix/store/80jzjl5b1ln84qpl1rl1qj2l9silh0q5-cargo-1.93.0.drv", "description": "Downloads your Rust project's dependencies and builds your project", "install_id": "cargo", "license": "[ MIT, Apache-2.0 ]", - "locked_url": "https://github.com/flox/nixpkgs?rev=00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "name": "cargo-1.92.0", + "locked_url": "https://github.com/flox/nixpkgs?rev=2fc6539b481e1d2569f25f8799236694180c0993", + "name": "cargo-1.93.0", "pname": "cargo", - "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "rev_count": 940249, - "rev_date": "2026-02-04T09:32:58Z", - "scrape_date": "2026-02-06T05:05:22.879191Z", + "rev": "2fc6539b481e1d2569f25f8799236694180c0993", + "rev_count": 953160, + "rev_date": "2026-02-23T12:05:20Z", + "scrape_date": "2026-02-25T05:24:01.157918Z", "stabilities": [ - "staging", "unstable" ], "unfree": false, - "version": "1.92.0", + "version": "1.93.0", "outputs_to_install": [ "out" ], "outputs": { - "out": "/nix/store/piiqs6x2m8gv0n3z3pys8scn0y673piy-cargo-1.92.0" + "out": "/nix/store/s7hb3dlngzws3rxjd30x9n6c7l2vmx2f-cargo-1.93.0" }, "system": "aarch64-linux", "group": "rust-toolchain", @@ -319,28 +317,27 @@ { "attr_path": "cargo", "broken": false, - "derivation": "/nix/store/60bc9hzhvc4bl212dnj4450y58hwmaaw-cargo-1.92.0.drv", + "derivation": "/nix/store/w54h94rl3ff8gpgipgp2xh7jnh05165s-cargo-1.93.0.drv", "description": "Downloads your Rust project's dependencies and builds your project", "install_id": "cargo", "license": "[ MIT, Apache-2.0 ]", - "locked_url": "https://github.com/flox/nixpkgs?rev=00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "name": "cargo-1.92.0", + "locked_url": "https://github.com/flox/nixpkgs?rev=2fc6539b481e1d2569f25f8799236694180c0993", + "name": "cargo-1.93.0", "pname": "cargo", - "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "rev_count": 940249, - "rev_date": "2026-02-04T09:32:58Z", - "scrape_date": "2026-02-06T05:33:32.762094Z", + "rev": "2fc6539b481e1d2569f25f8799236694180c0993", + "rev_count": 953160, + "rev_date": "2026-02-23T12:05:20Z", + "scrape_date": "2026-02-25T06:09:08.285201Z", "stabilities": [ - "staging", "unstable" ], "unfree": false, - "version": "1.92.0", + "version": "1.93.0", "outputs_to_install": [ "out" ], "outputs": { - "out": "/nix/store/wf6279pgydd1nny4s5nx1msian6dbf9p-cargo-1.92.0" + "out": "/nix/store/js7zd17w17ha6na0qj2rb052rpgjm69l-cargo-1.93.0" }, "system": "x86_64-darwin", "group": "rust-toolchain", @@ -349,28 +346,27 @@ { "attr_path": "cargo", "broken": false, - "derivation": "/nix/store/3gdr50smf0vcnv3v8slv9v3dd4splibj-cargo-1.92.0.drv", + "derivation": "/nix/store/mgbkjjzar3m144racl7sfnqq5pjfwb23-cargo-1.93.0.drv", "description": "Downloads your Rust project's dependencies and builds your project", "install_id": "cargo", "license": "[ MIT, Apache-2.0 ]", - "locked_url": "https://github.com/flox/nixpkgs?rev=00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "name": "cargo-1.92.0", + "locked_url": "https://github.com/flox/nixpkgs?rev=2fc6539b481e1d2569f25f8799236694180c0993", + "name": "cargo-1.93.0", "pname": "cargo", - "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "rev_count": 940249, - "rev_date": "2026-02-04T09:32:58Z", - "scrape_date": "2026-02-06T06:05:45.729819Z", + "rev": "2fc6539b481e1d2569f25f8799236694180c0993", + "rev_count": 953160, + "rev_date": "2026-02-23T12:05:20Z", + "scrape_date": "2026-02-25T07:00:09.716055Z", "stabilities": [ - "staging", "unstable" ], "unfree": false, - "version": "1.92.0", + "version": "1.93.0", "outputs_to_install": [ "out" ], "outputs": { - "out": "/nix/store/yqcsaywfvcyy9wmbzb5fawp29icgi7cb-cargo-1.92.0" + "out": "/nix/store/p96faw8wiwi10cy94hr42gzmiapv15fm-cargo-1.93.0" }, "system": "x86_64-linux", "group": "rust-toolchain", @@ -379,28 +375,27 @@ { "attr_path": "cargo-llvm-cov", "broken": false, - "derivation": "/nix/store/c6cqp91i92qbj0w0mwrcam7wjnmri3az-cargo-llvm-cov-0.8.1.drv", + "derivation": "/nix/store/6kz6sp2jl0dcirj24sbr0awby6876r52-cargo-llvm-cov-0.8.3.drv", "description": "Cargo subcommand to easily use LLVM source-based code coverage", "install_id": "cargo-llvm-cov", "license": "[ Apache-2.0, MIT ]", - "locked_url": "https://github.com/flox/nixpkgs?rev=00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "name": "cargo-llvm-cov-0.8.1", + "locked_url": "https://github.com/flox/nixpkgs?rev=2fc6539b481e1d2569f25f8799236694180c0993", + "name": "cargo-llvm-cov-0.8.3", "pname": "cargo-llvm-cov", - "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "rev_count": 940249, - "rev_date": "2026-02-04T09:32:58Z", - "scrape_date": "2026-02-06T04:35:54.565395Z", + "rev": "2fc6539b481e1d2569f25f8799236694180c0993", + "rev_count": 953160, + "rev_date": "2026-02-23T12:05:20Z", + "scrape_date": "2026-02-25T04:46:01.689828Z", "stabilities": [ - "staging", "unstable" ], "unfree": false, - "version": "0.8.1", + "version": "0.8.3", "outputs_to_install": [ "out" ], "outputs": { - "out": "/nix/store/7frfknj5qf10i9cpv0a9mds57hvfxkqi-cargo-llvm-cov-0.8.1" + "out": "/nix/store/bpwpy35xgm5hmqj7zqwi1p0v864m5p65-cargo-llvm-cov-0.8.3" }, "system": "aarch64-darwin", "group": "rust-toolchain", @@ -409,28 +404,27 @@ { "attr_path": "cargo-llvm-cov", "broken": false, - "derivation": "/nix/store/spga7zbajn56rizapaz94snkx6g5pzqm-cargo-llvm-cov-0.8.1.drv", + "derivation": "/nix/store/xsp63pcaj9iciwwzmb0ga6p4rq75kmi3-cargo-llvm-cov-0.8.3.drv", "description": "Cargo subcommand to easily use LLVM source-based code coverage", "install_id": "cargo-llvm-cov", "license": "[ Apache-2.0, MIT ]", - "locked_url": "https://github.com/flox/nixpkgs?rev=00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "name": "cargo-llvm-cov-0.8.1", + "locked_url": "https://github.com/flox/nixpkgs?rev=2fc6539b481e1d2569f25f8799236694180c0993", + "name": "cargo-llvm-cov-0.8.3", "pname": "cargo-llvm-cov", - "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "rev_count": 940249, - "rev_date": "2026-02-04T09:32:58Z", - "scrape_date": "2026-02-06T05:05:22.889428Z", + "rev": "2fc6539b481e1d2569f25f8799236694180c0993", + "rev_count": 953160, + "rev_date": "2026-02-23T12:05:20Z", + "scrape_date": "2026-02-25T05:24:01.168260Z", "stabilities": [ - "staging", "unstable" ], "unfree": false, - "version": "0.8.1", + "version": "0.8.3", "outputs_to_install": [ "out" ], "outputs": { - "out": "/nix/store/f0nmca75idfm62cn0fvm9h15zkibxnzb-cargo-llvm-cov-0.8.1" + "out": "/nix/store/1v9pq1gvnnpsfdxfb99hq257wn1drljj-cargo-llvm-cov-0.8.3" }, "system": "aarch64-linux", "group": "rust-toolchain", @@ -439,28 +433,27 @@ { "attr_path": "cargo-llvm-cov", "broken": false, - "derivation": "/nix/store/9bszrzikv706sxs21lkhfyl9q0nrvr6q-cargo-llvm-cov-0.8.1.drv", + "derivation": "/nix/store/y92y67kwb3pbvqy1b2vvshzj5d7vxbqj-cargo-llvm-cov-0.8.3.drv", "description": "Cargo subcommand to easily use LLVM source-based code coverage", "install_id": "cargo-llvm-cov", "license": "[ Apache-2.0, MIT ]", - "locked_url": "https://github.com/flox/nixpkgs?rev=00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "name": "cargo-llvm-cov-0.8.1", + "locked_url": "https://github.com/flox/nixpkgs?rev=2fc6539b481e1d2569f25f8799236694180c0993", + "name": "cargo-llvm-cov-0.8.3", "pname": "cargo-llvm-cov", - "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "rev_count": 940249, - "rev_date": "2026-02-04T09:32:58Z", - "scrape_date": "2026-02-06T05:33:32.771124Z", + "rev": "2fc6539b481e1d2569f25f8799236694180c0993", + "rev_count": 953160, + "rev_date": "2026-02-23T12:05:20Z", + "scrape_date": "2026-02-25T06:09:08.294479Z", "stabilities": [ - "staging", "unstable" ], "unfree": false, - "version": "0.8.1", + "version": "0.8.3", "outputs_to_install": [ "out" ], "outputs": { - "out": "/nix/store/86g1j4w8icws77b2340csly671i9fxf8-cargo-llvm-cov-0.8.1" + "out": "/nix/store/85rllsmy728jg82glyjj7cikf6r1ajn9-cargo-llvm-cov-0.8.3" }, "system": "x86_64-darwin", "group": "rust-toolchain", @@ -469,28 +462,27 @@ { "attr_path": "cargo-llvm-cov", "broken": false, - "derivation": "/nix/store/wm1hi0ai83c2r2bv5jf1gai532z4nmx2-cargo-llvm-cov-0.8.1.drv", + "derivation": "/nix/store/bx9y409rjn9kra12p465v3vrvz3n9qgs-cargo-llvm-cov-0.8.3.drv", "description": "Cargo subcommand to easily use LLVM source-based code coverage", "install_id": "cargo-llvm-cov", "license": "[ Apache-2.0, MIT ]", - "locked_url": "https://github.com/flox/nixpkgs?rev=00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "name": "cargo-llvm-cov-0.8.1", + "locked_url": "https://github.com/flox/nixpkgs?rev=2fc6539b481e1d2569f25f8799236694180c0993", + "name": "cargo-llvm-cov-0.8.3", "pname": "cargo-llvm-cov", - "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "rev_count": 940249, - "rev_date": "2026-02-04T09:32:58Z", - "scrape_date": "2026-02-06T06:05:45.741110Z", + "rev": "2fc6539b481e1d2569f25f8799236694180c0993", + "rev_count": 953160, + "rev_date": "2026-02-23T12:05:20Z", + "scrape_date": "2026-02-25T07:00:09.727467Z", "stabilities": [ - "staging", "unstable" ], "unfree": false, - "version": "0.8.1", + "version": "0.8.3", "outputs_to_install": [ "out" ], "outputs": { - "out": "/nix/store/33h1j6s89qvs16hsymd10xilqynpk9fm-cargo-llvm-cov-0.8.1" + "out": "/nix/store/ij0k5h6b745arc3hn0iwbxps0wynwf98-cargo-llvm-cov-0.8.3" }, "system": "x86_64-linux", "group": "rust-toolchain", @@ -499,28 +491,27 @@ { "attr_path": "clippy", "broken": false, - "derivation": "/nix/store/75mwy56m7jc5vwbq5d2gjzgad0inn4sx-clippy-1.92.0.drv", + "derivation": "/nix/store/pc6s400jrm9vwhn3w5kk04ik41xiz334-clippy-1.93.0.drv", "description": "Bunch of lints to catch common mistakes and improve your Rust code", "install_id": "clippy", "license": "[ MIT, Apache-2.0 ]", - "locked_url": "https://github.com/flox/nixpkgs?rev=00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "name": "clippy-1.92.0", + "locked_url": "https://github.com/flox/nixpkgs?rev=2fc6539b481e1d2569f25f8799236694180c0993", + "name": "clippy-1.93.0", "pname": "clippy", - "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "rev_count": 940249, - "rev_date": "2026-02-04T09:32:58Z", - "scrape_date": "2026-02-06T04:35:54.732255Z", + "rev": "2fc6539b481e1d2569f25f8799236694180c0993", + "rev_count": 953160, + "rev_date": "2026-02-23T12:05:20Z", + "scrape_date": "2026-02-25T04:46:01.879594Z", "stabilities": [ - "staging", "unstable" ], "unfree": false, - "version": "1.92.0", + "version": "1.93.0", "outputs_to_install": [ "out" ], "outputs": { - "out": "/nix/store/phhksxd0vv7ml9imsr0lwiqvvmiaz23p-clippy-1.92.0" + "out": "/nix/store/xaqjf38v233s2i60jpjhv9akyd5zkw0m-clippy-1.93.0" }, "system": "aarch64-darwin", "group": "rust-toolchain", @@ -529,29 +520,28 @@ { "attr_path": "clippy", "broken": false, - "derivation": "/nix/store/s5g78r9ab453qrfxhj27id4zibj9jcs3-clippy-1.92.0.drv", + "derivation": "/nix/store/fisnnif34a4fgfgy5mwax03ncxpkca6i-clippy-1.93.0.drv", "description": "Bunch of lints to catch common mistakes and improve your Rust code", "install_id": "clippy", "license": "[ MIT, Apache-2.0 ]", - "locked_url": "https://github.com/flox/nixpkgs?rev=00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "name": "clippy-1.92.0", + "locked_url": "https://github.com/flox/nixpkgs?rev=2fc6539b481e1d2569f25f8799236694180c0993", + "name": "clippy-1.93.0", "pname": "clippy", - "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "rev_count": 940249, - "rev_date": "2026-02-04T09:32:58Z", - "scrape_date": "2026-02-06T05:05:23.107330Z", + "rev": "2fc6539b481e1d2569f25f8799236694180c0993", + "rev_count": 953160, + "rev_date": "2026-02-23T12:05:20Z", + "scrape_date": "2026-02-25T05:24:01.393266Z", "stabilities": [ - "staging", "unstable" ], "unfree": false, - "version": "1.92.0", + "version": "1.93.0", "outputs_to_install": [ "out" ], "outputs": { - "debug": "/nix/store/anj7w4nnbw9cvsrsq85vnwqlw8miss5n-clippy-1.92.0-debug", - "out": "/nix/store/1iqky89dxzj12yy9dsbyrarknswx6iyj-clippy-1.92.0" + "debug": "/nix/store/2p0f4abpa9w0cgi7853ad8fqiv4rzsv2-clippy-1.93.0-debug", + "out": "/nix/store/gypngfg4hdpsfvjpswa95g77zz47r78v-clippy-1.93.0" }, "system": "aarch64-linux", "group": "rust-toolchain", @@ -560,28 +550,27 @@ { "attr_path": "clippy", "broken": false, - "derivation": "/nix/store/0i67r0aa9kp6njf0ji01pwbp76y6xn96-clippy-1.92.0.drv", + "derivation": "/nix/store/zxpzwb9dsh0zj09fv0gyaxfp59k1r8iq-clippy-1.93.0.drv", "description": "Bunch of lints to catch common mistakes and improve your Rust code", "install_id": "clippy", "license": "[ MIT, Apache-2.0 ]", - "locked_url": "https://github.com/flox/nixpkgs?rev=00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "name": "clippy-1.92.0", + "locked_url": "https://github.com/flox/nixpkgs?rev=2fc6539b481e1d2569f25f8799236694180c0993", + "name": "clippy-1.93.0", "pname": "clippy", - "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "rev_count": 940249, - "rev_date": "2026-02-04T09:32:58Z", - "scrape_date": "2026-02-06T05:33:32.940903Z", + "rev": "2fc6539b481e1d2569f25f8799236694180c0993", + "rev_count": 953160, + "rev_date": "2026-02-23T12:05:20Z", + "scrape_date": "2026-02-25T06:09:08.474693Z", "stabilities": [ - "staging", "unstable" ], "unfree": false, - "version": "1.92.0", + "version": "1.93.0", "outputs_to_install": [ "out" ], "outputs": { - "out": "/nix/store/34yqzpcjb1cms86lij7wkszcjxdivx6f-clippy-1.92.0" + "out": "/nix/store/i3m6fpdk1n1nqbb5gqzyjsn1lksqp60b-clippy-1.93.0" }, "system": "x86_64-darwin", "group": "rust-toolchain", @@ -590,29 +579,28 @@ { "attr_path": "clippy", "broken": false, - "derivation": "/nix/store/jpbxcqbqahvws2n40fx7j7iksn0nmb61-clippy-1.92.0.drv", + "derivation": "/nix/store/p8b3rc3dg1jp8w6mw609w9b7slnzgwlj-clippy-1.93.0.drv", "description": "Bunch of lints to catch common mistakes and improve your Rust code", "install_id": "clippy", "license": "[ MIT, Apache-2.0 ]", - "locked_url": "https://github.com/flox/nixpkgs?rev=00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "name": "clippy-1.92.0", + "locked_url": "https://github.com/flox/nixpkgs?rev=2fc6539b481e1d2569f25f8799236694180c0993", + "name": "clippy-1.93.0", "pname": "clippy", - "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "rev_count": 940249, - "rev_date": "2026-02-04T09:32:58Z", - "scrape_date": "2026-02-06T06:05:45.974631Z", + "rev": "2fc6539b481e1d2569f25f8799236694180c0993", + "rev_count": 953160, + "rev_date": "2026-02-23T12:05:20Z", + "scrape_date": "2026-02-25T07:00:09.981855Z", "stabilities": [ - "staging", "unstable" ], "unfree": false, - "version": "1.92.0", + "version": "1.93.0", "outputs_to_install": [ "out" ], "outputs": { - "debug": "/nix/store/yvamzvy4r4ml91sl0fap3jvp12wgwflx-clippy-1.92.0-debug", - "out": "/nix/store/zmrbcbd77w6nylgwyagnxh87all5swjf-clippy-1.92.0" + "debug": "/nix/store/5v47mbiidhck8rzw141hvmwkh515pfg3-clippy-1.93.0-debug", + "out": "/nix/store/0yj3hz7imj1zxmppn9gfps8h2jmvdi7g-clippy-1.93.0" }, "system": "x86_64-linux", "group": "rust-toolchain", @@ -621,32 +609,30 @@ { "attr_path": "llvm", "broken": false, - "derivation": "/nix/store/a0z2gcp85jm7568c9hrbl5zdbflgjnsl-llvm-21.1.8.drv", + "derivation": "/nix/store/si99zkx8xm3m8whfnvli5xr7ch08m265-llvm-21.1.8.drv", "description": "Collection of modular and reusable compiler and toolchain technologies", "install_id": "llvm", "license": "[ NCSA, Apache-2.0, LLVM-exception ]", - "locked_url": "https://github.com/flox/nixpkgs?rev=00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", + "locked_url": "https://github.com/flox/nixpkgs?rev=2fc6539b481e1d2569f25f8799236694180c0993", "name": "llvm-21.1.8", "pname": "llvm", - "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "rev_count": 940249, - "rev_date": "2026-02-04T09:32:58Z", - "scrape_date": "2026-02-06T04:36:12.840423Z", + "rev": "2fc6539b481e1d2569f25f8799236694180c0993", + "rev_count": 953160, + "rev_date": "2026-02-23T12:05:20Z", + "scrape_date": "2026-02-25T04:46:21.102613Z", "stabilities": [ - "staging", "unstable" ], "unfree": false, "version": "21.1.8", "outputs_to_install": [ - "out", "out" ], "outputs": { - "dev": "/nix/store/y96jxh4yxjvq6dhri26x35370w4vzpz9-llvm-21.1.8-dev", - "lib": "/nix/store/ixyxn9ipysnrr3q2x5mlfhbkl4ada78j-llvm-21.1.8-lib", - "out": "/nix/store/yq033yr0zwx5zcqmfgpdzl751xs9qzzb-llvm-21.1.8", - "python": "/nix/store/454pgdykfy2nwhd0cz1lgdi0v7ngk7yl-llvm-21.1.8-python" + "dev": "/nix/store/d3cxzbxgc4d5rx608c5kis89ym54bl5i-llvm-21.1.8-dev", + "lib": "/nix/store/225wp7066qa3v33bw76hzlh386qfn5y7-llvm-21.1.8-lib", + "out": "/nix/store/8w308r33lb6d0ijmypa802v6dvyswkcj-llvm-21.1.8", + "python": "/nix/store/cbqajgq06ailrg2m7isf2z0xfcim9fmn-llvm-21.1.8-python" }, "system": "aarch64-darwin", "group": "rust-toolchain", @@ -655,19 +641,18 @@ { "attr_path": "llvm", "broken": false, - "derivation": "/nix/store/lfqpgrr022iyxrs952i7376i2nw4nnw1-llvm-21.1.8.drv", + "derivation": "/nix/store/64yqhwcjp4ws97vp43csh96cz0nchxkp-llvm-21.1.8.drv", "description": "Collection of modular and reusable compiler and toolchain technologies", "install_id": "llvm", "license": "[ NCSA, Apache-2.0, LLVM-exception ]", - "locked_url": "https://github.com/flox/nixpkgs?rev=00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", + "locked_url": "https://github.com/flox/nixpkgs?rev=2fc6539b481e1d2569f25f8799236694180c0993", "name": "llvm-21.1.8", "pname": "llvm", - "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "rev_count": 940249, - "rev_date": "2026-02-04T09:32:58Z", - "scrape_date": "2026-02-06T05:05:51.100366Z", + "rev": "2fc6539b481e1d2569f25f8799236694180c0993", + "rev_count": 953160, + "rev_date": "2026-02-23T12:05:20Z", + "scrape_date": "2026-02-25T05:24:30.040763Z", "stabilities": [ - "staging", "unstable" ], "unfree": false, @@ -676,10 +661,10 @@ "out" ], "outputs": { - "dev": "/nix/store/4s30hy5240j3rvn10w4v55c9xvdn2slb-llvm-21.1.8-dev", - "lib": "/nix/store/fq36ddj5says0g96sv408zzrzvrdxb42-llvm-21.1.8-lib", - "out": "/nix/store/p15k615lkir3sxkj066yww4sfiz1b81n-llvm-21.1.8", - "python": "/nix/store/5mvv9pv1kadg1frjs0kmsk8x905x5kmd-llvm-21.1.8-python" + "dev": "/nix/store/nrkp8m93v7bxs9zvynkv315xxdh4mzfh-llvm-21.1.8-dev", + "lib": "/nix/store/qv86jhs6f6n4xpakldc15kgrd0mq92bk-llvm-21.1.8-lib", + "out": "/nix/store/bgggkfycnqg5yhzmvbw8djs02gavcyb2-llvm-21.1.8", + "python": "/nix/store/4zdkg137zsjbvs3846yi7j3lkb0f67by-llvm-21.1.8-python" }, "system": "aarch64-linux", "group": "rust-toolchain", @@ -688,19 +673,18 @@ { "attr_path": "llvm", "broken": false, - "derivation": "/nix/store/va5vrxggkampw9jh2dhycwijyyqs7si8-llvm-21.1.8.drv", + "derivation": "/nix/store/fqskj626217h0p1qcy26ks3ympf313pi-llvm-21.1.8.drv", "description": "Collection of modular and reusable compiler and toolchain technologies", "install_id": "llvm", "license": "[ NCSA, Apache-2.0, LLVM-exception ]", - "locked_url": "https://github.com/flox/nixpkgs?rev=00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", + "locked_url": "https://github.com/flox/nixpkgs?rev=2fc6539b481e1d2569f25f8799236694180c0993", "name": "llvm-21.1.8", "pname": "llvm", - "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "rev_count": 940249, - "rev_date": "2026-02-04T09:32:58Z", - "scrape_date": "2026-02-06T05:33:51.129745Z", + "rev": "2fc6539b481e1d2569f25f8799236694180c0993", + "rev_count": 953160, + "rev_date": "2026-02-23T12:05:20Z", + "scrape_date": "2026-02-25T06:09:27.192692Z", "stabilities": [ - "staging", "unstable" ], "unfree": false, @@ -709,10 +693,10 @@ "out" ], "outputs": { - "dev": "/nix/store/z2l891zsd5rzxsjhyzi552mg1lnq1yc0-llvm-21.1.8-dev", - "lib": "/nix/store/39h8zyrkknws8966p5kxgbyqcbc28q52-llvm-21.1.8-lib", - "out": "/nix/store/8wqrv04hhq3smj5id34402gdnjr44j2v-llvm-21.1.8", - "python": "/nix/store/kwia8hcfl8xfmgcqrvw7ii85cbdmb03k-llvm-21.1.8-python" + "dev": "/nix/store/2ykacfbqd7462bapcgbq335gxbjhad8r-llvm-21.1.8-dev", + "lib": "/nix/store/caczl629k3p7slsnh0f277n7qmy0nvb0-llvm-21.1.8-lib", + "out": "/nix/store/xr01y2hxsjqi30g8fkz5pbr768h3r4yd-llvm-21.1.8", + "python": "/nix/store/ypg1wv12k90k9b7xvdsjnhkhmlfrdcla-llvm-21.1.8-python" }, "system": "x86_64-darwin", "group": "rust-toolchain", @@ -721,19 +705,18 @@ { "attr_path": "llvm", "broken": false, - "derivation": "/nix/store/ilgysj1s90haj2ks4fr3v1a1h5kqplci-llvm-21.1.8.drv", + "derivation": "/nix/store/0k9k6480briirixyhk4z1vlhifsaj0b6-llvm-21.1.8.drv", "description": "Collection of modular and reusable compiler and toolchain technologies", "install_id": "llvm", "license": "[ NCSA, Apache-2.0, LLVM-exception ]", - "locked_url": "https://github.com/flox/nixpkgs?rev=00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", + "locked_url": "https://github.com/flox/nixpkgs?rev=2fc6539b481e1d2569f25f8799236694180c0993", "name": "llvm-21.1.8", "pname": "llvm", - "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "rev_count": 940249, - "rev_date": "2026-02-04T09:32:58Z", - "scrape_date": "2026-02-06T06:06:16.945436Z", + "rev": "2fc6539b481e1d2569f25f8799236694180c0993", + "rev_count": 953160, + "rev_date": "2026-02-23T12:05:20Z", + "scrape_date": "2026-02-25T07:00:43.178204Z", "stabilities": [ - "staging", "unstable" ], "unfree": false, @@ -742,10 +725,10 @@ "out" ], "outputs": { - "dev": "/nix/store/f36xxmmzivp37h20k4id9a1vximc508n-llvm-21.1.8-dev", - "lib": "/nix/store/apf2d7f948m7qch6vnk6i3rhrky0pd9v-llvm-21.1.8-lib", - "out": "/nix/store/5chxlq8nygpr9y6y9y9aqc3fsa28ng8w-llvm-21.1.8", - "python": "/nix/store/hyvq2nabmfhan1751dwb0dadrqc6nnlw-llvm-21.1.8-python" + "dev": "/nix/store/g8r229mkcdkkgfg5h90dp51a9rakvhfg-llvm-21.1.8-dev", + "lib": "/nix/store/s6v0k4cvllyp5cy03ngxz0snj7z23ljm-llvm-21.1.8-lib", + "out": "/nix/store/344zmxispaqv91c7wlsfqyf7gvpv3wj2-llvm-21.1.8", + "python": "/nix/store/xrnqvi71nd9h4kg253s2yi7zhhxxz6xm-llvm-21.1.8-python" }, "system": "x86_64-linux", "group": "rust-toolchain", @@ -754,17 +737,16 @@ { "attr_path": "rustPlatform.rustLibSrc", "broken": false, - "derivation": "/nix/store/3gvixi5dyjqywk1i737lp4ajvpcbgz16-rust-lib-src.drv", + "derivation": "/nix/store/8sv1hmcv6ydspm39abgg1smk4fm11wmh-rust-lib-src.drv", "install_id": "rust-lib-src", - "locked_url": "https://github.com/flox/nixpkgs?rev=00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", + "locked_url": "https://github.com/flox/nixpkgs?rev=2fc6539b481e1d2569f25f8799236694180c0993", "name": "rust-lib-src", "pname": "rustLibSrc", - "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "rev_count": 940249, - "rev_date": "2026-02-04T09:32:58Z", - "scrape_date": "2026-02-06T04:38:28.843465Z", + "rev": "2fc6539b481e1d2569f25f8799236694180c0993", + "rev_count": 953160, + "rev_date": "2026-02-23T12:05:20Z", + "scrape_date": "2026-02-25T04:48:40.644162Z", "stabilities": [ - "staging", "unstable" ], "unfree": false, @@ -773,7 +755,7 @@ "out" ], "outputs": { - "out": "/nix/store/2750983ggk4djlbx3m52x0zjy4mnf58z-rust-lib-src" + "out": "/nix/store/mfl3c8ckwk1cviaa2kgavlyhyvj7siab-rust-lib-src" }, "system": "aarch64-darwin", "group": "rust-toolchain", @@ -782,17 +764,16 @@ { "attr_path": "rustPlatform.rustLibSrc", "broken": false, - "derivation": "/nix/store/yrg1gqsh9m34r85nkx9l03rlwqnimwjx-rust-lib-src.drv", + "derivation": "/nix/store/7lmxl2qm5dlryycs048n6p9qb6xydg0m-rust-lib-src.drv", "install_id": "rust-lib-src", - "locked_url": "https://github.com/flox/nixpkgs?rev=00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", + "locked_url": "https://github.com/flox/nixpkgs?rev=2fc6539b481e1d2569f25f8799236694180c0993", "name": "rust-lib-src", "pname": "rustLibSrc", - "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "rev_count": 940249, - "rev_date": "2026-02-04T09:32:58Z", - "scrape_date": "2026-02-06T05:08:48.346586Z", + "rev": "2fc6539b481e1d2569f25f8799236694180c0993", + "rev_count": 953160, + "rev_date": "2026-02-23T12:05:20Z", + "scrape_date": "2026-02-25T05:27:28.357060Z", "stabilities": [ - "staging", "unstable" ], "unfree": false, @@ -801,7 +782,7 @@ "out" ], "outputs": { - "out": "/nix/store/kq9vnb8219xqv7xa734px0a6klwdc783-rust-lib-src" + "out": "/nix/store/c4d03kawid9k3dgglbla64kl12nciwz5-rust-lib-src" }, "system": "aarch64-linux", "group": "rust-toolchain", @@ -810,17 +791,16 @@ { "attr_path": "rustPlatform.rustLibSrc", "broken": false, - "derivation": "/nix/store/idyn0k1jq4cwqaxk2h3rkmi8dk7qp3qg-rust-lib-src.drv", + "derivation": "/nix/store/s0jd0v8hb594pmcib2vbaq5i1cqsgsxs-rust-lib-src.drv", "install_id": "rust-lib-src", - "locked_url": "https://github.com/flox/nixpkgs?rev=00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", + "locked_url": "https://github.com/flox/nixpkgs?rev=2fc6539b481e1d2569f25f8799236694180c0993", "name": "rust-lib-src", "pname": "rustLibSrc", - "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "rev_count": 940249, - "rev_date": "2026-02-04T09:32:58Z", - "scrape_date": "2026-02-06T05:36:08.079151Z", + "rev": "2fc6539b481e1d2569f25f8799236694180c0993", + "rev_count": 953160, + "rev_date": "2026-02-23T12:05:20Z", + "scrape_date": "2026-02-25T06:11:47.944299Z", "stabilities": [ - "staging", "unstable" ], "unfree": false, @@ -829,7 +809,7 @@ "out" ], "outputs": { - "out": "/nix/store/yqrhjivksan0hryskrvsqks89c155hrd-rust-lib-src" + "out": "/nix/store/gqmy70a3k7qfr6xi5rhzpx2a6ql79xr1-rust-lib-src" }, "system": "x86_64-darwin", "group": "rust-toolchain", @@ -838,17 +818,16 @@ { "attr_path": "rustPlatform.rustLibSrc", "broken": false, - "derivation": "/nix/store/2f741glprwf1hn7gs99cyxw1bq6zb7vj-rust-lib-src.drv", + "derivation": "/nix/store/xz19iprk4yz00zp5x5zibwv98jafgf1b-rust-lib-src.drv", "install_id": "rust-lib-src", - "locked_url": "https://github.com/flox/nixpkgs?rev=00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", + "locked_url": "https://github.com/flox/nixpkgs?rev=2fc6539b481e1d2569f25f8799236694180c0993", "name": "rust-lib-src", "pname": "rustLibSrc", - "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "rev_count": 940249, - "rev_date": "2026-02-04T09:32:58Z", - "scrape_date": "2026-02-06T06:09:22.470277Z", + "rev": "2fc6539b481e1d2569f25f8799236694180c0993", + "rev_count": 953160, + "rev_date": "2026-02-23T12:05:20Z", + "scrape_date": "2026-02-25T07:03:55.846610Z", "stabilities": [ - "staging", "unstable" ], "unfree": false, @@ -857,7 +836,7 @@ "out" ], "outputs": { - "out": "/nix/store/jslljr5js1wnn7hqlmlbs6cx43n4az8g-rust-lib-src" + "out": "/nix/store/xqnf1bdi96i044mgfqvw9kll3554nbr9-rust-lib-src" }, "system": "x86_64-linux", "group": "rust-toolchain", @@ -866,31 +845,30 @@ { "attr_path": "rustc", "broken": false, - "derivation": "/nix/store/cn0lz3fcyjn5208k0l0wcsrks0dr1cbn-rustc-wrapper-1.92.0.drv", + "derivation": "/nix/store/ijjx2qdkxhsyhid10lbn6b6p1rxmllzc-rustc-wrapper-1.93.0.drv", "description": "Safe, concurrent, practical language (wrapper script)", "install_id": "rustc", "license": "[ MIT, Apache-2.0 ]", - "locked_url": "https://github.com/flox/nixpkgs?rev=00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "name": "rustc-wrapper-1.92.0", + "locked_url": "https://github.com/flox/nixpkgs?rev=2fc6539b481e1d2569f25f8799236694180c0993", + "name": "rustc-wrapper-1.93.0", "pname": "rustc", - "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "rev_count": 940249, - "rev_date": "2026-02-04T09:32:58Z", - "scrape_date": "2026-02-06T04:37:21.198398Z", + "rev": "2fc6539b481e1d2569f25f8799236694180c0993", + "rev_count": 953160, + "rev_date": "2026-02-23T12:05:20Z", + "scrape_date": "2026-02-25T04:47:31.369660Z", "stabilities": [ - "staging", "unstable" ], "unfree": false, - "version": "1.92.0", + "version": "1.93.0", "outputs_to_install": [ "man", "out" ], "outputs": { - "doc": "/nix/store/09yhz82jqxwbmn4dbjy7p9hrvbr4g0mn-rustc-wrapper-1.92.0-doc", - "man": "/nix/store/ch54xfkz0dlqvhbinzlbkva2898nvihl-rustc-wrapper-1.92.0-man", - "out": "/nix/store/ymskl36napcfgl6wjz1xdjn0jd25inrv-rustc-wrapper-1.92.0" + "doc": "/nix/store/cvfmwvv2cff024icpb1flb1dj65h0l7r-rustc-wrapper-1.93.0-doc", + "man": "/nix/store/181zg8zlv5xcs0b42vbs0j48n7csihwn-rustc-wrapper-1.93.0-man", + "out": "/nix/store/aw92hggrw3z08jhc7ayl3l5x5nwq89cn-rustc-wrapper-1.93.0" }, "system": "aarch64-darwin", "group": "rust-toolchain", @@ -899,31 +877,30 @@ { "attr_path": "rustc", "broken": false, - "derivation": "/nix/store/m43c677crygc100zvns1jh3kn84nxid2-rustc-wrapper-1.92.0.drv", + "derivation": "/nix/store/19zhdm4pk92xhdbs8phb2qybi1k83lg6-rustc-wrapper-1.93.0.drv", "description": "Safe, concurrent, practical language (wrapper script)", "install_id": "rustc", "license": "[ MIT, Apache-2.0 ]", - "locked_url": "https://github.com/flox/nixpkgs?rev=00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "name": "rustc-wrapper-1.92.0", + "locked_url": "https://github.com/flox/nixpkgs?rev=2fc6539b481e1d2569f25f8799236694180c0993", + "name": "rustc-wrapper-1.93.0", "pname": "rustc", - "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "rev_count": 940249, - "rev_date": "2026-02-04T09:32:58Z", - "scrape_date": "2026-02-06T05:07:19.088812Z", + "rev": "2fc6539b481e1d2569f25f8799236694180c0993", + "rev_count": 953160, + "rev_date": "2026-02-23T12:05:20Z", + "scrape_date": "2026-02-25T05:25:58.625211Z", "stabilities": [ - "staging", "unstable" ], "unfree": false, - "version": "1.92.0", + "version": "1.93.0", "outputs_to_install": [ "man", "out" ], "outputs": { - "doc": "/nix/store/camfpqmi94ssc1p8vkygp2s621ykq80m-rustc-wrapper-1.92.0-doc", - "man": "/nix/store/5ggpm5m3wkxj05si2id4b6sq4alf90qg-rustc-wrapper-1.92.0-man", - "out": "/nix/store/qnvqgfiqh8s08cqp452665l2b60a811h-rustc-wrapper-1.92.0" + "doc": "/nix/store/r7xcg85wrwc19a1czwmlj1spn4pp38dn-rustc-wrapper-1.93.0-doc", + "man": "/nix/store/2i84bq802hhxgx7wmrw3p5xhh4rqabim-rustc-wrapper-1.93.0-man", + "out": "/nix/store/98dvh18vbqmckqw0pqp8iwh1imkax381-rustc-wrapper-1.93.0" }, "system": "aarch64-linux", "group": "rust-toolchain", @@ -932,31 +909,30 @@ { "attr_path": "rustc", "broken": false, - "derivation": "/nix/store/c1vlh5i24x1p7gnp1gbm0v78kdijj4jr-rustc-wrapper-1.92.0.drv", + "derivation": "/nix/store/19aw0qq278m5rprlbnbaa5q44yrkx0hz-rustc-wrapper-1.93.0.drv", "description": "Safe, concurrent, practical language (wrapper script)", "install_id": "rustc", "license": "[ MIT, Apache-2.0 ]", - "locked_url": "https://github.com/flox/nixpkgs?rev=00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "name": "rustc-wrapper-1.92.0", + "locked_url": "https://github.com/flox/nixpkgs?rev=2fc6539b481e1d2569f25f8799236694180c0993", + "name": "rustc-wrapper-1.93.0", "pname": "rustc", - "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "rev_count": 940249, - "rev_date": "2026-02-04T09:32:58Z", - "scrape_date": "2026-02-06T05:34:59.543236Z", + "rev": "2fc6539b481e1d2569f25f8799236694180c0993", + "rev_count": 953160, + "rev_date": "2026-02-23T12:05:20Z", + "scrape_date": "2026-02-25T06:10:38.030115Z", "stabilities": [ - "staging", "unstable" ], "unfree": false, - "version": "1.92.0", + "version": "1.93.0", "outputs_to_install": [ "man", "out" ], "outputs": { - "doc": "/nix/store/7271gqq93bbxrqb9rw3hf5b5x6wdm2cn-rustc-wrapper-1.92.0-doc", - "man": "/nix/store/2k3fdy9xjwkx06rlfwfn449w442vgk0i-rustc-wrapper-1.92.0-man", - "out": "/nix/store/zkcwgmgli06nsh0v8yv82gnh5whgcdyl-rustc-wrapper-1.92.0" + "doc": "/nix/store/qnal9scf4rwq17qr0fvwqkashl6mi0g8-rustc-wrapper-1.93.0-doc", + "man": "/nix/store/nwswdhsyiv342bvfhmfas3i0gdbr4b2b-rustc-wrapper-1.93.0-man", + "out": "/nix/store/5vvf4xg0rqwkl9d4p2nx2nl1bpy2gq4w-rustc-wrapper-1.93.0" }, "system": "x86_64-darwin", "group": "rust-toolchain", @@ -965,31 +941,30 @@ { "attr_path": "rustc", "broken": false, - "derivation": "/nix/store/xh6m92k61nzgxdssfhhigkbdwybdjdji-rustc-wrapper-1.92.0.drv", + "derivation": "/nix/store/m8kplcws7gsggfixskqlbavdc7nvqd7r-rustc-wrapper-1.93.0.drv", "description": "Safe, concurrent, practical language (wrapper script)", "install_id": "rustc", "license": "[ MIT, Apache-2.0 ]", - "locked_url": "https://github.com/flox/nixpkgs?rev=00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "name": "rustc-wrapper-1.92.0", + "locked_url": "https://github.com/flox/nixpkgs?rev=2fc6539b481e1d2569f25f8799236694180c0993", + "name": "rustc-wrapper-1.93.0", "pname": "rustc", - "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "rev_count": 940249, - "rev_date": "2026-02-04T09:32:58Z", - "scrape_date": "2026-02-06T06:07:49.637637Z", + "rev": "2fc6539b481e1d2569f25f8799236694180c0993", + "rev_count": 953160, + "rev_date": "2026-02-23T12:05:20Z", + "scrape_date": "2026-02-25T07:02:20.232269Z", "stabilities": [ - "staging", "unstable" ], "unfree": false, - "version": "1.92.0", + "version": "1.93.0", "outputs_to_install": [ "man", "out" ], "outputs": { - "doc": "/nix/store/425gvb2xnhkwb1izjr97wpwdwndwi516-rustc-wrapper-1.92.0-doc", - "man": "/nix/store/n1d4093lcx7ljgks1j352fbrf5w551v9-rustc-wrapper-1.92.0-man", - "out": "/nix/store/qvpg842zrjkywv7sqgw2h05spdyzcj86-rustc-wrapper-1.92.0" + "doc": "/nix/store/jxsjbrzcxww6v7g4n4fqcw6sj7z7s3y9-rustc-wrapper-1.93.0-doc", + "man": "/nix/store/kfxswj02a5bskwnwn3jdz3micybbaw2r-rustc-wrapper-1.93.0-man", + "out": "/nix/store/rsy1282lj3dg4wdf5d00yvz7f6hjzxin-rustc-wrapper-1.93.0" }, "system": "x86_64-linux", "group": "rust-toolchain", @@ -998,28 +973,27 @@ { "attr_path": "rustfmt", "broken": false, - "derivation": "/nix/store/0g8v0vbjgh1qmmspf721vrl5m59jx4xy-rustfmt-1.92.0.drv", + "derivation": "/nix/store/q1l1h99cqgdg7ziva4pa164nl4gbyy0g-rustfmt-1.93.0.drv", "description": "Tool for formatting Rust code according to style guidelines", "install_id": "rustfmt", "license": "[ MIT, Apache-2.0 ]", - "locked_url": "https://github.com/flox/nixpkgs?rev=00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "name": "rustfmt-1.92.0", + "locked_url": "https://github.com/flox/nixpkgs?rev=2fc6539b481e1d2569f25f8799236694180c0993", + "name": "rustfmt-1.93.0", "pname": "rustfmt", - "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "rev_count": 940249, - "rev_date": "2026-02-04T09:32:58Z", - "scrape_date": "2026-02-06T04:37:21.216938Z", + "rev": "2fc6539b481e1d2569f25f8799236694180c0993", + "rev_count": 953160, + "rev_date": "2026-02-23T12:05:20Z", + "scrape_date": "2026-02-25T04:47:31.394220Z", "stabilities": [ - "staging", "unstable" ], "unfree": false, - "version": "1.92.0", + "version": "1.93.0", "outputs_to_install": [ "out" ], "outputs": { - "out": "/nix/store/max1hp93q51kfj074lw6lg8w9m5nmh10-rustfmt-1.92.0" + "out": "/nix/store/9aq18v5y6ss8yrlfzqwrsq8j0ilm7mgp-rustfmt-1.93.0" }, "system": "aarch64-darwin", "group": "rust-toolchain", @@ -1028,28 +1002,27 @@ { "attr_path": "rustfmt", "broken": false, - "derivation": "/nix/store/ynbxbsbi3byjzxd24g987vbz86bpf2ip-rustfmt-1.92.0.drv", + "derivation": "/nix/store/80227z2c6s2zhylqkzzvli0z2jf8z4qc-rustfmt-1.93.0.drv", "description": "Tool for formatting Rust code according to style guidelines", "install_id": "rustfmt", "license": "[ MIT, Apache-2.0 ]", - "locked_url": "https://github.com/flox/nixpkgs?rev=00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "name": "rustfmt-1.92.0", + "locked_url": "https://github.com/flox/nixpkgs?rev=2fc6539b481e1d2569f25f8799236694180c0993", + "name": "rustfmt-1.93.0", "pname": "rustfmt", - "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "rev_count": 940249, - "rev_date": "2026-02-04T09:32:58Z", - "scrape_date": "2026-02-06T05:07:19.116220Z", + "rev": "2fc6539b481e1d2569f25f8799236694180c0993", + "rev_count": 953160, + "rev_date": "2026-02-23T12:05:20Z", + "scrape_date": "2026-02-25T05:25:58.655472Z", "stabilities": [ - "staging", "unstable" ], "unfree": false, - "version": "1.92.0", + "version": "1.93.0", "outputs_to_install": [ "out" ], "outputs": { - "out": "/nix/store/vq6cxhd7iv2g444imys9wkmwqz3ffqbc-rustfmt-1.92.0" + "out": "/nix/store/4aq1qa0xy4z06a2bdn5l6plhj28a0rq5-rustfmt-1.93.0" }, "system": "aarch64-linux", "group": "rust-toolchain", @@ -1058,28 +1031,27 @@ { "attr_path": "rustfmt", "broken": false, - "derivation": "/nix/store/8zs4i0m5m2bp9yvpw0hihhri3g5zk45i-rustfmt-1.92.0.drv", + "derivation": "/nix/store/v7visv4rvfag2b1vrz57i53ybs4pwqdg-rustfmt-1.93.0.drv", "description": "Tool for formatting Rust code according to style guidelines", "install_id": "rustfmt", "license": "[ MIT, Apache-2.0 ]", - "locked_url": "https://github.com/flox/nixpkgs?rev=00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "name": "rustfmt-1.92.0", + "locked_url": "https://github.com/flox/nixpkgs?rev=2fc6539b481e1d2569f25f8799236694180c0993", + "name": "rustfmt-1.93.0", "pname": "rustfmt", - "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "rev_count": 940249, - "rev_date": "2026-02-04T09:32:58Z", - "scrape_date": "2026-02-06T05:34:59.562152Z", + "rev": "2fc6539b481e1d2569f25f8799236694180c0993", + "rev_count": 953160, + "rev_date": "2026-02-23T12:05:20Z", + "scrape_date": "2026-02-25T06:10:38.052016Z", "stabilities": [ - "staging", "unstable" ], "unfree": false, - "version": "1.92.0", + "version": "1.93.0", "outputs_to_install": [ "out" ], "outputs": { - "out": "/nix/store/dnjwscz1r33dkmikx3ms58qmhr60db9i-rustfmt-1.92.0" + "out": "/nix/store/1ql1g2a253djjz10x019wii04d2dijjr-rustfmt-1.93.0" }, "system": "x86_64-darwin", "group": "rust-toolchain", @@ -1088,28 +1060,27 @@ { "attr_path": "rustfmt", "broken": false, - "derivation": "/nix/store/y9j3y5l6303n7vw930ymwa86bq2na72x-rustfmt-1.92.0.drv", + "derivation": "/nix/store/fvmrxwiklb99ghvx2qq7ma2wbcnx84ns-rustfmt-1.93.0.drv", "description": "Tool for formatting Rust code according to style guidelines", "install_id": "rustfmt", "license": "[ MIT, Apache-2.0 ]", - "locked_url": "https://github.com/flox/nixpkgs?rev=00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "name": "rustfmt-1.92.0", + "locked_url": "https://github.com/flox/nixpkgs?rev=2fc6539b481e1d2569f25f8799236694180c0993", + "name": "rustfmt-1.93.0", "pname": "rustfmt", - "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", - "rev_count": 940249, - "rev_date": "2026-02-04T09:32:58Z", - "scrape_date": "2026-02-06T06:07:49.665407Z", + "rev": "2fc6539b481e1d2569f25f8799236694180c0993", + "rev_count": 953160, + "rev_date": "2026-02-23T12:05:20Z", + "scrape_date": "2026-02-25T07:02:20.264009Z", "stabilities": [ - "staging", "unstable" ], "unfree": false, - "version": "1.92.0", + "version": "1.93.0", "outputs_to_install": [ "out" ], "outputs": { - "out": "/nix/store/gl4y2v4lwiyllh85z712w8ajydia053l-rustfmt-1.92.0" + "out": "/nix/store/yb62fm7wf3ysn5rnfj2r03snmk9pwwa2-rustfmt-1.93.0" }, "system": "x86_64-linux", "group": "rust-toolchain", @@ -1135,6 +1106,8 @@ "unfree": false, "version": "2.31.39", "outputs_to_install": [ + "out", + "out", "out" ], "outputs": { @@ -1225,6 +1198,7 @@ "unfree": false, "version": "2.31.39", "outputs_to_install": [ + "out", "out", "out", "out" @@ -2325,6 +2299,8 @@ "unfree": false, "version": "1.8.1", "outputs_to_install": [ + "bin", + "bin", "bin", "man" ], @@ -2432,6 +2408,7 @@ "bin", "bin", "bin", + "bin", "man" ], "outputs": { @@ -2472,6 +2449,10 @@ "out", "out", "out", + "out", + "out", + "out", + "out", "out" ], "outputs": { @@ -2881,6 +2862,8 @@ "unfree": false, "version": "3.6.0", "outputs_to_install": [ + "bin", + "bin", "bin", "bin", "bin", @@ -2998,6 +2981,7 @@ "bin", "bin", "bin", + "bin", "man" ], "outputs": { @@ -3160,6 +3144,8 @@ "unfree": false, "version": "4.5.0", "outputs_to_install": [ + "out", + "out", "out" ], "outputs": { @@ -3250,6 +3236,7 @@ "unfree": false, "version": "4.5.0", "outputs_to_install": [ + "out", "out", "out", "out" @@ -4087,4 +4074,4 @@ "priority": 5 } ] -} \ No newline at end of file +} diff --git a/applications/equitypricemodel/Dockerfile b/applications/equitypricemodel/Dockerfile index 2a10ce7bb..e4fb44aca 100644 --- a/applications/equitypricemodel/Dockerfile +++ b/applications/equitypricemodel/Dockerfile @@ -1,6 +1,6 @@ -FROM python:3.12.10-slim AS builder +FROM python:3.12.10-slim AS builder -COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv +COPY --from=ghcr.io/astral-sh/uv:0.7.2 /uv /bin/uv WORKDIR /app @@ -10,7 +10,7 @@ COPY applications/equitypricemodel/ applications/equitypricemodel/ COPY libraries/python/ libraries/python/ -RUN uv sync --no-dev +RUN uv sync --no-dev FROM nvidia/cuda:12.4.1-devel-ubuntu22.04 AS trainer @@ -32,13 +32,15 @@ ENV CUDA=1 WORKDIR /app -COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv +COPY --from=ghcr.io/astral-sh/uv:0.7.2 /uv /bin/uv COPY --from=builder /app /app ENV PYTHONPATH=/app/applications/equitypricemodel/src +ENV TRAINING_DATA_PATH=/app/training-data/filtered_tide_training_data.parquet +ENV MODEL_OUTPUT_PATH=/app/model-artifacts -ENTRYPOINT ["uv", "run", "--package", "equitypricemodel", "python", "applications/equitypricemodel/src/equitypricemodel/trainer.py"] +ENTRYPOINT ["uv", "run", "--package", "equitypricemodel", "python", "-m", "equitypricemodel.trainer"] FROM python:3.12.10-slim AS server @@ -50,7 +52,7 @@ ENV PYTHONPATH=/app/applications/equitypricemodel/src WORKDIR /app -COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv +COPY --from=ghcr.io/astral-sh/uv:0.7.2 /uv /bin/uv COPY --from=builder /app /app diff --git a/applications/equitypricemodel/pyproject.toml b/applications/equitypricemodel/pyproject.toml index 97c2f9f89..846541f4d 100644 --- a/applications/equitypricemodel/pyproject.toml +++ b/applications/equitypricemodel/pyproject.toml @@ -18,7 +18,7 @@ dependencies = [ ] [dependency-groups] -dev = ["boto3-stubs[s3]>=1.38.0"] +dev = ["boto3-stubs[s3,ssm]>=1.38.0"] [tool.uv.sources] internal = { workspace = true } diff --git a/applications/equitypricemodel/src/equitypricemodel/server.py b/applications/equitypricemodel/src/equitypricemodel/server.py index ccfa27a85..0d6439aab 100644 --- a/applications/equitypricemodel/src/equitypricemodel/server.py +++ b/applications/equitypricemodel/src/equitypricemodel/server.py @@ -59,6 +59,7 @@ logger = structlog.get_logger() DATAMANAGER_BASE_URL = os.getenv("FUND_DATAMANAGER_BASE_URL", "http://datamanager:8080") +MODEL_VERSION_SSM_PARAMETER = "/fund/equitypricemodel/model_version" def find_latest_artifact_key( @@ -156,10 +157,49 @@ def _safe_tar_filter( temp_path.unlink(missing_ok=True) +def _resolve_artifact_key( + s3_client: "S3Client", + bucket: str, + artifact_path: str, +) -> str: + """Resolve the model artifact S3 key using SSM Parameter Store.""" + try: + ssm_client = boto3.client("ssm") + response = ssm_client.get_parameter( + Name=MODEL_VERSION_SSM_PARAMETER, + WithDecryption=True, + ) + model_version = response["Parameter"]["Value"] + except ClientError: + logger.exception("SSM parameter not available, using default artifact path") + model_version = "latest" + + if model_version != "latest": + logger.info("Using model version from SSM", model_version=model_version) + if model_version.endswith(".tar.gz"): + return model_version + return f"{artifact_path.rstrip('/')}/{model_version}/output/model.tar.gz" + + if artifact_path.endswith(".tar.gz"): + return artifact_path + + return find_latest_artifact_key( + s3_client=s3_client, + bucket=bucket, + prefix=artifact_path, + ) + + +def cleanup_model_directory(model_directory: str) -> None: + if model_directory != "." and Path(model_directory).exists(): + import shutil # noqa: PLC0415 + + shutil.rmtree(model_directory, ignore_errors=True) + + @asynccontextmanager async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: """Load model artifacts from S3 at startup.""" - import shutil # noqa: PLC0415 bucket = os.environ.get("AWS_S3_MODEL_ARTIFACTS_BUCKET_NAME") artifact_path = os.environ.get("AWS_S3_MODEL_ARTIFACT_PATH", "artifacts/") @@ -172,14 +212,11 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: extract_path = Path(model_directory) try: - if artifact_path.endswith(".tar.gz"): - artifact_key = artifact_path - else: - artifact_key = find_latest_artifact_key( - s3_client=s3_client, - bucket=bucket, - prefix=artifact_path, - ) + artifact_key = _resolve_artifact_key( + s3_client=s3_client, + bucket=bucket, + artifact_path=artifact_path, + ) download_and_extract_artifacts( s3_client=s3_client, @@ -188,12 +225,12 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: extract_path=extract_path, ) except Exception: - logger.exception("failed_to_download_artifacts") + logger.exception("Failed to download artifacts") raise - logger.info("loading_model", directory=model_directory) + logger.info("Loading model", directory=model_directory) else: - logger.info("loading_model_from_local", directory=model_directory) + logger.info("Loading model from local", directory=model_directory) app.state.model_directory = model_directory app.state.tide_model = Model.load(directory_path=model_directory) @@ -201,8 +238,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: yield - if app.state.model_directory != "." and Path(app.state.model_directory).exists(): - shutil.rmtree(app.state.model_directory, ignore_errors=True) + cleanup_model_directory(app.state.model_directory) application = FastAPI(lifespan=lifespan) diff --git a/applications/equitypricemodel/src/equitypricemodel/tide_data.py b/applications/equitypricemodel/src/equitypricemodel/tide_data.py index 1432754f7..a66ce6bda 100644 --- a/applications/equitypricemodel/src/equitypricemodel/tide_data.py +++ b/applications/equitypricemodel/src/equitypricemodel/tide_data.py @@ -1,5 +1,6 @@ import json import os +from abc import ABC, abstractmethod from datetime import date, datetime, timedelta from typing import cast @@ -12,6 +13,49 @@ logger = structlog.get_logger() +RAW_COLUMNS = ( + "ticker", + "timestamp", + "open_price", + "high_price", + "low_price", + "close_price", + "volume", + "volume_weighted_average_price", + "sector", + "industry", +) + +CONTINUOUS_COLUMNS = [ + "open_price", + "high_price", + "low_price", + "close_price", + "volume", + "volume_weighted_average_price", + "daily_return", +] + +CATEGORICAL_COLUMNS = [ + "day_of_week", + "day_of_month", + "day_of_year", + "month", + "year", + "is_holiday", +] + +STATIC_CATEGORICAL_COLUMNS = [ + "ticker", + "sector", + "industry", +] + +EncodedCategoryValue = str | bool +CategoryMapping = dict[EncodedCategoryValue, int] +FeatureMappings = dict[str, CategoryMapping] + + class Scaler: def __init__(self) -> None: pass @@ -30,57 +74,33 @@ def inverse_transform(self, data: pl.DataFrame) -> pl.DataFrame: return data * self.standard_deviations + self.means -class Data: - """Time-series dense encoder data preprocessing and postprocessing.""" +class Stage(ABC): + @property + @abstractmethod + def name(self) -> str: ... - def __init__(self) -> None: - pass + @abstractmethod + def run(self, data: pl.DataFrame) -> pl.DataFrame: ... - def preprocess_and_set_data(self, data: pl.DataFrame) -> None: - data = data.clone() - raw_columns = ( - "ticker", - "timestamp", - "open_price", - "high_price", - "low_price", - "close_price", - "volume", - "volume_weighted_average_price", - "sector", - "industry", - ) +class ValidateColumns(Stage): + @property + def name(self) -> str: + return "validate_columns" - if set(data.columns) != set(raw_columns): - message = f"Expected columns {raw_columns} but got {data.columns}" + def run(self, data: pl.DataFrame) -> pl.DataFrame: + if set(data.columns) != set(RAW_COLUMNS): + message = f"Expected columns {RAW_COLUMNS} but got {data.columns}" raise ValueError(message) + return data - self.continuous_columns = [ - "open_price", - "high_price", - "low_price", - "close_price", - "volume", - "volume_weighted_average_price", - "daily_return", - ] - - self.categorical_columns = [ - "day_of_week", - "day_of_month", - "day_of_year", - "month", - "year", - "is_holiday", - ] - self.static_categorical_columns = [ - "ticker", - "sector", - "industry", - ] +class ExpandDateRange(Stage): + @property + def name(self) -> str: + return "expand_date_range" + def run(self, data: pl.DataFrame) -> pl.DataFrame: data = data.with_columns( pl.col("timestamp") .cast(pl.Datetime(time_unit="ms")) @@ -107,11 +127,17 @@ def preprocess_and_set_data(self, data: pl.DataFrame) -> None: dates_and_tickers = tickers.join(dates, how="cross") - data = dates_and_tickers.join(data, on=["ticker", "date"], how="left") + return dates_and_tickers.join(data, on=["ticker", "date"], how="left") + +class FillNulls(Stage): + @property + def name(self) -> str: + return "fill_nulls" + + def run(self, data: pl.DataFrame) -> pl.DataFrame: friday_number = 4 - # set is_holiday value for missing weekdays data = ( data.with_columns(pl.col("date").dt.weekday().alias("temporary_weekday")) .with_columns( @@ -131,8 +157,7 @@ def preprocess_and_set_data(self, data: pl.DataFrame) -> None: .drop("temporary_weekday") ) - # ensure all rows have values instead of nulls - data = data.with_columns( + return data.with_columns( [ pl.col("open_price").fill_null(0.0), pl.col("high_price").fill_null(0.0), @@ -153,7 +178,13 @@ def preprocess_and_set_data(self, data: pl.DataFrame) -> None: ] ) - # compute new calendar columns + +class EngineerFeatures(Stage): + @property + def name(self) -> str: + return "engineer_features" + + def run(self, data: pl.DataFrame) -> pl.DataFrame: data = data.with_columns( pl.col("date").dt.weekday().alias("day_of_week").cast(pl.Int64), pl.col("date").dt.day().alias("day_of_month").cast(pl.Int64), @@ -162,7 +193,6 @@ def preprocess_and_set_data(self, data: pl.DataFrame) -> None: pl.col("date").dt.year().alias("year").cast(pl.Int64), ) - # add time index column data = data.sort(["ticker", "timestamp"]).with_columns( pl.col("timestamp") .rank("dense") @@ -171,10 +201,17 @@ def preprocess_and_set_data(self, data: pl.DataFrame) -> None: .alias("time_idx") ) - data = data.with_columns( + return data.with_columns( pl.col("close_price").pct_change().over("ticker").alias("daily_return") ) + +class CleanData(Stage): + @property + def name(self) -> str: + return "clean_data" + + def run(self, data: pl.DataFrame) -> pl.DataFrame: data = data.with_columns( [ pl.col("ticker").str.to_uppercase(), @@ -193,7 +230,7 @@ def preprocess_and_set_data(self, data: pl.DataFrame) -> None: data = data.unique(subset=["ticker", "timestamp"]) - for col in self.continuous_columns: + for col in CONTINUOUS_COLUMNS: nan_count = data.filter(pl.col(col).is_nan()).height null_count = data.filter(pl.col(col).is_null()).height inf_count = data.filter(~pl.col(col).is_finite()).height @@ -205,7 +242,6 @@ def preprocess_and_set_data(self, data: pl.DataFrame) -> None: null_count=null_count, inf_count=inf_count, ) - # Filter out rows with invalid values to prevent downstream errors data = data.filter( pl.col(col).is_not_nan() & pl.col(col).is_not_null() @@ -213,45 +249,53 @@ def preprocess_and_set_data(self, data: pl.DataFrame) -> None: ) data_validated = data_schema.validate(data) - data = cast( + return cast( "pl.DataFrame", data_validated.collect() if isinstance(data_validated, pl.LazyFrame) else data_validated, ) - self.scaler = Scaler() - self.scaler.fit(data[self.continuous_columns]) +class ScaleAndEncode(Stage): + def __init__(self) -> None: + self.scaler: Scaler | None = None + self.mappings: FeatureMappings | None = None + + @property + def name(self) -> str: + return "scale_and_encode" - for col in self.continuous_columns: + def run(self, data: pl.DataFrame) -> pl.DataFrame: + self.scaler = Scaler() + self.scaler.fit(data[CONTINUOUS_COLUMNS]) + + for col in CONTINUOUS_COLUMNS: mean_val = self.scaler.means[col].item() std_val = self.scaler.standard_deviations[col].item() if np.isnan(mean_val) or np.isnan(std_val): - logger.error( - "Scaler has NaN values", - column=col, - mean=mean_val, - std=std_val, + message = ( + f"Scaler has NaN values for column '{col}': " + f"mean={mean_val}, std={std_val}" ) + raise ValueError(message) - data = data.with_columns( # scale continuous columns + data = data.with_columns( *[ (pl.col(col) - self.scaler.means[col]) / self.scaler.standard_deviations[col] - for col in self.continuous_columns + for col in CONTINUOUS_COLUMNS ] ) - for col in self.continuous_columns: + for col in CONTINUOUS_COLUMNS: nan_count = data.filter(pl.col(col).is_nan()).height if nan_count > 0: - logger.error( - "NaN values after scaling", - column=col, - nan_count=nan_count, - total_rows=data.height, + message = ( + f"NaN values after scaling column '{col}': " + f"{nan_count}/{data.height} rows" ) + raise ValueError(message) mapping_columns = [ "ticker", @@ -260,30 +304,99 @@ def preprocess_and_set_data(self, data: pl.DataFrame) -> None: "is_holiday", ] - mappings: dict[str, dict[str, int]] = {} + mappings: FeatureMappings = {} for column in mapping_columns: - data, mapping = self._create_mapping_and_encoding(data, column) + data, mapping = _create_mapping_and_encoding(data, column) mappings[column] = mapping self.mappings = mappings - self.data = data + return data - def _create_mapping_and_encoding( - self, - data: pl.DataFrame, - column: str, - ) -> tuple[pl.DataFrame, dict]: - unique_values = data[column].unique().to_list() - mapping = {val: idx for idx, val in enumerate(unique_values)} +def _create_mapping_and_encoding( + data: pl.DataFrame, + column: str, +) -> tuple[pl.DataFrame, CategoryMapping]: + unique_values = sorted(data[column].unique().to_list()) + mapping = {val: idx for idx, val in enumerate(unique_values)} + data = data.with_columns( + pl.col(column).replace(mapping).cast(pl.Int32).alias(column) + ) + return data, mapping - data = data.with_columns( - pl.col(column).replace(mapping).cast(pl.Int32).alias(column) - ) - return data, mapping +def default_stages() -> list[Stage]: + return [ + ValidateColumns(), + ExpandDateRange(), + FillNulls(), + EngineerFeatures(), + CleanData(), + ScaleAndEncode(), + ] + + +class Pipeline: + def __init__(self, stages: list[Stage] | None = None) -> None: + self.stages = stages if stages is not None else default_stages() + + def run(self, data: pl.DataFrame) -> pl.DataFrame: + data = data.clone() + for stage in self.stages: + logger.info("Running pipeline stage", stage=stage.name) + data = stage.run(data) + return data + + def run_to(self, stage_name: str, data: pl.DataFrame) -> pl.DataFrame: + valid_names = {s.name for s in self.stages} + if stage_name not in valid_names: + message = f"Unknown stage '{stage_name}'. Valid stages: {valid_names}" + raise ValueError(message) + data = data.clone() + for stage in self.stages: + logger.info("Running pipeline stage", stage=stage.name) + data = stage.run(data) + if stage.name == stage_name: + break + return data + + def get_stage(self, stage_name: str) -> Stage: + for stage in self.stages: + if stage.name == stage_name: + return stage + message = f"Stage '{stage_name}' not found" + raise ValueError(message) + + @staticmethod + def snapshot(data: pl.DataFrame, path: str) -> None: + data.write_parquet(path) + + @staticmethod + def load_snapshot(path: str) -> pl.DataFrame: + return pl.read_parquet(path) + + +class Data: + """Time-series dense encoder data preprocessing and postprocessing.""" + + def __init__(self) -> None: + pass + + def preprocess_and_set_data(self, data: pl.DataFrame) -> None: + self.continuous_columns = list(CONTINUOUS_COLUMNS) + self.categorical_columns = list(CATEGORICAL_COLUMNS) + self.static_categorical_columns = list(STATIC_CATEGORICAL_COLUMNS) + + pipeline = Pipeline() + self.data = pipeline.run(data) + + scale_and_encode = cast( + "ScaleAndEncode", pipeline.get_stage("scale_and_encode") + ) + self.scaler = cast("Scaler", scale_and_encode.scaler) + self.mappings = cast("FeatureMappings", scale_and_encode.mappings) def _get_training_and_validation_data( self, @@ -317,10 +430,10 @@ def _get_prediction_data( def get_dimensions(self) -> dict[str, int]: return { - "encoder_categorical_features": len(self.categorical_columns), - "encoder_continuous_features": len(self.continuous_columns), - "decoder_categorical_features": len(self.categorical_columns), - "decoder_continuous_features": 0, # not using decoder_continuous_features for now # noqa: E501 + "past_categorical_features": len(self.categorical_columns), + "past_continuous_features": len(self.continuous_columns), + "future_categorical_features": len(self.categorical_columns), + "future_continuous_features": 0, # not using future_continuous_features for now # noqa: E501 "static_categorical_features": len(self.static_categorical_columns), "static_continuous_features": 0, # not using static_continuous_features for now # noqa: E501 } @@ -371,17 +484,17 @@ def get_batches( # noqa: C901 has_targets = data_type in {"train", "validate"} # Partition by ticker once upfront (much faster than filtering per ticker) - logger.info("partitioning_data_by_ticker") + logger.info("Partitioning data by ticker") ticker_groups = self.batch_data.sort("time_idx").partition_by( "ticker", as_dict=True ) total_tickers = len(ticker_groups) - logger.info("batch_creation_started", total_tickers=total_tickers) + logger.info("Batch creation started", total_tickers=total_tickers) for idx, ticker_df in enumerate(ticker_groups.values()): if idx % 25 == 0: logger.info( - "batch_progress", ticker_idx=idx, total_tickers=total_tickers + "Batch progress", ticker_idx=idx, total_tickers=total_tickers ) # Convert to numpy once per ticker (avoid repeated DataFrame operations) @@ -417,9 +530,9 @@ def get_batches( # noqa: C901 # Use numpy slicing (much faster than DataFrame slicing) for i in window_indices: sample = { - "encoder_categorical": cat_array[i : i + input_length].copy(), - "encoder_continuous": cont_array[i : i + input_length].copy(), - "decoder_categorical": cat_array[ + "past_categorical": cat_array[i : i + input_length].copy(), + "past_continuous": cont_array[i : i + input_length].copy(), + "future_categorical": cat_array[ i + input_length : i + input_length + output_length ].copy(), "static_categorical": static_array.copy(), @@ -432,23 +545,23 @@ def get_batches( # noqa: C901 samples.append(sample) - logger.info("sample_collection_complete", total_samples=len(samples)) + logger.info("Sample collection complete", total_samples=len(samples)) # now batch the samples - logger.info("batching_samples", batch_size=batch_size) + logger.info("Batching samples", batch_size=batch_size) batches = [] for i in range(0, len(samples), batch_size): batch_samples = samples[i : i + batch_size] batch = { - "encoder_categorical_features": Tensor( - np.stack([s["encoder_categorical"] for s in batch_samples]) + "past_categorical_features": Tensor( + np.stack([s["past_categorical"] for s in batch_samples]) ), - "encoder_continuous_features": Tensor( - np.stack([s["encoder_continuous"] for s in batch_samples]) + "past_continuous_features": Tensor( + np.stack([s["past_continuous"] for s in batch_samples]) ), - "decoder_categorical_features": Tensor( - np.stack([s["decoder_categorical"] for s in batch_samples]) + "future_categorical_features": Tensor( + np.stack([s["future_categorical"] for s in batch_samples]) ), "static_categorical_features": Tensor( np.stack([s["static_categorical"] for s in batch_samples]) @@ -462,7 +575,7 @@ def get_batches( # noqa: C901 batches.append(batch) - logger.info("batch_creation_complete", total_batches=len(batches)) + logger.info("Batch creation complete", total_batches=len(batches)) return batches def save(self, directory_path: str) -> None: diff --git a/applications/equitypricemodel/src/equitypricemodel/tide_model.py b/applications/equitypricemodel/src/equitypricemodel/tide_model.py index 5be29211b..a6c2676c9 100644 --- a/applications/equitypricemodel/src/equitypricemodel/tide_model.py +++ b/applications/equitypricemodel/src/equitypricemodel/tide_model.py @@ -261,6 +261,7 @@ def train( # noqa: PLR0913, PLR0912, PLR0915, C901 validation_sample_size: int = 10, early_stopping_patience: int | None = 3, early_stopping_min_delta: float = 0.001, + checkpoint_directory: str | None = None, ) -> list: """Train the TiDE model using quantile loss. @@ -289,6 +290,9 @@ def train( # noqa: PLR0913, PLR0912, PLR0915, C901 - Increase validation_sample_size for more thorough checking or decrease for faster training startup """ + if not train_batches: + return [] + if validation_sample_size <= 0: message = "validation_sample_size must be positive" raise ValueError(message) @@ -311,7 +315,16 @@ def train( # noqa: PLR0913, PLR0912, PLR0915, C901 total_batches = len(train_batches) best_loss = float("inf") + best_saved_loss = float("inf") epochs_without_improvement = 0 + checkpoint_path = None + checkpoint_saved = False + + if checkpoint_directory: + os.makedirs(checkpoint_directory, exist_ok=True) # noqa: PTH103 + checkpoint_path = os.path.join( # noqa: PTH118 + checkpoint_directory, "best_checkpoint.safetensor" + ) try: for epoch in range(epochs): @@ -324,6 +337,14 @@ def train( # noqa: PLR0913, PLR0912, PLR0915, C901 epoch_losses = [] for step, batch in enumerate(train_batches): + if batch["past_continuous_features"].shape[0] == 0: + logger.warning( + "Skipping empty batch", + step=step + 1, + epoch=epoch + 1, + ) + continue + combined_input_features, targets, batch_size = ( self._combine_input_features(batch) ) @@ -376,6 +397,16 @@ def train( # noqa: PLR0913, PLR0912, PLR0915, C901 losses.append(epoch_loss) + if checkpoint_path and epoch_loss < best_saved_loss: + best_saved_loss = epoch_loss + safe_save(get_state_dict(self), checkpoint_path) + checkpoint_saved = True + logger.info( + "Saved best checkpoint", + checkpoint_path=checkpoint_path, + loss=f"{epoch_loss:.4f}", + ) + if early_stopping_patience is not None: if epoch_loss < best_loss - early_stopping_min_delta: best_loss = epoch_loss @@ -403,32 +434,48 @@ def train( # noqa: PLR0913, PLR0912, PLR0915, C901 finally: Tensor.training = prev_training + if ( + checkpoint_saved and checkpoint_path and os.path.exists(checkpoint_path) # noqa: PTH110 + ): + logger.info( + "Restoring best checkpoint weights", + checkpoint_path=checkpoint_path, + ) + best_state = safe_load(checkpoint_path) + load_state_dict(self, best_state) + return losses def validate(self, validation_batches: list) -> float: """Validate the model using quantile loss""" + prev_training = Tensor.training Tensor.training = False - validation_losses = [] + try: + validation_losses = [] - for batch in validation_batches: - combined_input, targets, batch_size = self._combine_input_features(batch) + for batch in validation_batches: + combined_input, targets, batch_size = self._combine_input_features( + batch + ) - if targets is None: - message = "Targets are required for validation batches" - raise ValueError(message) + if targets is None: + message = "Targets are required for validation batches" + raise ValueError(message) - predictions = self.forward(combined_input) + predictions = self.forward(combined_input) - targets_reshaped = targets.reshape(batch_size, self.output_length) + targets_reshaped = targets.reshape(batch_size, self.output_length) - loss = quantile_loss(predictions, targets_reshaped, self.quantiles) - validation_losses.append(loss.numpy().item()) + loss = quantile_loss(predictions, targets_reshaped, self.quantiles) + validation_losses.append(loss.numpy().item()) - if not validation_losses: - logger.warning("No validation batches provided; returning NaN loss") - return float("nan") + if not validation_losses: + logger.warning("No validation batches provided; returning NaN loss") + return float("nan") - return sum(validation_losses) / len(validation_losses) + return sum(validation_losses) / len(validation_losses) + finally: + Tensor.training = prev_training def save( self, @@ -484,18 +531,14 @@ def _combine_input_features( self, inputs: dict[str, Tensor], ) -> tuple[Tensor, Tensor | None, int]: - batch_size = inputs["encoder_continuous_features"].shape[0] + batch_size = inputs["past_continuous_features"].shape[0] - encoder_cont_flat = inputs["encoder_continuous_features"].reshape( - batch_size, -1 - ) - encoder_cat_flat = ( - inputs["encoder_categorical_features"] - .reshape(batch_size, -1) - .cast("float32") + past_cont_flat = inputs["past_continuous_features"].reshape(batch_size, -1) + past_cat_flat = ( + inputs["past_categorical_features"].reshape(batch_size, -1).cast("float32") ) - decoder_cat_flat = ( - inputs["decoder_categorical_features"] + future_cat_flat = ( + inputs["future_categorical_features"] .reshape(batch_size, -1) .cast("float32") ) @@ -507,9 +550,9 @@ def _combine_input_features( return ( Tensor.cat( - encoder_cont_flat, - encoder_cat_flat, - decoder_cat_flat, + past_cont_flat, + past_cat_flat, + future_cat_flat, static_cat_flat, dim=1, ), diff --git a/applications/equitypricemodel/src/equitypricemodel/trainer.py b/applications/equitypricemodel/src/equitypricemodel/trainer.py index 2aaff6a8a..569fd1d6f 100644 --- a/applications/equitypricemodel/src/equitypricemodel/trainer.py +++ b/applications/equitypricemodel/src/equitypricemodel/trainer.py @@ -7,37 +7,9 @@ from equitypricemodel.tide_model import Model from tinygrad import Device -# Configure structlog for CloudWatch-friendly output -structlog.configure( - processors=[ - structlog.stdlib.add_log_level, - structlog.processors.TimeStamper(fmt="iso"), - structlog.processors.JSONRenderer(), - ], - wrapper_class=structlog.BoundLogger, - context_class=dict, - logger_factory=structlog.PrintLoggerFactory(), - cache_logger_on_first_use=True, -) - logger = structlog.get_logger() -logger.info("trainer_started", device=Device.DEFAULT) - -training_data_input_path = os.path.join( # noqa: PTH118 - "/opt/ml/input/data/train", - "filtered_tide_training_data.parquet", -) - -model_output_path = "/opt/ml/model" - -logger.info( - "paths_configured", - training_data_path=training_data_input_path, - model_output_path=model_output_path, -) - -configuration = { +DEFAULT_CONFIGURATION = { "architecture": "TiDE", "learning_rate": 0.003, "epoch_count": 20, @@ -51,113 +23,164 @@ "batch_size": 256, } -logger.info("configuration_loaded", **configuration) - -logger.info("loading_training_data") -training_data = pl.read_parquet(training_data_input_path) -logger.info( - "training_data_loaded", - rows=training_data.height, - columns=training_data.width, -) - -logger.info("initializing_data_processor") -tide_data = Data() - -logger.info("preprocessing_training_data") -tide_data.preprocess_and_set_data(data=training_data) - -logger.info("getting_data_dimensions") -dimensions = tide_data.get_dimensions() -logger.info("data_dimensions", **dimensions) - -logger.info("creating_training_batches") -train_batches = tide_data.get_batches( - data_type="train", - validation_split=float(configuration["validation_split"]), - input_length=int(configuration["input_length"]), - output_length=int(configuration["output_length"]), - batch_size=int(configuration["batch_size"]), -) - -logger.info("training_batches_created", batch_count=len(train_batches)) - -if not train_batches: - logger.error( - "No training batches created", - validation_split=configuration["validation_split"], - input_length=configuration["input_length"], - output_length=configuration["output_length"], - batch_size=configuration["batch_size"], - training_data_rows=training_data.height, + +def train_model( + training_data: pl.DataFrame, + configuration: dict | None = None, + checkpoint_directory: str | None = None, +) -> tuple[Model, Data]: + """Train TiDE model and return model + data processor.""" + merged_configuration = dict(DEFAULT_CONFIGURATION) + if configuration is not None: + merged_configuration.update(configuration) + configuration = merged_configuration + + logger.info("Configuration loaded", **configuration) + + logger.info("Initializing data processor") + tide_data = Data() + + logger.info("Preprocessing training data") + tide_data.preprocess_and_set_data(data=training_data) + + logger.info("Getting data dimensions") + dimensions = tide_data.get_dimensions() + logger.info("Data dimensions", **dimensions) + + logger.info("Creating training batches") + train_batches = tide_data.get_batches( + data_type="train", + validation_split=float(configuration["validation_split"]), + input_length=int(configuration["input_length"]), + output_length=int(configuration["output_length"]), + batch_size=int(configuration["batch_size"]), + ) + + logger.info("Training batches created", batch_count=len(train_batches)) + + if not train_batches: + logger.error( + "No training batches created", + validation_split=configuration["validation_split"], + input_length=configuration["input_length"], + output_length=configuration["output_length"], + batch_size=configuration["batch_size"], + training_data_rows=training_data.height, + ) + message = ( + "No training batches created - check input data and configuration. " + f"Training data has {training_data.height} rows, " + f"input_length={configuration['input_length']}, " + f"output_length={configuration['output_length']}, " + f"batch_size={configuration['batch_size']}" + ) + raise ValueError(message) + + sample_batch = train_batches[0] + + batch_size = sample_batch["past_continuous_features"].shape[0] + logger.info("Batch size determined", batch_size=batch_size) + + past_continuous_size = ( + sample_batch["past_continuous_features"].reshape(batch_size, -1).shape[1] + ) + past_categorical_size = ( + sample_batch["past_categorical_features"].reshape(batch_size, -1).shape[1] + ) + future_categorical_size = ( + sample_batch["future_categorical_features"].reshape(batch_size, -1).shape[1] + ) + static_categorical_size = ( + sample_batch["static_categorical_features"].reshape(batch_size, -1).shape[1] + ) + + input_size = cast( + "int", + past_continuous_size + + past_categorical_size + + future_categorical_size + + static_categorical_size, + ) + + logger.info("Input size calculated", input_size=input_size) + + logger.info("Creating model") + tide_model = Model( + input_size=input_size, + hidden_size=int(configuration["hidden_size"]), + num_encoder_layers=int(configuration["num_encoder_layers"]), + num_decoder_layers=int(configuration["num_decoder_layers"]), + output_length=int(configuration["output_length"]), + dropout_rate=float(configuration["dropout_rate"]), + quantiles=[0.1, 0.5, 0.9], + ) + + logger.info("Training started", epochs=configuration["epoch_count"]) + + losses = tide_model.train( + train_batches=train_batches, + epochs=int(configuration["epoch_count"]), + learning_rate=float(configuration["learning_rate"]), + checkpoint_directory=checkpoint_directory, ) - message = ( - "No training batches created - check input data and configuration. " - f"Training data has {training_data.height} rows, " - f"input_length={configuration['input_length']}, " - f"output_length={configuration['output_length']}, " - f"batch_size={configuration['batch_size']}" + + logger.info( + "Training complete", + final_loss=losses[-1] if losses else None, + all_losses=losses, ) - raise ValueError(message) - -sample_batch = train_batches[0] - -batch_size = sample_batch["encoder_continuous_features"].shape[0] -logger.info("batch_size_determined", batch_size=batch_size) - -# calculate each component's flattened size - days * features (e.g. 35 * 7) -encoder_continuous_size = ( - sample_batch["encoder_continuous_features"].reshape(batch_size, -1).shape[1] -) -encoder_categorical_size = ( - sample_batch["encoder_categorical_features"].reshape(batch_size, -1).shape[1] -) -decoder_categorical_size = ( - sample_batch["decoder_categorical_features"].reshape(batch_size, -1).shape[1] -) -static_categorical_size = ( - sample_batch["static_categorical_features"].reshape(batch_size, -1).shape[1] -) - -input_size = cast( - "int", - encoder_continuous_size - + encoder_categorical_size - + decoder_categorical_size - + static_categorical_size, -) - -logger.info("input_size_calculated", input_size=input_size) - -logger.info("creating_model") -tide_model = Model( - input_size=input_size, - hidden_size=int(configuration["hidden_size"]), - num_encoder_layers=int(configuration["num_encoder_layers"]), - num_decoder_layers=int(configuration["num_decoder_layers"]), - output_length=int(configuration["output_length"]), - dropout_rate=float(configuration["dropout_rate"]), - quantiles=[0.1, 0.5, 0.9], -) - -logger.info("training_started", epochs=configuration["epoch_count"]) - -losses = tide_model.train( - train_batches=train_batches, - epochs=int(configuration["epoch_count"]), - learning_rate=float(configuration["learning_rate"]), -) - -logger.info( - "training_complete", - final_loss=losses[-1] if losses else None, - all_losses=losses, -) - -logger.info("saving_model") -tide_model.save(directory_path=model_output_path) - -logger.info("saving_data_processor") -tide_data.save(directory_path=model_output_path) - -logger.info("trainer_complete") + + return tide_model, tide_data + + +if __name__ == "__main__": + # Configure structlog for CloudWatch-friendly output + structlog.configure( + processors=[ + structlog.stdlib.add_log_level, + structlog.processors.TimeStamper(fmt="iso"), + structlog.processors.JSONRenderer(), + ], + wrapper_class=structlog.BoundLogger, + context_class=dict, + logger_factory=structlog.PrintLoggerFactory(), + cache_logger_on_first_use=True, + ) + + logger.info("Trainer started", device=Device.DEFAULT) + + training_data_input_path = os.environ.get( + "TRAINING_DATA_PATH", + os.path.join( # noqa: PTH118 + "/app/training-data", + "filtered_tide_training_data.parquet", + ), + ) + + model_output_path = os.environ.get("MODEL_OUTPUT_PATH", "/app/model-artifacts") + + logger.info( + "Paths configured", + training_data_path=training_data_input_path, + model_output_path=model_output_path, + ) + + logger.info("Loading training data") + training_data = pl.read_parquet(training_data_input_path) + logger.info( + "Training data loaded", + rows=training_data.height, + columns=training_data.width, + ) + + tide_model, tide_data = train_model( + training_data, checkpoint_directory=model_output_path + ) + + logger.info("Saving model") + tide_model.save(directory_path=model_output_path) + + logger.info("Saving data processor") + tide_data.save(directory_path=model_output_path) + + logger.info("Trainer complete") diff --git a/applications/equitypricemodel/tests/conftest.py b/applications/equitypricemodel/tests/conftest.py new file mode 100644 index 000000000..f143665b2 --- /dev/null +++ b/applications/equitypricemodel/tests/conftest.py @@ -0,0 +1,51 @@ +from collections.abc import Callable +from datetime import UTC, date, datetime, timedelta + +import polars as pl +import pytest + +SATURDAY_WEEKDAY = 5 + + +@pytest.fixture +def make_raw_data() -> Callable[..., pl.DataFrame]: + def _make_raw_data( + tickers: list[str] | None = None, + days: int = 60, + start_date: date | None = None, + ) -> pl.DataFrame: + tickers = tickers or ["AAPL", "GOOG"] + start = start_date or date(2024, 1, 2) + rows = [] + for ticker in tickers: + for day_offset in range(days): + current_date = start + timedelta(days=day_offset) + if current_date.weekday() >= SATURDAY_WEEKDAY: + continue + timestamp = int( + datetime( + current_date.year, + current_date.month, + current_date.day, + tzinfo=UTC, + ).timestamp() + * 1000 + ) + close = 100.0 + day_offset * 0.5 + rows.append( + { + "ticker": ticker, + "timestamp": timestamp, + "open_price": close - 1.0, + "high_price": close + 1.0, + "low_price": close - 2.0, + "close_price": close, + "volume": 1_000_000, + "volume_weighted_average_price": close + 0.1, + "sector": "Technology", + "industry": "Software", + } + ) + return pl.DataFrame(rows) + + return _make_raw_data diff --git a/applications/equitypricemodel/tests/test_server.py b/applications/equitypricemodel/tests/test_server.py new file mode 100644 index 000000000..ea93e62b3 --- /dev/null +++ b/applications/equitypricemodel/tests/test_server.py @@ -0,0 +1,86 @@ +from unittest.mock import MagicMock, patch + +from botocore.exceptions import ClientError +from equitypricemodel.server import _resolve_artifact_key + + +def test_resolve_artifact_key_uses_latest_by_default() -> None: + mock_s3 = MagicMock() + mock_ssm = MagicMock() + mock_ssm.get_parameter.side_effect = ClientError( + error_response={ + "Error": { + "Code": "ParameterNotFound", + "Message": "Parameter not found", + } + }, + operation_name="GetParameter", + ) + + with ( + patch("equitypricemodel.server.boto3") as mock_boto3, + patch("equitypricemodel.server.find_latest_artifact_key") as mock_find, + ): + mock_boto3.client.return_value = mock_ssm + mock_find.return_value = "artifacts/model-2026/output/model.tar.gz" + result = _resolve_artifact_key( + s3_client=mock_s3, + bucket="test-bucket", + artifact_path="artifacts/", + ) + + assert result == "artifacts/model-2026/output/model.tar.gz" + mock_find.assert_called_once() + + +def test_resolve_artifact_key_uses_ssm_version() -> None: + mock_s3 = MagicMock() + mock_ssm = MagicMock() + mock_ssm.get_parameter.return_value = { + "Parameter": {"Value": "equitypricemodel-trainer-2026-01-15"} + } + + with patch("equitypricemodel.server.boto3") as mock_boto3: + mock_boto3.client.return_value = mock_ssm + result = _resolve_artifact_key( + s3_client=mock_s3, + bucket="test-bucket", + artifact_path="artifacts/", + ) + + expected = "artifacts/equitypricemodel-trainer-2026-01-15/output/model.tar.gz" + assert result == expected + + +def test_resolve_artifact_key_ssm_tar_gz_value() -> None: + mock_s3 = MagicMock() + mock_ssm = MagicMock() + mock_ssm.get_parameter.return_value = { + "Parameter": {"Value": "custom/path/model.tar.gz"} + } + + with patch("equitypricemodel.server.boto3") as mock_boto3: + mock_boto3.client.return_value = mock_ssm + result = _resolve_artifact_key( + s3_client=mock_s3, + bucket="test-bucket", + artifact_path="artifacts/", + ) + + assert result == "custom/path/model.tar.gz" + + +def test_resolve_artifact_key_explicit_tar_gz_path() -> None: + mock_s3 = MagicMock() + mock_ssm = MagicMock() + mock_ssm.get_parameter.return_value = {"Parameter": {"Value": "latest"}} + + with patch("equitypricemodel.server.boto3") as mock_boto3: + mock_boto3.client.return_value = mock_ssm + result = _resolve_artifact_key( + s3_client=mock_s3, + bucket="test-bucket", + artifact_path="artifacts/specific/model.tar.gz", + ) + + assert result == "artifacts/specific/model.tar.gz" diff --git a/applications/equitypricemodel/tests/test_tide_data.py b/applications/equitypricemodel/tests/test_tide_data.py new file mode 100644 index 000000000..442bc21e2 --- /dev/null +++ b/applications/equitypricemodel/tests/test_tide_data.py @@ -0,0 +1,326 @@ +import json +import tempfile +from collections.abc import Callable +from pathlib import Path +from typing import TYPE_CHECKING + +import polars as pl +import pytest +from equitypricemodel.tide_data import ( + CleanData, + Data, + EngineerFeatures, + ExpandDateRange, + FillNulls, + Pipeline, + ScaleAndEncode, + ValidateColumns, +) + +if TYPE_CHECKING: + from datetime import date + +FRIDAY_WEEKDAY = 4 +EXPECTED_PAST_CONTINUOUS_FEATURES = 7 +EXPECTED_PAST_CATEGORICAL_FEATURES = 6 +EXPECTED_STATIC_CATEGORICAL_FEATURES = 3 + + +def test_validate_columns_accepts_valid_data( + make_raw_data: Callable[..., pl.DataFrame], +) -> None: + data = make_raw_data() + result = ValidateColumns().run(data) + assert result.height == data.height + + +def test_validate_columns_rejects_missing_column( + make_raw_data: Callable[..., pl.DataFrame], +) -> None: + data = make_raw_data().drop("ticker") + with pytest.raises(ValueError, match="Expected columns"): + ValidateColumns().run(data) + + +def test_validate_columns_rejects_extra_column( + make_raw_data: Callable[..., pl.DataFrame], +) -> None: + data = make_raw_data().with_columns(pl.lit(1).alias("extra")) + with pytest.raises(ValueError, match="Expected columns"): + ValidateColumns().run(data) + + +def test_expand_date_range_fills_gaps( + make_raw_data: Callable[..., pl.DataFrame], +) -> None: + data = make_raw_data(tickers=["AAPL"], days=10) + expanded = ExpandDateRange().run(data) + unique_dates = expanded.select("date").unique().height + min_date: date = expanded.select(pl.col("date").min()).item() + max_date: date = expanded.select(pl.col("date").max()).item() + expected_dates = (max_date - min_date).days + 1 + assert unique_dates == expected_dates + + +def test_fill_nulls_replaces_null_prices( + make_raw_data: Callable[..., pl.DataFrame], +) -> None: + data = make_raw_data(tickers=["AAPL"], days=10) + expanded = ExpandDateRange().run(data) + filled = FillNulls().run(expanded) + null_count = filled.select(pl.col("open_price").is_null().sum()).item() + assert null_count == 0 + + +def test_fill_nulls_sets_holidays_for_missing_weekdays( + make_raw_data: Callable[..., pl.DataFrame], +) -> None: + data = make_raw_data(tickers=["AAPL"], days=10) + expanded = ExpandDateRange().run(data) + filled = FillNulls().run(expanded) + weekday_nulls = filled.filter( + (pl.col("date").dt.weekday() <= FRIDAY_WEEKDAY) & pl.col("is_holiday").is_null() + ) + assert weekday_nulls.height == 0 + + +def test_engineer_features_adds_calendar_columns( + make_raw_data: Callable[..., pl.DataFrame], +) -> None: + data = make_raw_data(tickers=["AAPL"], days=10) + expanded = ExpandDateRange().run(data) + filled = FillNulls().run(expanded) + featured = EngineerFeatures().run(filled) + expected_columns = { + "day_of_week", + "day_of_month", + "day_of_year", + "month", + "year", + "time_idx", + "daily_return", + } + assert expected_columns.issubset(set(featured.columns)) + + +def test_engineer_features_time_idx_is_dense_rank( + make_raw_data: Callable[..., pl.DataFrame], +) -> None: + data = make_raw_data(tickers=["AAPL"], days=10) + expanded = ExpandDateRange().run(data) + filled = FillNulls().run(expanded) + featured = EngineerFeatures().run(filled) + time_indices = featured.sort("timestamp").select("time_idx").to_series().to_list() + assert time_indices == sorted(time_indices) + assert time_indices[0] == 1 + + +def test_clean_data_removes_unknown_tickers( + make_raw_data: Callable[..., pl.DataFrame], +) -> None: + data = make_raw_data(tickers=["AAPL"], days=10) + expanded = ExpandDateRange().run(data) + filled = FillNulls().run(expanded) + featured = EngineerFeatures().run(filled) + # Add an UNKNOWN ticker row + unknown_row = featured.head(1).with_columns(pl.lit("UNKNOWN").alias("ticker")) + featured_with_unknown = pl.concat([featured, unknown_row]) + cleaned = CleanData().run(featured_with_unknown) + assert cleaned.filter(pl.col("ticker") == "UNKNOWN").height == 0 + + +def test_clean_data_removes_nan_daily_return( + make_raw_data: Callable[..., pl.DataFrame], +) -> None: + data = make_raw_data(tickers=["AAPL"], days=10) + expanded = ExpandDateRange().run(data) + filled = FillNulls().run(expanded) + featured = EngineerFeatures().run(filled) + cleaned = CleanData().run(featured) + nan_count = cleaned.filter(pl.col("daily_return").is_nan()).height + null_count = cleaned.filter(pl.col("daily_return").is_null()).height + assert nan_count == 0 + assert null_count == 0 + + +def test_scale_and_encode_produces_scaler_and_mappings( + make_raw_data: Callable[..., pl.DataFrame], +) -> None: + data = make_raw_data(tickers=["AAPL"], days=60) + pipeline = Pipeline() + result = pipeline.run_to("clean_data", data) + stage = ScaleAndEncode() + encoded = stage.run(result) + assert stage.scaler is not None + assert stage.mappings is not None + assert "ticker" in stage.mappings + assert "sector" in stage.mappings + assert encoded.height > 0 + + +def test_pipeline_run_produces_complete_output( + make_raw_data: Callable[..., pl.DataFrame], +) -> None: + data = make_raw_data() + pipeline = Pipeline() + result = pipeline.run(data) + assert result.height > 0 + scale_stage = pipeline.get_stage("scale_and_encode") + assert isinstance(scale_stage, ScaleAndEncode) + assert scale_stage.scaler is not None + assert scale_stage.mappings is not None + + +def test_pipeline_run_to_stops_at_stage( + make_raw_data: Callable[..., pl.DataFrame], +) -> None: + data = make_raw_data() + pipeline = Pipeline() + result = pipeline.run_to("fill_nulls", data) + assert "day_of_week" not in result.columns + + +def test_pipeline_get_stage_raises_for_unknown() -> None: + pipeline = Pipeline() + with pytest.raises(ValueError, match="not found"): + pipeline.get_stage("nonexistent") + + +def test_pipeline_snapshot_roundtrip( + tmp_path: Path, make_raw_data: Callable[..., pl.DataFrame] +) -> None: + data = make_raw_data(tickers=["AAPL"], days=10) + pipeline = Pipeline() + result = pipeline.run_to("fill_nulls", data) + snapshot_path = str(tmp_path / "snapshot.parquet") + Pipeline.snapshot(result, snapshot_path) + loaded = Pipeline.load_snapshot(snapshot_path) + assert loaded.equals(result) + + +def test_data_preprocess_sets_attributes( + make_raw_data: Callable[..., pl.DataFrame], +) -> None: + raw = make_raw_data() + data = Data() + data.preprocess_and_set_data(raw) + assert hasattr(data, "data") + assert hasattr(data, "scaler") + assert hasattr(data, "mappings") + assert data.data.height > 0 + + +def test_data_preprocess_output_columns_match_schema( + make_raw_data: Callable[..., pl.DataFrame], +) -> None: + raw = make_raw_data() + data = Data() + data.preprocess_and_set_data(raw) + expected_columns = { + "ticker", + "timestamp", + "open_price", + "high_price", + "low_price", + "close_price", + "volume", + "volume_weighted_average_price", + "sector", + "industry", + "date", + "is_holiday", + "day_of_week", + "day_of_month", + "day_of_year", + "month", + "year", + "time_idx", + "daily_return", + } + assert expected_columns == set(data.data.columns) + + +def test_data_save_and_load_roundtrip( + make_raw_data: Callable[..., pl.DataFrame], +) -> None: + raw = make_raw_data() + data = Data() + data.preprocess_and_set_data(raw) + with tempfile.TemporaryDirectory() as tmpdir: + data.save(tmpdir) + loaded = Data.load(tmpdir) + # JSON roundtrip converts bool keys to strings, so compare via JSON + assert json.dumps(loaded.mappings, sort_keys=True) == json.dumps( + data.mappings, sort_keys=True, default=str + ) + assert loaded.continuous_columns == data.continuous_columns + assert loaded.categorical_columns == data.categorical_columns + assert loaded.static_categorical_columns == data.static_categorical_columns + + +def test_data_get_dimensions( + make_raw_data: Callable[..., pl.DataFrame], +) -> None: + raw = make_raw_data() + data = Data() + data.preprocess_and_set_data(raw) + dimensions = data.get_dimensions() + assert dimensions["past_continuous_features"] == EXPECTED_PAST_CONTINUOUS_FEATURES + assert dimensions["past_categorical_features"] == EXPECTED_PAST_CATEGORICAL_FEATURES + assert ( + dimensions["static_categorical_features"] + == EXPECTED_STATIC_CATEGORICAL_FEATURES + ) + + +def test_data_get_batches_train( + make_raw_data: Callable[..., pl.DataFrame], +) -> None: + raw = make_raw_data(days=90) + data = Data() + data.preprocess_and_set_data(raw) + batches = data.get_batches( + data_type="train", + validation_split=0.8, + input_length=35, + output_length=7, + batch_size=32, + ) + assert len(batches) > 0 + batch = batches[0] + assert "past_continuous_features" in batch + assert "past_categorical_features" in batch + assert "future_categorical_features" in batch + assert "static_categorical_features" in batch + assert "targets" in batch + + +def test_scale_and_encode_raises_on_nan_scaler( + make_raw_data: Callable[..., pl.DataFrame], +) -> None: + data = make_raw_data(tickers=["AAPL"], days=60) + pipeline = Pipeline() + cleaned = pipeline.run_to("clean_data", data) + stage = ScaleAndEncode() + # Inject NaN into continuous column to trigger NaN in scaler means + nan_data = cleaned.with_columns(pl.lit(float("nan")).alias("close_price")) + with pytest.raises(ValueError, match="Scaler has NaN values"): + stage.run(nan_data) + + +def test_pipeline_run_to_raises_for_unknown_stage( + make_raw_data: Callable[..., pl.DataFrame], +) -> None: + pipeline = Pipeline() + data = make_raw_data() + with pytest.raises(ValueError, match="Unknown stage"): + pipeline.run_to("nonexistent_stage", data) + + +def test_pipeline_honors_explicit_empty_stage_list( + make_raw_data: Callable[..., pl.DataFrame], +) -> None: + data = make_raw_data() + pipeline = Pipeline(stages=[]) + result = pipeline.run(data) + assert result.equals(data) diff --git a/applications/equitypricemodel/tests/test_tide_model.py b/applications/equitypricemodel/tests/test_tide_model.py new file mode 100644 index 000000000..54857002b --- /dev/null +++ b/applications/equitypricemodel/tests/test_tide_model.py @@ -0,0 +1,304 @@ +import tempfile +from dataclasses import dataclass + +import numpy as np +import pytest +from equitypricemodel.tide_model import Model, quantile_loss +from tinygrad.tensor import Tensor + +BATCH_SIZE = 4 +INPUT_LENGTH = 35 +OUTPUT_LENGTH = 7 +CONTINUOUS_FEATURES = 7 +CATEGORICAL_FEATURES = 6 +STATIC_FEATURES = 3 +HIDDEN_SIZE = 32 +NUM_QUANTILES = 3 +CATEGORICAL_UPPER_BOUND = 10 +EPOCHS_SHORT = 2 +EPOCHS_LONG = 20 +LEARNING_RATE = 0.001 +EARLY_STOPPING_PATIENCE = 2 +EPOCH_SINGLE = 1 + +rng = np.random.default_rng(42) + + +@dataclass +class BatchConfig: + batch_size: int = BATCH_SIZE + input_length: int = INPUT_LENGTH + output_length: int = OUTPUT_LENGTH + continuous_features: int = CONTINUOUS_FEATURES + categorical_features: int = CATEGORICAL_FEATURES + static_features: int = STATIC_FEATURES + + +def _make_batch( + config: BatchConfig | None = None, + *, + include_targets: bool = True, +) -> dict[str, Tensor]: + if config is None: + config = BatchConfig() + batch: dict[str, Tensor] = { + "past_continuous_features": Tensor( + rng.standard_normal( + (config.batch_size, config.input_length, config.continuous_features) + ).astype(np.float32) + ), + "past_categorical_features": Tensor( + rng.integers( + 0, + CATEGORICAL_UPPER_BOUND, + (config.batch_size, config.input_length, config.categorical_features), + ).astype(np.int32) + ), + "future_categorical_features": Tensor( + rng.integers( + 0, + CATEGORICAL_UPPER_BOUND, + (config.batch_size, config.output_length, config.categorical_features), + ).astype(np.int32) + ), + "static_categorical_features": Tensor( + rng.integers( + 0, + CATEGORICAL_UPPER_BOUND, + (config.batch_size, 1, config.static_features), + ).astype(np.int32) + ), + } + if include_targets: + batch["targets"] = Tensor( + rng.standard_normal((config.batch_size, config.output_length, 1)).astype( + np.float32 + ) + ) + return batch + + +def _compute_input_size( + input_length: int = INPUT_LENGTH, + output_length: int = OUTPUT_LENGTH, + continuous_features: int = CONTINUOUS_FEATURES, + categorical_features: int = CATEGORICAL_FEATURES, + static_features: int = STATIC_FEATURES, +) -> int: + return ( + input_length * continuous_features + + input_length * categorical_features + + output_length * categorical_features + + 1 * static_features + ) + + +def test_quantile_loss_valid_inputs() -> None: + predictions = Tensor( + rng.standard_normal((BATCH_SIZE, OUTPUT_LENGTH, NUM_QUANTILES)).astype( + np.float32 + ) + ) + targets = Tensor( + rng.standard_normal((BATCH_SIZE, OUTPUT_LENGTH)).astype(np.float32) + ) + loss = quantile_loss(predictions, targets) + assert loss.numpy().item() >= 0 + + +def test_quantile_loss_rejects_invalid_quantiles() -> None: + predictions = Tensor( + rng.standard_normal((BATCH_SIZE, OUTPUT_LENGTH, NUM_QUANTILES)).astype( + np.float32 + ) + ) + targets = Tensor( + rng.standard_normal((BATCH_SIZE, OUTPUT_LENGTH)).astype(np.float32) + ) + with pytest.raises(ValueError, match="between 0 and 1"): + quantile_loss(predictions, targets, quantiles=[0.1, 1.5, 0.9]) + + +def test_model_forward_output_shape() -> None: + input_size = _compute_input_size() + model = Model( + input_size=input_size, hidden_size=HIDDEN_SIZE, output_length=OUTPUT_LENGTH + ) + batch = _make_batch() + combined, _, _ = model._combine_input_features(batch) # noqa: SLF001 + output = model.forward(combined) + assert output.shape == (BATCH_SIZE, OUTPUT_LENGTH, NUM_QUANTILES) + + +def test_model_train_returns_losses() -> None: + input_size = _compute_input_size() + model = Model( + input_size=input_size, hidden_size=HIDDEN_SIZE, output_length=OUTPUT_LENGTH + ) + batches = [_make_batch()] + losses = model.train( + train_batches=batches, + epochs=EPOCHS_SHORT, + learning_rate=LEARNING_RATE, + validate_data=False, + ) + assert len(losses) == EPOCHS_SHORT + assert all(isinstance(loss, float) for loss in losses) + + +def test_model_train_empty_batch_list() -> None: + input_size = _compute_input_size() + model = Model( + input_size=input_size, hidden_size=HIDDEN_SIZE, output_length=OUTPUT_LENGTH + ) + losses = model.train( + train_batches=[], + epochs=EPOCHS_SHORT, + learning_rate=LEARNING_RATE, + validate_data=False, + ) + assert losses == [] + + +def test_model_train_skips_zero_size_batch() -> None: + input_size = _compute_input_size() + model = Model( + input_size=input_size, hidden_size=HIDDEN_SIZE, output_length=OUTPUT_LENGTH + ) + empty_batch = { + "past_continuous_features": Tensor( + np.zeros((0, INPUT_LENGTH, CONTINUOUS_FEATURES), dtype=np.float32) + ), + "past_categorical_features": Tensor( + np.zeros((0, INPUT_LENGTH, CATEGORICAL_FEATURES), dtype=np.int32) + ), + "future_categorical_features": Tensor( + np.zeros((0, OUTPUT_LENGTH, CATEGORICAL_FEATURES), dtype=np.int32) + ), + "static_categorical_features": Tensor( + np.zeros((0, 1, STATIC_FEATURES), dtype=np.int32) + ), + "targets": Tensor(np.zeros((0, OUTPUT_LENGTH, 1), dtype=np.float32)), + } + normal_batch = _make_batch(BatchConfig(batch_size=BATCH_SIZE)) + losses = model.train( + train_batches=[empty_batch, normal_batch], + epochs=EPOCH_SINGLE, + learning_rate=LEARNING_RATE, + validate_data=False, + ) + assert len(losses) == EPOCH_SINGLE + + +def test_model_train_missing_targets_raises() -> None: + input_size = _compute_input_size() + model = Model( + input_size=input_size, hidden_size=HIDDEN_SIZE, output_length=OUTPUT_LENGTH + ) + batch = _make_batch(include_targets=False) + with pytest.raises(ValueError, match="Targets are required"): + model.train( + train_batches=[batch], + epochs=EPOCH_SINGLE, + learning_rate=LEARNING_RATE, + validate_data=False, + ) + + +def test_model_validate_returns_loss() -> None: + input_size = _compute_input_size() + model = Model( + input_size=input_size, hidden_size=HIDDEN_SIZE, output_length=OUTPUT_LENGTH + ) + batches = [_make_batch()] + loss = model.validate(batches) + assert isinstance(loss, float) + assert loss >= 0 + + +def test_model_validate_empty_batches_returns_nan() -> None: + input_size = _compute_input_size() + model = Model( + input_size=input_size, hidden_size=HIDDEN_SIZE, output_length=OUTPUT_LENGTH + ) + loss = model.validate([]) + assert np.isnan(loss) + + +def test_model_validate_missing_targets_raises() -> None: + input_size = _compute_input_size() + model = Model( + input_size=input_size, hidden_size=HIDDEN_SIZE, output_length=OUTPUT_LENGTH + ) + batch = _make_batch(include_targets=False) + with pytest.raises(ValueError, match="Targets are required"): + model.validate([batch]) + + +def test_model_predict_output_shape() -> None: + input_size = _compute_input_size() + model = Model( + input_size=input_size, hidden_size=HIDDEN_SIZE, output_length=OUTPUT_LENGTH + ) + batch = _make_batch(include_targets=False) + predictions = model.predict(batch) + assert predictions.shape == (BATCH_SIZE, OUTPUT_LENGTH, NUM_QUANTILES) + + +def test_model_save_and_load_roundtrip() -> None: + input_size = _compute_input_size() + model = Model( + input_size=input_size, hidden_size=HIDDEN_SIZE, output_length=OUTPUT_LENGTH + ) + with tempfile.TemporaryDirectory() as tmpdir: + model.save(tmpdir) + loaded = Model.load(tmpdir) + assert loaded.input_size == model.input_size + assert loaded.hidden_size == model.hidden_size + assert loaded.output_length == model.output_length + assert loaded.quantiles == model.quantiles + + +def test_model_early_stopping() -> None: + input_size = _compute_input_size() + model = Model( + input_size=input_size, hidden_size=HIDDEN_SIZE, output_length=OUTPUT_LENGTH + ) + batches = [_make_batch()] + losses = model.train( + train_batches=batches, + epochs=EPOCHS_LONG, + learning_rate=LEARNING_RATE, + validate_data=False, + early_stopping_patience=EARLY_STOPPING_PATIENCE, + ) + assert len(losses) <= EPOCHS_LONG + + +def test_model_validation_sample_size_must_be_positive() -> None: + input_size = _compute_input_size() + model = Model( + input_size=input_size, hidden_size=HIDDEN_SIZE, output_length=OUTPUT_LENGTH + ) + with pytest.raises(ValueError, match="positive"): + model.train( + train_batches=[_make_batch()], + epochs=EPOCH_SINGLE, + validate_data=True, + validation_sample_size=0, + ) + + +def test_model_validate_restores_training_state() -> None: + input_size = _compute_input_size() + model = Model( + input_size=input_size, hidden_size=HIDDEN_SIZE, output_length=OUTPUT_LENGTH + ) + batches = [_make_batch()] + Tensor.training = True + model.validate(batches) + assert Tensor.training is True + + Tensor.training = False + model.validate(batches) + assert Tensor.training is False diff --git a/applications/equitypricemodel/tests/test_trainer.py b/applications/equitypricemodel/tests/test_trainer.py new file mode 100644 index 000000000..490d88493 --- /dev/null +++ b/applications/equitypricemodel/tests/test_trainer.py @@ -0,0 +1,62 @@ +from collections.abc import Callable + +import polars as pl +import pytest +from equitypricemodel.trainer import DEFAULT_CONFIGURATION, train_model + +PARTIAL_HIDDEN_SIZE = 16 + + +def test_train_model_returns_model_and_data( + make_raw_data: Callable[..., pl.DataFrame], +) -> None: + training_data = make_raw_data(days=90) + model, data = train_model(training_data) + assert model is not None + assert data is not None + assert hasattr(data, "scaler") + assert hasattr(data, "mappings") + + +def test_train_model_uses_custom_configuration( + make_raw_data: Callable[..., pl.DataFrame], +) -> None: + training_data = make_raw_data(days=90) + custom_config = dict(DEFAULT_CONFIGURATION) + custom_hidden_size = 32 + custom_config["epoch_count"] = 1 + custom_config["hidden_size"] = custom_hidden_size + model, _data = train_model(training_data, configuration=custom_config) + assert model.hidden_size == custom_hidden_size + + +def test_train_model_raises_on_insufficient_data( + make_raw_data: Callable[..., pl.DataFrame], +) -> None: + short_data = make_raw_data(tickers=["AAPL"], days=5) + with pytest.raises(ValueError, match="Total days available"): + train_model(short_data) + + +def test_train_model_uses_default_configuration( + make_raw_data: Callable[..., pl.DataFrame], +) -> None: + training_data = make_raw_data(days=90) + model, _ = train_model(training_data) + assert model.hidden_size == DEFAULT_CONFIGURATION["hidden_size"] + assert model.output_length == DEFAULT_CONFIGURATION["output_length"] + + +def test_train_model_merges_partial_configuration( + make_raw_data: Callable[..., pl.DataFrame], +) -> None: + training_data = make_raw_data(days=90) + model, _ = train_model( + training_data, + configuration={ + "epoch_count": 1, + "hidden_size": PARTIAL_HIDDEN_SIZE, + }, + ) + assert model.hidden_size == PARTIAL_HIDDEN_SIZE + assert model.output_length == DEFAULT_CONFIGURATION["output_length"] diff --git a/applications/portfoliomanager/src/portfoliomanager/alpaca_client.py b/applications/portfoliomanager/src/portfoliomanager/alpaca_client.py index 05651a72a..9be01eaff 100644 --- a/applications/portfoliomanager/src/portfoliomanager/alpaca_client.py +++ b/applications/portfoliomanager/src/portfoliomanager/alpaca_client.py @@ -134,7 +134,7 @@ def close_position( http_not_found = 404 position_not_found = ( status_code == http_not_found - or error_code in {"position_not_found"} + or error_code == "position_not_found" or "position not found" in error_str or "position does not exist" in error_str ) diff --git a/applications/portfoliomanager/src/portfoliomanager/portfolio_schema.py b/applications/portfoliomanager/src/portfoliomanager/portfolio_schema.py index 3a8d254c4..9b686f7e3 100644 --- a/applications/portfoliomanager/src/portfoliomanager/portfolio_schema.py +++ b/applications/portfoliomanager/src/portfoliomanager/portfolio_schema.py @@ -130,11 +130,11 @@ def check_position_side_sums( coerce=True, checks=[ pa.Check( - check_fn=lambda df: check_position_side_counts(df), + check_fn=check_position_side_counts, error="Each side must have expected position counts", ), pa.Check( - check_fn=lambda df: check_position_side_sums(df), + check_fn=check_position_side_sums, error="Position side sums must be approximately equal", ), ], diff --git a/infrastructure/Pulumi.production.yaml b/infrastructure/Pulumi.production.yaml index 66e5ccb8c..d28f9eaec 100644 --- a/infrastructure/Pulumi.production.yaml +++ b/infrastructure/Pulumi.production.yaml @@ -25,3 +25,12 @@ config: secure: AAABACKTBgsKXMGiDo/WXf5/WTwxHIKAKYUGOMhCecEe09+g/huViXxO1fYA+I2EdIcxBk8zerAoxkOGNUMkik+45skuj3vUYHraOLiKmzSt9h7Z1R56ixPoNMrTSbCMjpHOHZSji0G7lH2qCdCj6jGH/aouZjZRsnPLGa5/pxjhe+1aUtuvwLoqr6IlyuPEkw== fund:randomSuffix: secure: AAABAPl47ORoO6t8NqEn8I/e49nYu7cuGBwRgA59mBFIKX7RyHTk1w== + fund:prefectAllowedCidrs: + - secure: AAABALiKgrczJ7GJHyFYtUDbI+zOIUqeg4NYuNNDXbI6iP5g2CGQ1bktFy2ZzjBVZrRLPW7HOZEccMTMYNDyIlQZ7xEKMVWhOFoL + - secure: AAABAHTPeLTWD9IZhYCwF3s601vNqbvIiYPx6dStB1zMhFZvDq4hp0mdrExk0CnG9Q== + - secure: AAABAA0owr9bPUoKIJvvgmmMfFL/QHE/dq2G5UVG5nKvoC6N/Q9QM//FsmYhOVSe + - secure: AAABAGabwlpPzBNk/V4FHC1FBihsyHgpgDOnJMQ9bXR5dB+qxvQsPxRy9iIit9NfbTwpEpLKGPU+xnpvG5JEpXaVE5ePau+gM46H + fund:trainingNotificationSenderEmail: + secure: AAABAAUimDipynPxKiwZJM1KNHM5uSBMxNfZ6lu5CiCrT14CPI/gdt2ezaqz102km/4XgGq0MRjuDPc2 + fund:trainingNotificationRecipientEmails: + secure: AAABAM/wajUzYpwBJ3J/hqxAiFRYXg6uM46l/hbYAKDs9cNwOMg3DMZg22agKg/r6MenE8lUtuxOuZr1in3QnCD9s3ypncn+cS9LxcyTAARXcDMKV2g= diff --git a/infrastructure/__main__.py b/infrastructure/__main__.py index 434969590..4457b9cbe 100644 --- a/infrastructure/__main__.py +++ b/infrastructure/__main__.py @@ -82,7 +82,26 @@ def serialize_secret_config_object( raise ValueError(message) monthly_budget_limit_usd = stack_config.require_float("monthlyBudgetLimitUsd") -sagemaker_execution_role_name = stack_config.require("sagemakerExecutionRoleName") + +prefect_allowed_cidrs = cast( + "list[str]", + stack_config.require_object("prefectAllowedCidrs"), +) +if not prefect_allowed_cidrs: + message = ( + "Pulumi config 'prefectAllowedCidrs' must include at least one CIDR block." + ) + raise ValueError(message) + +prefect_allowed_ipv4_cidrs = [c for c in prefect_allowed_cidrs if ":" not in c] +prefect_allowed_ipv6_cidrs = [c for c in prefect_allowed_cidrs if ":" in c] + +training_notification_sender_email = stack_config.require_secret( + "trainingNotificationSenderEmail" +) +training_notification_recipient_emails = stack_config.require_secret( + "trainingNotificationRecipientEmails" +) datamanager_secret_name = stack_config.require_secret("datamanagerSecretName") portfoliomanager_secret_name = stack_config.require_secret("portfoliomanagerSecretName") @@ -362,6 +381,28 @@ def serialize_secret_config_object( tags=tags, ) +prefect_server_repository = aws.ecr.Repository( + "prefect_server_repository", + name="fund/prefect-server", + image_tag_mutability="MUTABLE", + force_delete=True, + image_scanning_configuration=aws.ecr.RepositoryImageScanningConfigurationArgs( + scan_on_push=True, + ), + tags=tags, +) + +prefect_worker_repository = aws.ecr.Repository( + "prefect_worker_repository", + name="fund/prefect-worker", + image_tag_mutability="MUTABLE", + force_delete=True, + image_scanning_configuration=aws.ecr.RepositoryImageScanningConfigurationArgs( + scan_on_push=True, + ), + tags=tags, +) + # Generate image URIs - these will be used in task definitions # For initial deployment, use a placeholder that will be updated when images are pushed datamanager_image_uri = datamanager_repository.repository_url.apply( @@ -378,6 +419,12 @@ def serialize_secret_config_object( lambda url: f"{url}:latest" ) ) +prefect_server_image_uri = prefect_server_repository.repository_url.apply( + lambda url: f"{url}:latest" +) +prefect_worker_image_uri = prefect_worker_repository.repository_url.apply( + lambda url: f"{url}:latest" +) vpc = aws.ec2.Vpc( "vpc", @@ -535,6 +582,32 @@ def serialize_secret_config_object( cidr_blocks=["0.0.0.0/0"], description="Allow HTTPS", ), + *( + [ + aws.ec2.SecurityGroupIngressArgs( + protocol="tcp", + from_port=4200, + to_port=4200, + cidr_blocks=prefect_allowed_ipv4_cidrs, + description="Allow Prefect dashboard from team IPv4", + ), + ] + if prefect_allowed_ipv4_cidrs + else [] + ), + *( + [ + aws.ec2.SecurityGroupIngressArgs( + protocol="tcp", + from_port=4200, + to_port=4200, + ipv6_cidr_blocks=prefect_allowed_ipv6_cidrs, + description="Allow Prefect dashboard from team IPv6", + ), + ] + if prefect_allowed_ipv6_cidrs + else [] + ), ], egress=[ aws.ec2.SecurityGroupEgressArgs( @@ -723,7 +796,57 @@ def serialize_secret_config_object( tags=tags, ) -acm_certificate_arn = None # temporary disable HTTPS +prefect_tg = aws.lb.TargetGroup( + "prefect_tg", + name="fund-prefect", + port=4200, + protocol="HTTP", + vpc_id=vpc.id, + target_type="ip", + health_check=aws.lb.TargetGroupHealthCheckArgs( + path="/api/health", + healthy_threshold=2, + unhealthy_threshold=3, + timeout=5, + interval=30, + ), + tags=tags, +) + +# Set acm_certificate_arn to enable HTTPS for the Prefect dashboard listener. +acm_certificate_arn = None + +# Prefect dashboard listener on port 4200 (restricted by ALB security group) +if acm_certificate_arn: + prefect_listener = aws.lb.Listener( + "prefect_listener", + load_balancer_arn=alb.arn, + port=4200, + protocol="HTTPS", + ssl_policy="ELBSecurityPolicy-TLS13-1-2-2021-06", + certificate_arn=acm_certificate_arn, + default_actions=[ + aws.lb.ListenerDefaultActionArgs( + type="forward", + target_group_arn=prefect_tg.arn, + ) + ], + tags=tags, + ) +else: + prefect_listener = aws.lb.Listener( + "prefect_listener", + load_balancer_arn=alb.arn, + port=4200, + protocol="HTTP", + default_actions=[ + aws.lb.ListenerDefaultActionArgs( + type="forward", + target_group_arn=prefect_tg.arn, + ) + ], + tags=tags, + ) if acm_certificate_arn: # HTTPS Listener (port 443) @@ -1025,6 +1148,21 @@ def serialize_secret_config_object( f"arn:aws:sns:{region}:{account_id}:fund-infrastructure-alerts:*", ], }, + { + "Sid": "ManageSESIdentities", + "Effect": "Allow", + "Action": [ + "ses:CreateEmailIdentity", + "ses:DeleteEmailIdentity", + "ses:GetEmailIdentity", + "ses:TagResource", + "ses:UntagResource", + "ses:ListTagsForResource", + ], + "Resource": [ + f"arn:aws:ses:{region}:{account_id}:identity/*", + ], + }, { "Sid": "CreateGithubActionsOIDCProvider", "Effect": "Allow", @@ -1043,7 +1181,6 @@ def serialize_secret_config_object( "fund-ecs-execution-role", "fund-ecs-task-role", github_actions_role_name, - sagemaker_execution_role_name, ] } }, @@ -1091,7 +1228,6 @@ def serialize_secret_config_object( f"arn:aws:iam::{account_id}:role/fund-ecs-execution-role", f"arn:aws:iam::{account_id}:role/fund-ecs-task-role", f"arn:aws:iam::{account_id}:role/{github_actions_role_name}", - f"arn:aws:iam::{account_id}:role/{sagemaker_execution_role_name}", ], "Condition": { "ArnLikeIfExists": { @@ -1104,7 +1240,6 @@ def serialize_secret_config_object( "iam:PassedToService": [ "ecs-tasks.amazonaws.com", "ecs.amazonaws.com", - "sagemaker.amazonaws.com", ] }, }, @@ -1126,6 +1261,7 @@ def serialize_secret_config_object( "fund-ecs-execution-role-secrets-policy", "fund-ecs-task-role-s3-policy", "fund-ecs-task-role-ssm-policy", + "fund-ecs-task-role-ses-policy", ] } }, @@ -1337,91 +1473,430 @@ def serialize_secret_config_object( ), ) -# SageMaker Execution Policy -sagemaker_execution_policy = aws.iam.Policy( - "sagemaker_execution_policy", - name="fund-sagemaker-execution-policy", - description="Least-privilege policy for SageMaker execution role.", - policy=pulumi.Output.all( - data_bucket.arn, - model_artifacts_bucket.arn, - equitypricemodel_trainer_repository.arn, - ).apply( - lambda args: json.dumps( +# SES Email Identity for training notifications +training_notification_email_identity = aws.ses.EmailIdentity( + "training_notification_email_identity", + email=training_notification_sender_email, +) + +training_notification_sender_email_parameter = aws.ssm.Parameter( + "training_notification_sender_email_parameter", + name="/fund/prefect/training_notification_sender_email", + type="SecureString", + value=training_notification_sender_email, + tags=tags, +) + +training_notification_recipients_parameter = aws.ssm.Parameter( + "training_notification_recipients_parameter", + name="/fund/prefect/training_notification_recipients", + type="SecureString", + value=training_notification_recipient_emails, + tags=tags, +) + +# Allow ECS tasks to send emails via SES +aws.iam.RolePolicy( + "task_role_ses_policy", + name="fund-ecs-task-role-ses-policy", + role=task_role.id, + policy=training_notification_email_identity.arn.apply( + lambda identity_arn: json.dumps( { "Version": "2012-10-17", "Statement": [ { - "Sid": "S3Access", - "Effect": "Allow", - "Action": [ - "s3:GetObject", - "s3:PutObject", - "s3:DeleteObject", - "s3:ListBucket", - ], - "Resource": [ - args[0], - f"{args[0]}/*", - args[1], - f"{args[1]}/*", - ], - }, - { - "Sid": "ECRRepositoryAccess", - "Effect": "Allow", - "Action": [ - "ecr:GetDownloadUrlForLayer", - "ecr:BatchGetImage", - "ecr:BatchCheckLayerAvailability", - ], - "Resource": args[2], - }, - { - "Sid": "ECRAuthorizationToken", "Effect": "Allow", - "Action": "ecr:GetAuthorizationToken", - "Resource": "*", - }, + "Action": ["ses:SendEmail", "ses:SendRawEmail"], + "Resource": identity_arn, + } + ], + }, + sort_keys=True, + ) + ), +) + +# Prefect Infrastructure + +# RDS Security Group - allows inbound Postgres from ECS tasks +prefect_rds_security_group = aws.ec2.SecurityGroup( + "prefect_rds_sg", + name="fund-prefect-rds", + vpc_id=vpc.id, + description="Security group for Prefect RDS database", + tags=tags, +) + +aws.ec2.SecurityGroupRule( + "prefect_rds_ingress", + type="ingress", + security_group_id=prefect_rds_security_group.id, + source_security_group_id=ecs_security_group.id, + protocol="tcp", + from_port=5432, + to_port=5432, + description="Allow Postgres from ECS tasks", +) + +aws.ec2.SecurityGroupRule( + "prefect_rds_egress", + type="egress", + security_group_id=prefect_rds_security_group.id, + protocol="-1", + from_port=0, + to_port=0, + cidr_blocks=["0.0.0.0/0"], + description="Allow all outbound", +) + +# Redis Security Group - allows inbound Redis from ECS tasks +prefect_redis_security_group = aws.ec2.SecurityGroup( + "prefect_redis_sg", + name="fund-prefect-redis", + vpc_id=vpc.id, + description="Security group for Prefect Redis cache", + tags=tags, +) + +aws.ec2.SecurityGroupRule( + "prefect_redis_ingress", + type="ingress", + security_group_id=prefect_redis_security_group.id, + source_security_group_id=ecs_security_group.id, + protocol="tcp", + from_port=6379, + to_port=6379, + description="Allow Redis from ECS tasks", +) + +aws.ec2.SecurityGroupRule( + "prefect_redis_egress", + type="egress", + security_group_id=prefect_redis_security_group.id, + protocol="-1", + from_port=0, + to_port=0, + cidr_blocks=["0.0.0.0/0"], + description="Allow all outbound", +) + +# RDS Subnet Group +prefect_rds_subnet_group = aws.rds.SubnetGroup( + "prefect_rds_subnet_group", + name="fund-prefect-rds", + subnet_ids=[private_subnet_1.id, private_subnet_2.id], + tags=tags, +) + +# RDS PostgreSQL for Prefect database +prefect_database = aws.rds.Instance( + "prefect_database", + identifier="fund-prefect", + engine="postgres", + engine_version="14", + instance_class="db.t3.micro", + allocated_storage=20, + db_name="prefect", + username="prefect", + manage_master_user_password=True, + db_subnet_group_name=prefect_rds_subnet_group.name, + vpc_security_group_ids=[prefect_rds_security_group.id], + skip_final_snapshot=False, + final_snapshot_identifier=f"fund-prefect-final-{pulumi.get_stack()}", + backup_retention_period=7, + storage_encrypted=True, + deletion_protection=True, + tags=tags, +) + +# Grant ECS execution role access to the RDS-managed master password secret +aws.iam.RolePolicy( + "execution_role_prefect_db_secret_policy", + name="fund-ecs-execution-role-prefect-db-secret", + role=execution_role.id, + policy=prefect_database.master_user_secrets[0]["secret_arn"].apply( + lambda arn: json.dumps( + { + "Version": "2012-10-17", + "Statement": [ { - "Sid": "CloudWatchLogs", "Effect": "Allow", - "Action": [ - "logs:CreateLogGroup", - "logs:CreateLogStream", - "logs:PutLogEvents", - "logs:DescribeLogStreams", - ], - "Resource": "arn:aws:logs:*:*:log-group:/aws/sagemaker/*", - }, + "Action": ["secretsmanager:GetSecretValue"], + "Resource": arn, + } ], }, sort_keys=True, ) ), - opts=pulumi.ResourceOptions(retain_on_delete=True), +) + +# ElastiCache Subnet Group +prefect_elasticache_subnet_group = aws.elasticache.SubnetGroup( + "prefect_elasticache_subnet_group", + name="fund-prefect-redis", + subnet_ids=[private_subnet_1.id, private_subnet_2.id], tags=tags, ) -# SageMaker Execution Role for training jobs -sagemaker_execution_role = aws.iam.Role( - "sagemaker_execution_role", - name=sagemaker_execution_role_name, - assume_role_policy=json.dumps( - { - "Version": "2012-10-17", - "Statement": [ +# ElastiCache Redis for Prefect messaging +prefect_redis = aws.elasticache.Cluster( + "prefect_redis", + cluster_id="fund-prefect-redis", + engine="redis", + engine_version="7.0", + node_type="cache.t3.micro", + num_cache_nodes=1, + subnet_group_name=prefect_elasticache_subnet_group.name, + security_group_ids=[prefect_redis_security_group.id], + tags=tags, +) + +# Allow ECS tasks to communicate with Prefect server on port 4200 +aws.ec2.SecurityGroupRule( + "ecs_prefect_ingress", + type="ingress", + security_group_id=ecs_security_group.id, + source_security_group_id=ecs_security_group.id, + protocol="tcp", + from_port=4200, + to_port=4200, + description="Allow Prefect server communication", +) + +# Allow ALB to reach Prefect server on port 4200 +aws.ec2.SecurityGroupRule( + "ecs_prefect_alb_ingress", + type="ingress", + security_group_id=ecs_security_group.id, + source_security_group_id=alb_security_group.id, + protocol="tcp", + from_port=4200, + to_port=4200, + description="Allow ALB traffic to Prefect server", +) + +# Prefect Server Log Group +prefect_server_log_group = aws.cloudwatch.LogGroup( + "prefect_server_logs", + name="/ecs/fund/prefect-server", + retention_in_days=7, + tags=tags, +) + +# Prefect Worker Log Group +prefect_worker_log_group = aws.cloudwatch.LogGroup( + "prefect_worker_logs", + name="/ecs/fund/prefect-worker", + retention_in_days=7, + tags=tags, +) + +# Prefect Server Task Definition +prefect_server_task_definition = aws.ecs.TaskDefinition( + "prefect_server_task", + family="prefect-server", + cpu="512", + memory="1024", + network_mode="awsvpc", + requires_compatibilities=["FARGATE"], + execution_role_arn=execution_role.arn, + task_role_arn=task_role.arn, + container_definitions=pulumi.Output.all( + prefect_server_log_group.name, + prefect_database.endpoint, + prefect_database.master_user_secrets[0]["secret_arn"], + prefect_server_image_uri, + alb.dns_name, + ).apply( + lambda args: json.dumps( + [ { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": {"Service": "sagemaker.amazonaws.com"}, + "name": "prefect-server", + "image": args[3], + # Inline bash/python constructs the database URL at runtime + # because the password comes from Secrets Manager and must be + # URL-encoded before embedding in the connection string. + # Extracting this to a separate script would require building + # and deploying another Docker image. + "command": [ + "bash", + "-c", + ( + "export PREFECT_API_DATABASE_CONNECTION_URL=" + '$(python3 -c "' + "import os, urllib.parse;" + "p=urllib.parse.quote(os.environ['PREFECT_DB_PASSWORD'],safe='');" + f"print(f'postgresql+asyncpg://prefect:{{p}}@{args[1]}/prefect')" + '")' + " && prefect server start --host 0.0.0.0" + ), + ], + "portMappings": [{"containerPort": 4200, "protocol": "tcp"}], + "environment": [ + { + "name": "PREFECT_UI_API_URL", + "value": ( + f"{'https' if acm_certificate_arn else 'http'}://" + f"{args[4]}:4200/api" + ), + }, + ], + "secrets": [ + { + "name": "PREFECT_DB_PASSWORD", + "valueFrom": f"{args[2]}:password::", + }, + ], + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-group": args[0], + "awslogs-region": region, + "awslogs-stream-prefix": "prefect-server", + }, + }, + "essential": True, } ], - }, - sort_keys=True, + sort_keys=True, + ) + ), + tags=tags, +) + +# Prefect Server Service Discovery +prefect_server_sd_service = aws.servicediscovery.Service( + "prefect_server_sd", + name="prefect-server", + dns_config=aws.servicediscovery.ServiceDnsConfigArgs( + namespace_id=service_discovery_namespace.id, + dns_records=[ + aws.servicediscovery.ServiceDnsConfigDnsRecordArgs(ttl=10, type="A") + ], + ), + tags=tags, +) + +# Prefect Server ECS Service +prefect_server_service = aws.ecs.Service( + "prefect_server_service", + name="fund-prefect-server", + cluster=cluster.arn, + task_definition=prefect_server_task_definition.arn, + desired_count=1, + launch_type="FARGATE", + network_configuration=aws.ecs.ServiceNetworkConfigurationArgs( + subnets=[private_subnet_1.id, private_subnet_2.id], + security_groups=[ecs_security_group.id], + assign_public_ip=False, + ), + load_balancers=[ + aws.ecs.ServiceLoadBalancerArgs( + target_group_arn=prefect_tg.arn, + container_name="prefect-server", + container_port=4200, + ) + ], + service_registries=aws.ecs.ServiceServiceRegistriesArgs( + registry_arn=prefect_server_sd_service.arn + ), + opts=pulumi.ResourceOptions( + depends_on=[prefect_database, prefect_redis, prefect_listener], + ), + tags=tags, +) + +# Prefect Worker Task Definition +prefect_worker_task_definition = aws.ecs.TaskDefinition( + "prefect_worker_task", + family="prefect-worker", + cpu="4096", + memory="8192", + network_mode="awsvpc", + requires_compatibilities=["FARGATE"], + execution_role_arn=execution_role.arn, + task_role_arn=task_role.arn, + container_definitions=pulumi.Output.all( + prefect_worker_log_group.name, + service_discovery_namespace.name, + data_bucket.bucket, + model_artifacts_bucket.bucket, + prefect_worker_image_uri, + training_notification_sender_email_parameter.arn, + training_notification_recipients_parameter.arn, + ).apply( + lambda args: json.dumps( + [ + { + "name": "prefect-worker", + "image": args[4], + "environment": [ + { + "name": "PREFECT_API_URL", + "value": f"http://prefect-server.{args[1]}:4200/api", + }, + { + "name": "AWS_S3_DATA_BUCKET_NAME", + "value": args[2], + }, + { + "name": "AWS_S3_MODEL_ARTIFACTS_BUCKET_NAME", + "value": args[3], + }, + { + "name": "FUND_DATAMANAGER_BASE_URL", + "value": f"http://datamanager.{args[1]}:8080", + }, + { + "name": "LOOKBACK_DAYS", + "value": "365", + }, + ], + "secrets": [ + { + "name": "TRAINING_NOTIFICATION_SENDER_EMAIL", + "valueFrom": args[5], + }, + { + "name": "TRAINING_NOTIFICATION_RECIPIENT_EMAILS", + "valueFrom": args[6], + }, + ], + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-group": args[0], + "awslogs-region": region, + "awslogs-stream-prefix": "prefect-worker", + }, + }, + "essential": True, + } + ], + sort_keys=True, + ) + ), + tags=tags, +) + +# Prefect Worker ECS Service +prefect_worker_service = aws.ecs.Service( + "prefect_worker_service", + name="fund-prefect-worker", + cluster=cluster.arn, + task_definition=prefect_worker_task_definition.arn, + desired_count=1, + launch_type="FARGATE", + network_configuration=aws.ecs.ServiceNetworkConfigurationArgs( + subnets=[private_subnet_1.id, private_subnet_2.id], + security_groups=[ecs_security_group.id], + assign_public_ip=False, + ), + opts=pulumi.ResourceOptions( + depends_on=[prefect_server_service], ), - managed_policy_arns=[sagemaker_execution_policy.arn], - opts=pulumi.ResourceOptions(retain_on_delete=True), tags=tags, ) @@ -1808,7 +2283,23 @@ def serialize_secret_config_object( pulumi.export( "aws_ecr_equitypricemodel_trainer_image", equitypricemodel_trainer_image_uri ) -pulumi.export("aws_iam_sagemaker_role_arn", sagemaker_execution_role.arn) +pulumi.export( + "aws_ecr_prefect_worker_repository", prefect_worker_repository.repository_url +) +pulumi.export("aws_ecr_prefect_worker_image", prefect_worker_image_uri) +pulumi.export( + "prefect_api_url", + pulumi.Output.concat( + "http://prefect-server.", service_discovery_namespace.name, ":4200/api" + ), +) +prefect_ui_url = ( + pulumi.Output.concat("https://", alb.dns_name, ":4200") + if acm_certificate_arn + else pulumi.Output.from_input("TLS certificate not configured") +) +pulumi.export("prefect_ui_url", prefect_ui_url) +pulumi.export("prefect_ui_tls_enabled", bool(acm_certificate_arn)) pulumi.export( "aws_iam_github_actions_infrastructure_role_arn", github_actions_infrastructure_role.arn, diff --git a/infrastructure/parameters.py b/infrastructure/parameters.py index 69011ac83..1893319cb 100644 --- a/infrastructure/parameters.py +++ b/infrastructure/parameters.py @@ -22,3 +22,15 @@ description="Maximum inter-quartile range for predictions to be considered valid", tags=tags, ) + +# Equity Price Model Configuration +equitypricemodel_model_version = aws.ssm.Parameter( + "ssm_equitypricemodel_model_version", + name="/fund/equitypricemodel/model_version", + type="String", + value="latest", + description=( + "Model artifact version to load (S3 key suffix or 'latest' for auto-discovery)" + ), + tags=tags, +) diff --git a/maskfile.md b/maskfile.md index 7faa38781..176cc81ad 100644 --- a/maskfile.md +++ b/maskfile.md @@ -208,13 +208,6 @@ if [ -n "$GITHUB_POLICY_ARN" ]; then pulumi import --yes --generate-code=false aws:iam/policy:Policy github_actions_infrastructure_policy "$GITHUB_POLICY_ARN" 2>/dev/null || true fi -pulumi import --yes --generate-code=false aws:iam/role:Role sagemaker_execution_role fund-sagemaker-execution-role 2>/dev/null || true - -SAGEMAKER_POLICY_ARN=$(aws iam list-policies --scope Local --query 'Policies[?PolicyName==`fund-sagemaker-execution-policy`].Arn' --output text 2>/dev/null || echo "") -if [ -n "$SAGEMAKER_POLICY_ARN" ]; then - pulumi import --yes --generate-code=false aws:iam/policy:Policy sagemaker_execution_policy "$SAGEMAKER_POLICY_ARN" 2>/dev/null || true -fi - pulumi import --yes --generate-code=false aws:s3/bucket:Bucket data_bucket "fund-data-${RANDOM_SUFFIX}" 2>/dev/null || true pulumi import --yes --generate-code=false aws:s3/bucketServerSideEncryptionConfiguration:BucketServerSideEncryptionConfiguration data_bucket_encryption "fund-data-${RANDOM_SUFFIX}" 2>/dev/null || true pulumi import --yes --generate-code=false aws:s3/bucketPublicAccessBlock:BucketPublicAccessBlock data_bucket_public_access_block "fund-data-${RANDOM_SUFFIX}" 2>/dev/null || true @@ -225,6 +218,8 @@ pulumi import --yes --generate-code=false aws:s3/bucketServerSideEncryptionConfi pulumi import --yes --generate-code=false aws:s3/bucketPublicAccessBlock:BucketPublicAccessBlock model_artifacts_bucket_public_access_block "fund-model-artifacts-${RANDOM_SUFFIX}" 2>/dev/null || true pulumi import --yes --generate-code=false aws:s3/bucketVersioning:BucketVersioning model_artifacts_bucket_versioning "fund-model-artifacts-${RANDOM_SUFFIX}" 2>/dev/null || true +pulumi import --yes --generate-code=false aws:ssm/parameter:Parameter ssm_equitypricemodel_model_version "/fund/equitypricemodel/model_version" 2>/dev/null || true + echo "Importing resources complete" pulumi up --diff --yes @@ -270,7 +265,7 @@ else # Note: Service names use 'fund' prefix matching the Pulumi project name. # These must exactly match the ECS service names created by the infrastructure code. # The AWS account provides environment context (one account = one environment). - for service in fund-datamanager fund-portfoliomanager fund-equitypricemodel; do + for service in fund-datamanager fund-portfoliomanager fund-equitypricemodel fund-prefect-server fund-prefect-worker; do echo "Checking if $service exists and is ready" # Wait up to 60 seconds for service to be active @@ -797,63 +792,43 @@ cd ../ uv run python -m tools.prepare_training_data ``` -### train (application_name) [instance_preset] +### train (application_name) -> Train model on SageMaker. Presets: testing, standard (default), performance, or custom instance type +> Train model via Prefect training pipeline ```bash set -euo pipefail export APPLICATION_NAME="${application_name}" -preset="${instance_preset:-standard}" +cd infrastructure -case "$preset" in - testing) - instance_type="ml.t3.xlarge" - echo "================================================" - echo "Training with TESTING architecture (CPU)" - echo "Instance: ml.t3.xlarge (~\$0.23/hr)" - echo "Use for: Quick iteration, debugging" - echo "================================================" - ;; - standard) - instance_type="ml.g5.xlarge" - echo "================================================" - echo "Training with STANDARD architecture (GPU)" - echo "Instance: ml.g5.xlarge - 1x A10G (~\$1.41/hr)" - echo "Use for: Regular training runs" - echo "================================================" - ;; - performance) - instance_type="ml.p3.2xlarge" - echo "================================================" - echo "Training with PERFORMANCE architecture (GPU)" - echo "Instance: ml.p3.2xlarge - 1x V100 (~\$3.82/hr)" - echo "Use for: Large datasets, faster training" - echo "================================================" - ;; - ml.*) - instance_type="$preset" - echo "================================================" - echo "Training with CUSTOM architecture" - echo "Instance: ${instance_type}" - echo "================================================" - ;; - *) - echo "Unknown preset: $preset" - echo "" - echo "Available presets:" - echo "testing - ml.t3.xlarge (CPU, ~\$0.23/hr)" - echo "standard - ml.g5.xlarge (GPU, ~\$1.41/hr) [default]" - echo "performance - ml.p3.2xlarge (GPU, ~\$3.82/hr)" - echo "" - echo "Or specify a custom instance type: ml.g4dn.xlarge" - exit 1 - ;; -esac +if ! organization_name=$(pulumi org get-default 2>/dev/null) || [ -z "${organization_name}" ]; then + echo "Unable to determine Pulumi organization name - ensure you are logged in" + exit 1 +fi -export SAGEMAKER_INSTANCE_TYPE="${instance_type}" +pulumi stack select ${organization_name}/fund/production + +export FUND_DATAMANAGER_BASE_URL="$(pulumi stack output aws_alb_url)" +export AWS_S3_DATA_BUCKET_NAME="$(pulumi stack output aws_s3_data_bucket_name)" +export AWS_S3_MODEL_ARTIFACTS_BUCKET_NAME="$(pulumi stack output aws_s3_model_artifacts_bucket_name)" +export PREFECT_API_URL="$(pulumi stack output prefect_api_url)" +export LOOKBACK_DAYS="${LOOKBACK_DAYS:-365}" + +cd ../ + +uv run python -m tools.run_training_job +``` + +### deploy (application_name) + +> Register flow deployment with Prefect server + +```bash +set -euo pipefail + +export APPLICATION_NAME="${application_name}" cd infrastructure @@ -864,15 +839,15 @@ fi pulumi stack select ${organization_name}/fund/production -export AWS_ECR_EQUITY_PRICE_MODEL_TRAINER_IMAGE_ARN="$(pulumi stack output aws_ecr_equitypricemodel_trainer_image)" -export AWS_IAM_SAGEMAKER_ROLE_ARN="$(pulumi stack output aws_iam_sagemaker_role_arn)" +export FUND_DATAMANAGER_BASE_URL="http://datamanager.$(pulumi stack output aws_service_discovery_namespace):8080" +export AWS_S3_DATA_BUCKET_NAME="$(pulumi stack output aws_s3_data_bucket_name)" export AWS_S3_MODEL_ARTIFACTS_BUCKET_NAME="$(pulumi stack output aws_s3_model_artifacts_bucket_name)" -export AWS_S3_EQUITY_PRICE_MODEL_ARTIFACT_OUTPUT_PATH="s3://${AWS_S3_MODEL_ARTIFACTS_BUCKET_NAME}/artifacts" -export AWS_S3_EQUITY_PRICE_MODEL_TRAINING_DATA_PATH="s3://${AWS_S3_MODEL_ARTIFACTS_BUCKET_NAME}/training" +export PREFECT_API_URL="$(pulumi stack output prefect_api_url)" +export LOOKBACK_DAYS="${LOOKBACK_DAYS:-365}" cd ../ -uv run python -m tools.run_training_job +uv run python -m tools.deploy_training_flow ``` ### artifacts @@ -891,6 +866,51 @@ export APPLICATION_NAME="${application_name}" uv run python -m tools.download_model_artifacts ``` +## prefect + +> Prefect infrastructure management + +### build-worker + +> Build and push the Prefect worker Docker image to ECR + +```bash +set -euo pipefail + +echo "Building Prefect worker image" + +aws_account_id=$(aws sts get-caller-identity --query Account --output text) +aws_region=${AWS_REGION} +if [ -z "$aws_region" ]; then + echo "AWS_REGION environment variable is not set" + exit 1 +fi + +image_reference="${aws_account_id}.dkr.ecr.${aws_region}.amazonaws.com/fund/prefect-worker" + +echo "Logging into ECR" +aws ecr get-login-password --region ${aws_region} | docker login \ + --username AWS \ + --password-stdin ${aws_account_id}.dkr.ecr.${aws_region}.amazonaws.com > /dev/null + +echo "Building image for linux/amd64" +docker build \ + --platform linux/amd64 \ + --target worker \ + --file tools/Dockerfile \ + --tag ${image_reference}:latest \ + . + +echo "Pushing image to ECR" +docker push ${image_reference}:latest + +commit_hash=$(git rev-parse --short HEAD) +docker tag "${image_reference}:latest" "${image_reference}:git-${commit_hash}" +docker push "${image_reference}:git-${commit_hash}" + +echo "Prefect worker image pushed: ${image_reference}:latest (commit: ${commit_hash})" +``` + ## mcp > MCP server management diff --git a/pyproject.toml b/pyproject.toml index bbc8861b0..cefaa1498 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -112,6 +112,3 @@ include = ["infrastructure/**"] invalid-argument-type = "ignore" missing-argument = "ignore" possibly-missing-attribute = "ignore" - -[tool.pyright] -reportMissingImports = "none" diff --git a/tools/Dockerfile b/tools/Dockerfile new file mode 100644 index 000000000..f78abea10 --- /dev/null +++ b/tools/Dockerfile @@ -0,0 +1,61 @@ +FROM python:3.12.10-slim AS builder + +COPY --from=ghcr.io/astral-sh/uv:0.7.2 /uv /bin/uv + +RUN apt-get update && \ + apt-get install -y --no-install-recommends build-essential clang && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY pyproject.toml uv.lock ./ + +COPY tools/ tools/ + +COPY applications/equitypricemodel/ applications/equitypricemodel/ + +COPY libraries/python/ libraries/python/ + +RUN uv sync --no-dev + +FROM python:3.12.10-slim AS worker + +WORKDIR /app + +RUN groupadd --system worker && useradd --system --gid worker worker + +COPY --from=ghcr.io/astral-sh/uv:0.7.2 /uv /bin/uv + +COPY --from=builder /app /app + +ENV PYTHONPATH=/app/tools/src:/app/applications/equitypricemodel/src:/app/libraries/python/src +ENV HOME=/home/worker + +RUN mkdir -p /home/worker && chown -R worker:worker /home/worker /app && \ + printf '%s\n' \ + '#!/usr/bin/env python3' \ + 'import os' \ + 'import sys' \ + '' \ + 'for process_id in os.listdir("/proc"):' \ + ' if not process_id.isdigit():' \ + ' continue' \ + ' command_path = f"/proc/{process_id}/cmdline"' \ + ' try:' \ + ' with open(command_path, "rb") as command_file:' \ + ' command = command_file.read().replace(b"\\x00", b" ").decode("utf-8", errors="ignore")' \ + ' except OSError:' \ + ' continue' \ + ' if "prefect worker start" in command and "training-pool" in command:' \ + ' sys.exit(0)' \ + '' \ + 'sys.exit(1)' \ + > /usr/local/bin/prefect_worker_healthcheck.py && \ + chmod +x /usr/local/bin/prefect_worker_healthcheck.py + +USER worker + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD ["python", "/usr/local/bin/prefect_worker_healthcheck.py"] + +ENTRYPOINT ["uv", "run", "--package", "tools", "prefect", "worker", "start", "--pool", "training-pool", "--type", "process"] diff --git a/tools/pyproject.toml b/tools/pyproject.toml index 3872c4bb2..b0ebeb456 100644 --- a/tools/pyproject.toml +++ b/tools/pyproject.toml @@ -5,7 +5,7 @@ description = "Project tools and scripts" requires-python = "==3.12.10" dependencies = [ "boto3>=1.40.74", - "sagemaker>=2.256.0,<3.0.0", + "prefect>=3.0.0,<4.0.0", "structlog>=25.5.0", "requests>=2.32.5", "polars>=1.29.0", diff --git a/tools/src/tools/deploy_training_flow.py b/tools/src/tools/deploy_training_flow.py new file mode 100644 index 000000000..d3dbe7551 --- /dev/null +++ b/tools/src/tools/deploy_training_flow.py @@ -0,0 +1,77 @@ +import os +import sys + +import structlog +from prefect.flows import EntrypointType + +from tools.flows.training_flow import training_pipeline + +logger = structlog.get_logger() + + +def deploy_training_flow( + base_url: str, + data_bucket: str, + artifacts_bucket: str, + lookback_days: int = 365, +) -> None: + """Register the training pipeline deployment with the Prefect server.""" + logger.info( + "Deploying training pipeline", + base_url=base_url, + data_bucket=data_bucket, + artifacts_bucket=artifacts_bucket, + lookback_days=lookback_days, + ) + + training_pipeline.deploy( + name="daily-training", + work_pool_name="training-pool", + cron="0 22 * * *", + parameters={ + "base_url": base_url, + "data_bucket": data_bucket, + "artifacts_bucket": artifacts_bucket, + "lookback_days": lookback_days, + }, + tags=["training", "daily"], + entrypoint_type=EntrypointType.MODULE_PATH, + build=False, + push=False, + ) + + logger.info("Training pipeline deployed") + + +if __name__ == "__main__": + base_url = os.getenv("FUND_DATAMANAGER_BASE_URL", "") + data_bucket = os.getenv("AWS_S3_DATA_BUCKET_NAME", "") + artifacts_bucket = os.getenv("AWS_S3_MODEL_ARTIFACTS_BUCKET_NAME", "") + + try: + lookback_days = int(os.getenv("LOOKBACK_DAYS", "365")) + except ValueError: + logger.exception("LOOKBACK_DAYS must be a valid integer") + sys.exit(1) + + if lookback_days <= 0: + logger.error("LOOKBACK_DAYS must be positive", lookback_days=lookback_days) + sys.exit(1) + + required_vars = { + "FUND_DATAMANAGER_BASE_URL": base_url, + "AWS_S3_DATA_BUCKET_NAME": data_bucket, + "AWS_S3_MODEL_ARTIFACTS_BUCKET_NAME": artifacts_bucket, + } + + missing = [key for key, value in required_vars.items() if not value] + if missing: + logger.error("Missing required environment variables", missing=missing) + sys.exit(1) + + deploy_training_flow( + base_url=base_url, + data_bucket=data_bucket, + artifacts_bucket=artifacts_bucket, + lookback_days=lookback_days, + ) diff --git a/tools/src/tools/flows/__init__.py b/tools/src/tools/flows/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tools/src/tools/flows/notifications.py b/tools/src/tools/flows/notifications.py new file mode 100644 index 000000000..eda405af2 --- /dev/null +++ b/tools/src/tools/flows/notifications.py @@ -0,0 +1,86 @@ +import os +from datetime import UTC, datetime + +import boto3 +import structlog +from prefect.client.schemas.objects import Flow, FlowRun, State + +logger = structlog.get_logger() + + +def send_training_notification(flow: Flow, flow_run: FlowRun, state: State) -> None: + """Send email notification via SES on training pipeline completion or failure.""" + sender_email = os.getenv("TRAINING_NOTIFICATION_SENDER_EMAIL", "").strip() + recipient_emails_raw = os.getenv( + "TRAINING_NOTIFICATION_RECIPIENT_EMAILS", "" + ).strip() + + if not sender_email or not recipient_emails_raw: + logger.warning( + "Notification emails not configured, skipping", + sender_email=sender_email, + recipient_emails=recipient_emails_raw, + ) + return + + recipient_emails = [ + email.strip() for email in recipient_emails_raw.split(",") if email.strip() + ] + if not recipient_emails: + logger.warning( + "Notification recipients are empty after parsing, skipping", + recipient_emails_raw=recipient_emails_raw, + ) + return + + state_name = state.name or "Unknown" + is_failure = state.is_failed() + + duration_seconds = None + if flow_run.start_time and flow_run.end_time: + duration_seconds = (flow_run.end_time - flow_run.start_time).total_seconds() + + duration_text = ( + f"{duration_seconds:.0f} seconds" if duration_seconds is not None else "unknown" + ) + + subject = ( + f"Training pipeline {'FAILED' if is_failure else 'completed'}: " + f"{flow.name}/{flow_run.name}" + ) + + body_parts = [ + f"Flow: {flow.name}", + f"Run: {flow_run.name}", + f"State: {state_name}", + f"Duration: {duration_text}", + f"Timestamp: {datetime.now(tz=UTC).isoformat()}", + ] + + if is_failure and state.message: + body_parts.append(f"\nError: {state.message}") + + body = "\n".join(body_parts) + + try: + ses_client = boto3.client("ses") + ses_client.send_email( + Source=sender_email, + Destination={"ToAddresses": recipient_emails}, + Message={ + "Subject": {"Data": subject, "Charset": "UTF-8"}, + "Body": {"Text": {"Data": body, "Charset": "UTF-8"}}, + }, + ) + logger.info( + "Training notification sent", + recipients=recipient_emails, + state=state_name, + ) + except Exception: + logger.exception( + "Failed to send training notification", + sender_email=sender_email, + recipients=recipient_emails, + state=state_name, + ) diff --git a/tools/src/tools/flows/training_flow.py b/tools/src/tools/flows/training_flow.py new file mode 100644 index 000000000..58fdd68c7 --- /dev/null +++ b/tools/src/tools/flows/training_flow.py @@ -0,0 +1,233 @@ +import io +import os +import sys +import tarfile +import tempfile +from datetime import UTC, datetime, timedelta +from pathlib import Path + +import boto3 +import polars as pl +import structlog +from prefect import flow, task + +from tools.flows.notifications import send_training_notification +from tools.prepare_training_data import prepare_training_data +from tools.sync_equity_bars_data import sync_equity_bars_data +from tools.sync_equity_details_data import sync_equity_details_data + +logger = structlog.get_logger() + + +def get_training_date_range(lookback_days: int) -> tuple[datetime, datetime]: + """Build a UTC date range used by sync + prepare steps.""" + end_date = datetime.now(tz=UTC).replace(hour=0, minute=0, second=0, microsecond=0) + start_date = end_date - timedelta(days=lookback_days) + return start_date, end_date + + +@task(name="sync-equity-bars", retries=2, retry_delay_seconds=30) +def sync_equity_bars(base_url: str, start_date: datetime, end_date: datetime) -> None: + """Trigger datamanager to sync equity bars.""" + + logger.info( + "Syncing equity bars", + base_url=base_url, + start_date=start_date.isoformat(), + end_date=end_date.isoformat(), + ) + + sync_equity_bars_data( + base_url=base_url, + date_range=(start_date, end_date), + ) + + +@task(name="sync-equity-details", retries=2, retry_delay_seconds=30) +def sync_equity_details(base_url: str) -> None: + """Trigger datamanager to sync equity details.""" + logger.info("Syncing equity details", base_url=base_url) + try: + sync_equity_details_data(base_url=base_url) + except RuntimeError as error: + if "status 501" in str(error): + logger.warning( + "Equity details sync is not implemented, skipping", + base_url=base_url, + ) + return + raise + + +@task(name="prepare-training-data") +def prepare_data( + data_bucket: str, + artifacts_bucket: str, + start_date: datetime, + end_date: datetime, + output_key: str = "training/filtered_tide_training_data.parquet", +) -> str: + """Read equity bars + categories from S3, filter, write consolidated parquet.""" + logger.info( + "Preparing training data", + data_bucket=data_bucket, + artifacts_bucket=artifacts_bucket, + start_date=start_date.isoformat(), + end_date=end_date.isoformat(), + ) + + training_data_uri = prepare_training_data( + data_bucket_name=data_bucket, + model_artifacts_bucket_name=artifacts_bucket, + start_date=start_date, + end_date=end_date, + output_key=output_key, + ) + + bucket_prefix = f"s3://{artifacts_bucket}/" + if training_data_uri.startswith(bucket_prefix): + return training_data_uri.removeprefix(bucket_prefix) + + logger.warning( + "Prepared training data URI did not match expected bucket", + expected_bucket=artifacts_bucket, + training_data_uri=training_data_uri, + ) + return output_key + + +@task(name="train-tide-model", timeout_seconds=3600) +def train_tide_model( + artifacts_bucket: str, + training_data_key: str = "training/filtered_tide_training_data.parquet", +) -> str: + """Download training data from S3, train model, upload artifact to S3.""" + # Defer import to avoid importing tinygrad at module level (heavy GPU dependency) + from equitypricemodel.trainer import train_model # noqa: PLC0415 + + resolved_training_data_key = training_data_key + bucket_prefix = f"s3://{artifacts_bucket}/" + if training_data_key.startswith(bucket_prefix): + resolved_training_data_key = training_data_key.removeprefix(bucket_prefix) + + logger.info( + "Starting model training", + artifacts_bucket=artifacts_bucket, + training_data_key=resolved_training_data_key, + ) + + s3_client = boto3.client("s3") + + response = s3_client.get_object( + Bucket=artifacts_bucket, + Key=resolved_training_data_key, + ) + training_data = pl.read_parquet(response["Body"].read()) + logger.info("Training data loaded", rows=training_data.height) + + with tempfile.TemporaryDirectory(prefix="checkpoints_") as checkpoint_directory: + tide_model, tide_data = train_model( + training_data, + checkpoint_directory=checkpoint_directory, + ) + + timestamp = datetime.now(tz=UTC).strftime("%Y-%m-%d-%H-%M-%S-%f")[:-3] + artifact_folder = f"artifacts/equitypricemodel-trainer-{timestamp}" + artifact_key = f"{artifact_folder}/output/model.tar.gz" + + with tempfile.TemporaryDirectory() as tmpdir: + tide_model.save(directory_path=tmpdir) + tide_data.save(directory_path=tmpdir) + + tar_buffer = io.BytesIO() + with tarfile.open(fileobj=tar_buffer, mode="w:gz") as tar: + for entry in Path(tmpdir).iterdir(): + tar.add(entry, arcname=entry.name) + tar_bytes = tar_buffer.getvalue() + + logger.info( + "Uploading model artifact", + bucket=artifacts_bucket, + key=artifact_key, + size_bytes=len(tar_bytes), + ) + + s3_client.put_object( + Bucket=artifacts_bucket, + Key=artifact_key, + Body=tar_bytes, + ContentType="application/gzip", + ) + + logger.info( + "Model artifact uploaded", + artifact_path=f"s3://{artifacts_bucket}/{artifact_key}", + ) + + return f"s3://{artifacts_bucket}/{artifact_key}" + + +@flow( # type: ignore[no-matching-overload] + name="tide-training-pipeline", + log_prints=True, + on_completion=[send_training_notification], + on_failure=[send_training_notification], +) +def training_pipeline( + base_url: str, + data_bucket: str, + artifacts_bucket: str, + lookback_days: int = 365, +) -> str: + """End-to-end training pipeline.""" + if lookback_days <= 0: + message = "lookback_days must be positive" + raise ValueError(message) + + training_data_key = "training/filtered_tide_training_data.parquet" + start_date, end_date = get_training_date_range(lookback_days) + + sync_equity_bars(base_url, start_date, end_date) + sync_equity_details(base_url) + prepared_key = prepare_data( + data_bucket, + artifacts_bucket, + start_date, + end_date, + training_data_key, + ) + return train_tide_model(artifacts_bucket, prepared_key) + + +if __name__ == "__main__": + base_url = os.getenv("FUND_DATAMANAGER_BASE_URL", "") + data_bucket = os.getenv("AWS_S3_DATA_BUCKET_NAME", "") + artifacts_bucket = os.getenv("AWS_S3_MODEL_ARTIFACTS_BUCKET_NAME", "") + + try: + lookback_days = int(os.getenv("LOOKBACK_DAYS", "365")) + except ValueError: + logger.exception("LOOKBACK_DAYS must be a valid integer") + sys.exit(1) + + if lookback_days <= 0: + logger.error("LOOKBACK_DAYS must be positive", lookback_days=lookback_days) + sys.exit(1) + + required_vars = { + "FUND_DATAMANAGER_BASE_URL": base_url, + "AWS_S3_DATA_BUCKET_NAME": data_bucket, + "AWS_S3_MODEL_ARTIFACTS_BUCKET_NAME": artifacts_bucket, + } + + missing = [key for key, value in required_vars.items() if not value] + if missing: + logger.error("Missing required environment variables", missing=missing) + sys.exit(1) + + training_pipeline( + base_url=base_url, + data_bucket=data_bucket, + artifacts_bucket=artifacts_bucket, + lookback_days=lookback_days, + ) diff --git a/tools/src/tools/run_training_job.py b/tools/src/tools/run_training_job.py index c9f3e5f25..20f9c6c88 100644 --- a/tools/src/tools/run_training_job.py +++ b/tools/src/tools/run_training_job.py @@ -1,113 +1,77 @@ import os import sys -import boto3 import structlog -from sagemaker.estimator import Estimator -from sagemaker.inputs import TrainingInput -from sagemaker.session import Session + +from tools.flows.training_flow import training_pipeline logger = structlog.get_logger() -def run_training_job( # noqa: PLR0913 - application_name: str, - trainer_image_uri: str, - s3_data_path: str, - iam_sagemaker_role_arn: str, - s3_artifact_path: str, - instance_type: str = "ml.g5.xlarge", -) -> None: +def run_training_job( + base_url: str, + data_bucket: str, + artifacts_bucket: str, + lookback_days: int = 365, +) -> str: + """Run the TiDE training pipeline via Prefect.""" + if lookback_days <= 0: + message = "lookback_days must be positive" + raise ValueError(message) + logger.info( - "Starting training job", - application_name=application_name, - instance_type=instance_type, + "Starting training pipeline", + base_url=base_url, + data_bucket=data_bucket, + artifacts_bucket=artifacts_bucket, + lookback_days=lookback_days, ) - try: - session = boto3.Session() - sagemaker_session = Session(boto_session=session) - - except Exception as e: - logger.exception( - "Error creating SageMaker session", - error=f"{e}", - application_name=application_name, - ) - raise RuntimeError from e - - estimator = Estimator( - image_uri=trainer_image_uri, - role=iam_sagemaker_role_arn, - instance_count=1, - instance_type=instance_type, - sagemaker_session=sagemaker_session, - output_path=s3_artifact_path, + artifact_path = training_pipeline( + base_url=base_url, + data_bucket=data_bucket, + artifacts_bucket=artifacts_bucket, + lookback_days=lookback_days, ) - training_data_input = TrainingInput( - s3_data=s3_data_path, - content_type="application/x-parquet", - input_mode="File", - ) + logger.info("Training pipeline complete", artifact_path=artifact_path) - try: - estimator.fit({"train": training_data_input}) - except Exception as e: - logger.exception( - "Error during training job", - error=f"{e}", - application_name=application_name, - ) - raise RuntimeError from e + return artifact_path if __name__ == "__main__": - application_name = os.getenv("APPLICATION_NAME", "") - trainer_image_uri = os.getenv("AWS_ECR_EQUITY_PRICE_MODEL_TRAINER_IMAGE_ARN", "") - s3_data_path = os.getenv("AWS_S3_EQUITY_PRICE_MODEL_TRAINING_DATA_PATH", "") - iam_sagemaker_role_arn = os.getenv("AWS_IAM_SAGEMAKER_ROLE_ARN", "") - s3_artifact_path = os.getenv("AWS_S3_EQUITY_PRICE_MODEL_ARTIFACT_OUTPUT_PATH", "") - instance_type_raw = os.getenv("SAGEMAKER_INSTANCE_TYPE") - instance_type = ( - instance_type_raw.strip() - if instance_type_raw and instance_type_raw.strip() - else "ml.g5.xlarge" - ) + base_url = os.getenv("FUND_DATAMANAGER_BASE_URL", "") + data_bucket = os.getenv("AWS_S3_DATA_BUCKET_NAME", "") + artifacts_bucket = os.getenv("AWS_S3_MODEL_ARTIFACTS_BUCKET_NAME", "") - environment_variables = { - "APPLICATION_NAME": application_name, - "AWS_ECR_EQUITY_PRICE_MODEL_TRAINER_IMAGE_ARN": trainer_image_uri, - "AWS_S3_EQUITY_PRICE_MODEL_TRAINING_DATA_PATH": s3_data_path, - "AWS_IAM_SAGEMAKER_ROLE_ARN": iam_sagemaker_role_arn, - "AWS_S3_EQUITY_PRICE_MODEL_ARTIFACT_OUTPUT_PATH": s3_artifact_path, + required_vars = { + "FUND_DATAMANAGER_BASE_URL": base_url, + "AWS_S3_DATA_BUCKET_NAME": data_bucket, + "AWS_S3_MODEL_ARTIFACTS_BUCKET_NAME": artifacts_bucket, } - missing_environment_variables = [ - key for key, value in environment_variables.items() if not value - ] - if missing_environment_variables: - logger.error( - "Missing required environment variables", - missing_environment_variables=missing_environment_variables, - **environment_variables, - ) + missing = [key for key, value in required_vars.items() if not value] + if missing: + logger.error("Missing required environment variables", missing=missing) + sys.exit(1) + + try: + lookback_days = int(os.getenv("LOOKBACK_DAYS", "365")) + except ValueError: + logger.exception("LOOKBACK_DAYS must be a valid integer") + sys.exit(1) + + if lookback_days <= 0: + logger.error("LOOKBACK_DAYS must be positive", lookback_days=lookback_days) sys.exit(1) try: run_training_job( - application_name=application_name, - trainer_image_uri=trainer_image_uri, - s3_data_path=s3_data_path, - iam_sagemaker_role_arn=iam_sagemaker_role_arn, - s3_artifact_path=s3_artifact_path, - instance_type=instance_type, + base_url=base_url, + data_bucket=data_bucket, + artifacts_bucket=artifacts_bucket, + lookback_days=lookback_days, ) - except Exception as e: - logger.exception( - "Training job failed", - error=f"{e}", - application_name=application_name, - ) + logger.exception("Training pipeline failed", error=str(e)) sys.exit(1) diff --git a/tools/tests/test_deploy_training_flow.py b/tools/tests/test_deploy_training_flow.py new file mode 100644 index 000000000..2306f0172 --- /dev/null +++ b/tools/tests/test_deploy_training_flow.py @@ -0,0 +1,46 @@ +from unittest.mock import MagicMock, patch + +from prefect.flows import EntrypointType +from tools.deploy_training_flow import deploy_training_flow + +LOOKBACK_DAYS = 30 + + +@patch("tools.deploy_training_flow.training_pipeline") +def test_deploy_training_flow_calls_deploy(mock_pipeline: MagicMock) -> None: + mock_deploy = MagicMock() + mock_pipeline.deploy = mock_deploy + + deploy_training_flow( + base_url="http://example.com", + data_bucket="data-bucket", + artifacts_bucket="artifacts-bucket", + lookback_days=LOOKBACK_DAYS, + ) + + mock_deploy.assert_called_once() + call_kwargs = mock_deploy.call_args.kwargs + assert call_kwargs["name"] == "daily-training" + assert call_kwargs["work_pool_name"] == "training-pool" + assert call_kwargs["cron"] == "0 22 * * *" + assert call_kwargs["parameters"]["base_url"] == "http://example.com" + assert call_kwargs["parameters"]["lookback_days"] == LOOKBACK_DAYS + + +@patch("tools.deploy_training_flow.training_pipeline") +def test_deploy_training_flow_sets_module_path_entrypoint( + mock_pipeline: MagicMock, +) -> None: + mock_deploy = MagicMock() + mock_pipeline.deploy = mock_deploy + + deploy_training_flow( + base_url="http://example.com", + data_bucket="data-bucket", + artifacts_bucket="artifacts-bucket", + ) + + call_kwargs = mock_deploy.call_args.kwargs + assert call_kwargs["entrypoint_type"] == EntrypointType.MODULE_PATH + assert call_kwargs["build"] is False + assert call_kwargs["push"] is False diff --git a/tools/tests/test_notifications.py b/tools/tests/test_notifications.py new file mode 100644 index 000000000..7ed347072 --- /dev/null +++ b/tools/tests/test_notifications.py @@ -0,0 +1,194 @@ +from typing import cast +from unittest.mock import MagicMock, patch +from uuid import uuid4 + +import pendulum +from prefect.client.schemas.objects import Flow, FlowRun, State, StateType +from pydantic_extra_types.pendulum_dt import DateTime +from tools.flows.notifications import send_training_notification + + +def _make_flow() -> Flow: + return Flow(id=uuid4(), name="tide-training-pipeline") + + +def _make_flow_run( + state_type: StateType, + start_time: DateTime | None = None, + end_time: DateTime | None = None, +) -> FlowRun: + return FlowRun( + id=uuid4(), + flow_id=uuid4(), + name="test-run-1", + state_type=state_type, + start_time=start_time, + end_time=end_time, + ) + + +def test_send_training_notification_on_completion() -> None: + flow = _make_flow() + flow_run = _make_flow_run( + StateType.COMPLETED, + start_time=cast("DateTime", pendulum.datetime(2024, 1, 1, 12, 0, 0, tz="UTC")), + end_time=cast("DateTime", pendulum.datetime(2024, 1, 1, 12, 30, 0, tz="UTC")), + ) + state = State(type=StateType.COMPLETED, name="Completed") + + mock_ses = MagicMock() + + with ( + patch.dict( + "os.environ", + { + "TRAINING_NOTIFICATION_SENDER_EMAIL": "sender@example.com", + "TRAINING_NOTIFICATION_RECIPIENT_EMAILS": "recipient@example.com", + }, + ), + patch("tools.flows.notifications.boto3") as mock_boto3, + ): + mock_boto3.client.return_value = mock_ses + send_training_notification(flow, flow_run, state) + + mock_boto3.client.assert_called_once_with("ses") + mock_ses.send_email.assert_called_once() + + call_kwargs = mock_ses.send_email.call_args[1] + assert call_kwargs["Source"] == "sender@example.com" + assert call_kwargs["Destination"]["ToAddresses"] == ["recipient@example.com"] + assert "completed" in call_kwargs["Message"]["Subject"]["Data"] + assert "1800 seconds" in call_kwargs["Message"]["Body"]["Text"]["Data"] + + +def test_send_training_notification_reports_zero_second_duration() -> None: + flow = _make_flow() + timestamp = cast("DateTime", pendulum.datetime(2024, 1, 1, 12, 0, 0, tz="UTC")) + flow_run = _make_flow_run( + StateType.COMPLETED, + start_time=timestamp, + end_time=timestamp, + ) + state = State(type=StateType.COMPLETED, name="Completed") + + mock_ses = MagicMock() + + with ( + patch.dict( + "os.environ", + { + "TRAINING_NOTIFICATION_SENDER_EMAIL": "sender@example.com", + "TRAINING_NOTIFICATION_RECIPIENT_EMAILS": "recipient@example.com", + }, + ), + patch("tools.flows.notifications.boto3") as mock_boto3, + ): + mock_boto3.client.return_value = mock_ses + send_training_notification(flow, flow_run, state) + + call_kwargs = mock_ses.send_email.call_args[1] + assert "0 seconds" in call_kwargs["Message"]["Body"]["Text"]["Data"] + + +def test_send_training_notification_on_failure() -> None: + flow = _make_flow() + flow_run = _make_flow_run(StateType.FAILED) + state = State( + type=StateType.FAILED, + name="Failed", + message="Something went wrong", + ) + + mock_ses = MagicMock() + + with ( + patch.dict( + "os.environ", + { + "TRAINING_NOTIFICATION_SENDER_EMAIL": "sender@example.com", + "TRAINING_NOTIFICATION_RECIPIENT_EMAILS": "a@example.com,b@example.com", + }, + ), + patch("tools.flows.notifications.boto3") as mock_boto3, + ): + mock_boto3.client.return_value = mock_ses + send_training_notification(flow, flow_run, state) + + call_kwargs = mock_ses.send_email.call_args[1] + assert call_kwargs["Destination"]["ToAddresses"] == [ + "a@example.com", + "b@example.com", + ] + assert "FAILED" in call_kwargs["Message"]["Subject"]["Data"] + assert "Something went wrong" in call_kwargs["Message"]["Body"]["Text"]["Data"] + + +def test_send_training_notification_skips_when_not_configured() -> None: + flow = _make_flow() + flow_run = _make_flow_run(StateType.COMPLETED) + state = State(type=StateType.COMPLETED, name="Completed") + + with ( + patch.dict( + "os.environ", + { + "TRAINING_NOTIFICATION_SENDER_EMAIL": "", + "TRAINING_NOTIFICATION_RECIPIENT_EMAILS": "", + }, + ), + patch("tools.flows.notifications.boto3") as mock_boto3, + ): + send_training_notification(flow, flow_run, state) + + mock_boto3.client.assert_not_called() + + +def test_send_training_notification_skips_when_recipients_parse_empty() -> None: + flow = _make_flow() + flow_run = _make_flow_run(StateType.COMPLETED) + state = State(type=StateType.COMPLETED, name="Completed") + + with ( + patch.dict( + "os.environ", + { + "TRAINING_NOTIFICATION_SENDER_EMAIL": "sender@example.com", + "TRAINING_NOTIFICATION_RECIPIENT_EMAILS": " , , ", + }, + ), + patch("tools.flows.notifications.boto3") as mock_boto3, + ): + send_training_notification(flow, flow_run, state) + + mock_boto3.client.assert_not_called() + + +def test_send_training_notification_handles_ses_error() -> None: + flow = _make_flow() + flow_run = _make_flow_run(StateType.COMPLETED) + state = State(type=StateType.COMPLETED, name="Completed") + + mock_ses = MagicMock() + mock_ses.send_email.side_effect = Exception("SES error") + + with ( + patch.dict( + "os.environ", + { + "TRAINING_NOTIFICATION_SENDER_EMAIL": "sender@example.com", + "TRAINING_NOTIFICATION_RECIPIENT_EMAILS": "recipient@example.com", + }, + ), + patch("tools.flows.notifications.boto3") as mock_boto3, + patch("tools.flows.notifications.logger") as mock_logger, + ): + mock_boto3.client.return_value = mock_ses + send_training_notification(flow, flow_run, state) + + mock_ses.send_email.assert_called_once() + mock_logger.exception.assert_called_once_with( + "Failed to send training notification", + sender_email="sender@example.com", + recipients=["recipient@example.com"], + state="Completed", + ) diff --git a/tools/tests/test_run_training_job.py b/tools/tests/test_run_training_job.py index f2cf81ffd..fdb12a4d2 100644 --- a/tools/tests/test_run_training_job.py +++ b/tools/tests/test_run_training_job.py @@ -1,44 +1,65 @@ -import sys -from unittest.mock import MagicMock, patch +from unittest.mock import patch -# sagemaker is a runtime dependency resolved from the workspace root; it is not -# available in isolated test environments, so mock its modules before importing -# the module under test to allow pytest to collect this file. -sys.modules.setdefault("sagemaker", MagicMock()) -sys.modules.setdefault("sagemaker.estimator", MagicMock()) -sys.modules.setdefault("sagemaker.inputs", MagicMock()) -sys.modules.setdefault("sagemaker.session", MagicMock()) +import pytest +from tools.run_training_job import run_training_job -from tools.run_training_job import run_training_job # noqa: E402 +def test_run_training_job_calls_training_pipeline() -> None: + with patch( + "tools.run_training_job.training_pipeline", + return_value="s3://bucket/artifacts/model.tar.gz", + ) as mock_pipeline: + result = run_training_job( + base_url="http://datamanager:8080", + data_bucket="fund-data-bucket", + artifacts_bucket="fund-artifacts-bucket", + lookback_days=365, + ) + + mock_pipeline.assert_called_once_with( + base_url="http://datamanager:8080", + data_bucket="fund-data-bucket", + artifacts_bucket="fund-artifacts-bucket", + lookback_days=365, + ) + assert result == "s3://bucket/artifacts/model.tar.gz" + + +def test_run_training_job_returns_artifact_path() -> None: + expected_path = "s3://my-bucket/artifacts/equitypricemodel-trainer-2024-01-01/output/model.tar.gz" + with patch( + "tools.run_training_job.training_pipeline", + return_value=expected_path, + ): + result = run_training_job( + base_url="http://datamanager:8080", + data_bucket="fund-data-bucket", + artifacts_bucket="fund-artifacts-bucket", + ) + + assert result == expected_path -def test_run_training_job_calls_estimator_fit() -> None: - mock_boto3_session = MagicMock() - mock_sagemaker_session = MagicMock() - mock_estimator_instance = MagicMock() +def test_run_training_job_propagates_errors() -> None: with ( patch( - "tools.run_training_job.boto3.Session", - return_value=mock_boto3_session, - ), - patch( - "tools.run_training_job.Session", - return_value=mock_sagemaker_session, - ), - patch( - "tools.run_training_job.Estimator", - return_value=mock_estimator_instance, + "tools.run_training_job.training_pipeline", + side_effect=RuntimeError("Training failed"), ), + pytest.raises(RuntimeError, match="Training failed"), ): run_training_job( - application_name="equitypricemodel", - trainer_image_uri="123456789.dkr.ecr.us-east-1.amazonaws.com/trainer:latest", - s3_data_path="s3://test-bucket/training/data.parquet", - iam_sagemaker_role_arn="arn:aws:iam::123456789:role/SageMakerRole", - s3_artifact_path="s3://test-bucket/artifacts", + base_url="http://datamanager:8080", + data_bucket="fund-data-bucket", + artifacts_bucket="fund-artifacts-bucket", ) - mock_estimator_instance.fit.assert_called_once() - fit_call_args = mock_estimator_instance.fit.call_args.args[0] - assert "train" in fit_call_args + +def test_run_training_job_rejects_non_positive_lookback() -> None: + with pytest.raises(ValueError, match="lookback_days must be positive"): + run_training_job( + base_url="http://datamanager:8080", + data_bucket="fund-data-bucket", + artifacts_bucket="fund-artifacts-bucket", + lookback_days=0, + ) diff --git a/tools/tests/test_training_flow.py b/tools/tests/test_training_flow.py new file mode 100644 index 000000000..a2effa341 --- /dev/null +++ b/tools/tests/test_training_flow.py @@ -0,0 +1,164 @@ +import io +from datetime import UTC, datetime +from unittest.mock import MagicMock, patch + +import polars as pl +import pytest +from tools.flows.training_flow import ( + prepare_data, + sync_equity_bars, + sync_equity_details, + train_tide_model, + training_pipeline, +) + +LOOKBACK_DAYS = 30 + + +@patch("tools.flows.training_flow.sync_equity_bars_data") +def test_sync_equity_bars_calls_sync_with_date_range(mock_sync: MagicMock) -> None: + start_date = datetime(2024, 1, 1, tzinfo=UTC) + end_date = datetime(2024, 1, 31, tzinfo=UTC) + sync_equity_bars.fn( + base_url="http://example.com", + start_date=start_date, + end_date=end_date, + ) + mock_sync.assert_called_once() + call_kwargs = mock_sync.call_args + assert call_kwargs.kwargs["base_url"] == "http://example.com" + start, end = call_kwargs.kwargs["date_range"] + assert start == start_date + assert end == end_date + + +@patch("tools.flows.training_flow.sync_equity_details_data") +def test_sync_equity_details_calls_sync(mock_sync: MagicMock) -> None: + sync_equity_details.fn(base_url="http://example.com") + mock_sync.assert_called_once_with(base_url="http://example.com") + + +@patch("tools.flows.training_flow.sync_equity_details_data") +def test_sync_equity_details_ignores_not_implemented(mock_sync: MagicMock) -> None: + mock_sync.side_effect = RuntimeError("Sync failed with status 501: not implemented") + sync_equity_details.fn(base_url="http://example.com") + mock_sync.assert_called_once_with(base_url="http://example.com") + + +@patch("tools.flows.training_flow.sync_equity_details_data") +def test_sync_equity_details_raises_non_501_errors(mock_sync: MagicMock) -> None: + mock_sync.side_effect = RuntimeError("Sync failed with status 500: failure") + with pytest.raises(RuntimeError, match="status 500"): + sync_equity_details.fn(base_url="http://example.com") + + +@patch("tools.flows.training_flow.prepare_training_data") +def test_prepare_data_calls_prepare_training_data(mock_prepare: MagicMock) -> None: + start_date = datetime(2024, 1, 1, tzinfo=UTC) + end_date = datetime(2024, 1, 31, tzinfo=UTC) + mock_prepare.return_value = "s3://artifacts-bucket/training/output.parquet" + result = prepare_data.fn( + data_bucket="data-bucket", + artifacts_bucket="artifacts-bucket", + start_date=start_date, + end_date=end_date, + ) + mock_prepare.assert_called_once() + assert result == "training/output.parquet" + + +@patch("tools.flows.training_flow.prepare_training_data") +def test_prepare_data_passes_output_key(mock_prepare: MagicMock) -> None: + start_date = datetime(2024, 1, 1, tzinfo=UTC) + end_date = datetime(2024, 1, 31, tzinfo=UTC) + mock_prepare.return_value = "s3://artifacts-bucket/custom/key.parquet" + prepare_data.fn( + data_bucket="data-bucket", + artifacts_bucket="artifacts-bucket", + start_date=start_date, + end_date=end_date, + output_key="custom/key.parquet", + ) + call_kwargs = mock_prepare.call_args.kwargs + assert call_kwargs["output_key"] == "custom/key.parquet" + + +@patch("tools.flows.training_flow.boto3") +def test_train_tide_model_downloads_trains_uploads(mock_boto3: MagicMock) -> None: + mock_s3 = MagicMock() + mock_boto3.client.return_value = mock_s3 + + sample_data = pl.DataFrame( + { + "ticker": ["AAPL"], + "timestamp": [1000000], + "open_price": [100.0], + "high_price": [101.0], + "low_price": [99.0], + "close_price": [100.5], + "volume": [1000000], + "volume_weighted_average_price": [100.3], + "sector": ["Technology"], + "industry": ["Software"], + } + ) + parquet_buffer = io.BytesIO() + sample_data.write_parquet(parquet_buffer) + parquet_bytes = parquet_buffer.getvalue() + mock_s3.get_object.return_value = {"Body": MagicMock(read=lambda: parquet_bytes)} + + mock_model = MagicMock() + mock_data = MagicMock() + + with patch("equitypricemodel.trainer.train_model") as mock_train: + mock_train.return_value = (mock_model, mock_data) + result = train_tide_model.fn( + artifacts_bucket="artifacts-bucket", + training_data_key="training/data.parquet", + ) + + assert result.startswith("s3://artifacts-bucket/artifacts/") + mock_s3.get_object.assert_called_once() + mock_s3.put_object.assert_called_once() + mock_train.assert_called_once() + assert "checkpoint_directory" in mock_train.call_args.kwargs + + +@patch("tools.flows.training_flow.train_tide_model", return_value="s3://bucket/model") +@patch("tools.flows.training_flow.prepare_data", return_value="training/data.parquet") +@patch("tools.flows.training_flow.sync_equity_details") +@patch("tools.flows.training_flow.sync_equity_bars") +@patch("tools.flows.training_flow.get_training_date_range") +def test_training_pipeline_threads_data_key( + mock_date_range: MagicMock, + mock_bars: MagicMock, + mock_details: MagicMock, + mock_prepare: MagicMock, + mock_train: MagicMock, +) -> None: + start_date = datetime(2024, 1, 1, tzinfo=UTC) + end_date = datetime(2024, 1, 31, tzinfo=UTC) + mock_date_range.return_value = (start_date, end_date) + + result = training_pipeline.fn( + base_url="http://example.com", + data_bucket="data-bucket", + artifacts_bucket="artifacts-bucket", + lookback_days=LOOKBACK_DAYS, + ) + + mock_date_range.assert_called_once_with(LOOKBACK_DAYS) + mock_bars.assert_called_once_with("http://example.com", start_date, end_date) + mock_details.assert_called_once_with("http://example.com") + mock_prepare.assert_called_once_with( + "data-bucket", + "artifacts-bucket", + start_date, + end_date, + "training/filtered_tide_training_data.parquet", + ) + mock_train.assert_called_once_with( + "artifacts-bucket", + "training/data.parquet", + ) + assert result == "s3://bucket/model" diff --git a/uv.lock b/uv.lock index 0a369cf04..0b0b1715f 100644 --- a/uv.lock +++ b/uv.lock @@ -23,6 +23,29 @@ dev = [ { name = "pytest", specifier = ">=8.3.5" }, ] +[[package]] +name = "aiosqlite" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/8a/64761f4005f17809769d23e518d915db74e6310474e733e3593cfc854ef1/aiosqlite-0.22.1.tar.gz", hash = "sha256:043e0bd78d32888c0a9ca90fc788b38796843360c855a7262a532813133a0650", size = 14821, upload-time = "2025-12-23T19:25:43.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/b7/e3bf5133d697a08128598c8d0abc5e16377b51465a33756de24fa7dee953/aiosqlite-0.22.1-py3-none-any.whl", hash = "sha256:21c002eb13823fad740196c5a2e9d8e62f6243bd9e7e4a1f87fb5e44ecb4fceb", size = 17405, upload-time = "2025-12-23T19:25:42.139Z" }, +] + +[[package]] +name = "alembic" +version = "1.18.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mako" }, + { name = "sqlalchemy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/13/8b084e0f2efb0275a1d534838844926f798bd766566b1375174e2448cd31/alembic-1.18.4.tar.gz", hash = "sha256:cb6e1fd84b6174ab8dbb2329f86d631ba9559dd78df550b57804d607672cedbc", size = 2056725, upload-time = "2026-02-10T16:00:47.195Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/29/6533c317b74f707ea28f8d633734dbda2119bbadfc61b2f3640ba835d0f7/alembic-1.18.4-py3-none-any.whl", hash = "sha256:a5ed4adcf6d8a4cb575f3d759f071b03cd6e5c7618eb796cb52497be25bfe19a", size = 263893, upload-time = "2026-02-10T16:00:49.997Z" }, +] + [[package]] name = "alpaca-py" version = "0.43.2" @@ -41,6 +64,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/25/ea/dac50720ee46f63b0c6339014b28a3dc976b87e314c2418f3ae5ee7e13f0/alpaca_py-0.43.2-py3-none-any.whl", hash = "sha256:ee608d9744b57766dcce60ff88523073fad798a7361c9bf1ec7a499eec5f19e5", size = 122502, upload-time = "2025-11-04T06:14:30.279Z" }, ] +[[package]] +name = "amplitude-analytics" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3c/f3/92e68da6feca11b8d279da0b8f413d35395eef12d243d0c6826924e6f04d/amplitude_analytics-1.2.2.tar.gz", hash = "sha256:ed82b39aa04447e5f156398249d632a5b51591905ec16556c7641a3258d38366", size = 22262, upload-time = "2026-02-17T17:17:09.637Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/c0/1486294b558353b0ee91cac7e876b594b61824e19fcbd4c3f9e7f749a3b8/amplitude_analytics-1.2.2-py3-none-any.whl", hash = "sha256:18f1b6e03cf360fcbea4bffb3a44883f063039ea1622fc85b0da945bdd13f6c6", size = 24660, upload-time = "2026-02-17T17:17:08.8Z" }, +] + [[package]] name = "annotated-doc" version = "0.0.4" @@ -59,12 +91,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] -[[package]] -name = "antlr4-python3-runtime" -version = "4.9.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3e/38/7859ff46355f76f8d19459005ca000b6e7012f2f1ca597746cbcd1fbfe5e/antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b", size = 117034, upload-time = "2021-11-06T17:52:23.524Z" } - [[package]] name = "anyio" version = "4.12.1" @@ -78,6 +104,24 @@ 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 = "apprise" +version = "1.9.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "click" }, + { name = "markdown" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "requests-oauthlib" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/f5/97dc06b3401bb67abcef6e8bef7155f192b75795c2a2aa4d59eb5aa7fa66/apprise-1.9.7.tar.gz", hash = "sha256:2f73cc1e0264fb119fdb9b7cde82e8fde40a0f531ac885d8c6f0cf0f6e13aec2", size = 1937173, upload-time = "2026-01-20T18:51:32.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/6b/cfa80a13437896eb8f4504ddac6dfa4ef7f1d2b2261057aa4a30003b8de6/apprise-1.9.7-py3-none-any.whl", hash = "sha256:c7640a81a1097685de66e0508e3da89f49235d566cb44bbead1dd98419bf5ee3", size = 1459879, upload-time = "2026-01-20T18:51:30.766Z" }, +] + [[package]] name = "arpeggio" version = "2.0.3" @@ -87,6 +131,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/84/4d/53b8186b41842f7a5e971b1d1c28e678364dcf841e4170f5d14d38ac1e2a/Arpeggio-2.0.3-py2.py3-none-any.whl", hash = "sha256:9374d9c531b62018b787635f37fd81c9a6ee69ef2d28c5db3cd18791b1f7db2f", size = 54656, upload-time = "2025-09-12T12:45:17.971Z" }, ] +[[package]] +name = "asgi-lifespan" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/da/e7908b54e0f8043725a990bf625f2041ecf6bfe8eb7b19407f1c00b630f7/asgi-lifespan-2.1.0.tar.gz", hash = "sha256:5e2effaf0bfe39829cf2d64e7ecc47c7d86d676a6599f7afba378c31f5e3a308", size = 15627, upload-time = "2023-03-28T17:35:49.126Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/f5/c36551e93acba41a59939ae6a0fb77ddb3f2e8e8caa716410c65f7341f72/asgi_lifespan-2.1.0-py3-none-any.whl", hash = "sha256:ed840706680e28428c01e14afb3875d7d76d3206f3d5b2f2294e059b5c23804f", size = 10895, upload-time = "2023-03-28T17:35:47.772Z" }, +] + +[[package]] +name = "asyncpg" +version = "0.31.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cc/d18065ce2380d80b1bcce927c24a2642efd38918e33fd724bc4bca904877/asyncpg-0.31.0.tar.gz", hash = "sha256:c989386c83940bfbd787180f2b1519415e2d3d6277a70d9d0f0145ac73500735", size = 993667, upload-time = "2025-11-24T23:27:00.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/a6/59d0a146e61d20e18db7396583242e32e0f120693b67a8de43f1557033e2/asyncpg-0.31.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b44c31e1efc1c15188ef183f287c728e2046abb1d26af4d20858215d50d91fad", size = 662042, upload-time = "2025-11-24T23:25:49.578Z" }, + { url = "https://files.pythonhosted.org/packages/36/01/ffaa189dcb63a2471720615e60185c3f6327716fdc0fc04334436fbb7c65/asyncpg-0.31.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0c89ccf741c067614c9b5fc7f1fc6f3b61ab05ae4aaa966e6fd6b93097c7d20d", size = 638504, upload-time = "2025-11-24T23:25:51.501Z" }, + { url = "https://files.pythonhosted.org/packages/9f/62/3f699ba45d8bd24c5d65392190d19656d74ff0185f42e19d0bbd973bb371/asyncpg-0.31.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:12b3b2e39dc5470abd5e98c8d3373e4b1d1234d9fbdedf538798b2c13c64460a", size = 3426241, upload-time = "2025-11-24T23:25:53.278Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d1/a867c2150f9c6e7af6462637f613ba67f78a314b00db220cd26ff559d532/asyncpg-0.31.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:aad7a33913fb8bcb5454313377cc330fbb19a0cd5faa7272407d8a0c4257b671", size = 3520321, upload-time = "2025-11-24T23:25:54.982Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1a/cce4c3f246805ecd285a3591222a2611141f1669d002163abef999b60f98/asyncpg-0.31.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3df118d94f46d85b2e434fd62c84cb66d5834d5a890725fe625f498e72e4d5ec", size = 3316685, upload-time = "2025-11-24T23:25:57.43Z" }, + { url = "https://files.pythonhosted.org/packages/40/ae/0fc961179e78cc579e138fad6eb580448ecae64908f95b8cb8ee2f241f67/asyncpg-0.31.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd5b6efff3c17c3202d4b37189969acf8927438a238c6257f66be3c426beba20", size = 3471858, upload-time = "2025-11-24T23:25:59.636Z" }, + { url = "https://files.pythonhosted.org/packages/52/b2/b20e09670be031afa4cbfabd645caece7f85ec62d69c312239de568e058e/asyncpg-0.31.0-cp312-cp312-win32.whl", hash = "sha256:027eaa61361ec735926566f995d959ade4796f6a49d3bde17e5134b9964f9ba8", size = 527852, upload-time = "2025-11-24T23:26:01.084Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f0/f2ed1de154e15b107dc692262395b3c17fc34eafe2a78fc2115931561730/asyncpg-0.31.0-cp312-cp312-win_amd64.whl", hash = "sha256:72d6bdcbc93d608a1158f17932de2321f68b1a967a13e014998db87a72ed3186", size = 597175, upload-time = "2025-11-24T23:26:02.564Z" }, +] + [[package]] name = "attrs" version = "25.4.0" @@ -96,6 +168,15 @@ 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 = "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 = "behave" version = "1.3.3" @@ -144,6 +225,9 @@ wheels = [ s3 = [ { name = "mypy-boto3-s3" }, ] +ssm = [ + { name = "mypy-boto3-ssm" }, +] [[package]] name = "botocore" @@ -171,6 +255,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/76/cab7af7f16c0b09347f2ebe7ffda7101132f786acb767666dce43055faab/botocore_stubs-1.42.41-py3-none-any.whl", hash = "sha256:9423110fb0e391834bd2ed44ae5f879d8cb370a444703d966d30842ce2bcb5f0", size = 66759, upload-time = "2026-02-03T20:46:13.02Z" }, ] +[[package]] +name = "cachetools" +version = "7.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/07/56595285564e90777d758ebd383d6b0b971b87729bbe2184a849932a3736/cachetools-7.0.1.tar.gz", hash = "sha256:e31e579d2c5b6e2944177a0397150d312888ddf4e16e12f1016068f0c03b8341", size = 36126, upload-time = "2026-02-10T22:24:05.03Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/9e/5faefbf9db1db466d633735faceda1f94aa99ce506ac450d232536266b32/cachetools-7.0.1-py3-none-any.whl", hash = "sha256:8f086515c254d5664ae2146d14fc7f65c9a4bce75152eb247e5a9c5e6d7b2ecf", size = 13484, upload-time = "2026-02-10T22:24:03.741Z" }, +] + [[package]] name = "certifi" version = "2026.1.4" @@ -180,6 +273,29 @@ 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 = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, +] + [[package]] name = "charset-normalizer" version = "3.4.4" @@ -235,6 +351,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "coolname" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/57/03591c9dda2f394db126db86c9eb47d37006174d8aaa712e60e1d2376ade/coolname-4.0.0.tar.gz", hash = "sha256:168ec04acbd58c4b7db39c5988a0e9daa204631dc975367adeb762b2c880d767", size = 38143, upload-time = "2026-02-22T14:10:17.655Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/96/a4501854ec7178a8c1ccafa457f44b1a28b25b816a7407ba71b816216f32/coolname-4.0.0-py3-none-any.whl", hash = "sha256:6eb1d5471b40b718d26ae7f25466e76fd54eb27f816d67051a9cc4f3f690f940", size = 39761, upload-time = "2026-02-22T14:10:15.955Z" }, +] + [[package]] name = "coverage" version = "7.13.4" @@ -259,6 +384,58 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" }, ] +[[package]] +name = "croniter" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "pytz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/2f/44d1ae153a0e27be56be43465e5cb39b9650c781e001e7864389deb25090/croniter-6.0.0.tar.gz", hash = "sha256:37c504b313956114a983ece2c2b07790b1f1094fe9d81cc94739214748255577", size = 64481, upload-time = "2024-12-17T17:17:47.32Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/4b/290b4c3efd6417a8b0c284896de19b1d5855e6dbdb97d2a35e68fa42de85/croniter-6.0.0-py2.py3-none-any.whl", hash = "sha256:2f878c3856f17896979b2a4379ba1f09c83e374931ea15cc835c5dd2eee9b368", size = 25468, upload-time = "2024-12-17T17:17:45.359Z" }, +] + +[[package]] +name = "cryptography" +version = "46.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" }, + { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, + { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, + { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, + { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, + { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, + { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, + { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, + { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" }, + { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, + { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, + { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, + { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, + { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, + { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, + { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, + { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, + { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, + { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" }, + { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, +] + [[package]] name = "cucumber-expressions" version = "19.0.0" @@ -277,6 +454,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6b/cf/8e8d034f7d55fceb2e4765bf9fab5da6d6a09204cd09de7bb5054f242cd0/cucumber_tag_expressions-9.1.0-py3-none-any.whl", hash = "sha256:cca145d677a942c1877e5a2cf13da8c6ec99260988877c817efd284d8455bb56", size = 9726, upload-time = "2026-02-17T21:59:04.755Z" }, ] +[[package]] +name = "dateparser" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "regex" }, + { name = "tzlocal" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/2c/668dfb8c073a5dde3efb80fa382de1502e3b14002fd386a8c1b0b49e92a9/dateparser-1.3.0.tar.gz", hash = "sha256:5bccf5d1ec6785e5be71cc7ec80f014575a09b4923e762f850e57443bddbf1a5", size = 337152, upload-time = "2026-02-04T16:00:06.162Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/c7/95349670e193b2891176e1b8e5f43e12b31bff6d9994f70e74ab385047f6/dateparser-1.3.0-py3-none-any.whl", hash = "sha256:8dc678b0a526e103379f02ae44337d424bd366aac727d3c6cf52ce1b01efbb5a", size = 318688, upload-time = "2026-02-04T16:00:04.652Z" }, +] + [[package]] name = "debugpy" version = "1.8.20" @@ -333,7 +525,7 @@ dependencies = [ [package.dev-dependencies] dev = [ - { name = "boto3-stubs", extra = ["s3"] }, + { name = "boto3-stubs", extra = ["s3", "ssm"] }, ] [package.metadata] @@ -352,7 +544,37 @@ requires-dist = [ ] [package.metadata.requires-dev] -dev = [{ name = "boto3-stubs", extras = ["s3"], specifier = ">=1.38.0" }] +dev = [{ name = "boto3-stubs", extras = ["s3", "ssm"], specifier = ">=1.38.0" }] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +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 = "fakeredis" +version = "2.34.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "redis" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/40/fd09efa66205eb32253d2b2ebc63537281384d2040f0a88bcd2289e120e4/fakeredis-2.34.1.tar.gz", hash = "sha256:4ff55606982972eecce3ab410e03d746c11fe5deda6381d913641fbd8865ea9b", size = 177315, upload-time = "2026-02-25T13:17:51.315Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/b5/82f89307d0d769cd9bf46a54fb9136be08e4e57c5570ae421db4c9a2ba62/fakeredis-2.34.1-py3-none-any.whl", hash = "sha256:0107ec99d48913e7eec2a5e3e2403d1bd5f8aa6489d1a634571b975289c48f12", size = 122160, upload-time = "2026-02-25T13:17:49.701Z" }, +] + +[package.optional-dependencies] +lua = [ + { name = "lupa" }, +] [[package]] name = "fastapi" @@ -371,51 +593,70 @@ wheels = [ ] [[package]] -name = "google-pasta" -version = "0.2.0" +name = "fsspec" +version = "2026.2.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/35/4a/0bd53b36ff0323d10d5f24ebd67af2de10a1117f5cf4d7add90df92756f1/google-pasta-0.2.0.tar.gz", hash = "sha256:c9f2c8dfc8f96d0d5808299920721be30c9eec37f2389f28904f454565c8a16e", size = 40430, upload-time = "2020-03-13T18:57:50.34Z" } +sdist = { url = "https://files.pythonhosted.org/packages/51/7c/f60c259dcbf4f0c47cc4ddb8f7720d2dcdc8888c8e5ad84c73ea4531cc5b/fsspec-2026.2.0.tar.gz", hash = "sha256:6544e34b16869f5aacd5b90bdf1a71acb37792ea3ddf6125ee69a22a53fb8bff", size = 313441, upload-time = "2026-02-05T21:50:53.743Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/de/c648ef6835192e6e2cc03f40b19eeda4382c49b5bafb43d88b931c4c74ac/google_pasta-0.2.0-py3-none-any.whl", hash = "sha256:b32482794a366b5366a32c92a9a9201b107821889935a02b3e51f6b432ea84ed", size = 57471, upload-time = "2020-03-13T18:57:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" }, ] [[package]] -name = "graphene" -version = "3.4.3" +name = "graphviz" +version = "0.21" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "graphql-core" }, - { name = "graphql-relay" }, - { name = "python-dateutil" }, - { name = "typing-extensions" }, +sdist = { url = "https://files.pythonhosted.org/packages/f8/b3/3ac91e9be6b761a4b30d66ff165e54439dcd48b83f4e20d644867215f6ca/graphviz-0.21.tar.gz", hash = "sha256:20743e7183be82aaaa8ad6c93f8893c923bd6658a04c32ee115edb3c8a835f78", size = 200434, upload-time = "2025-06-15T09:35:05.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl", hash = "sha256:54f33de9f4f911d7e84e4191749cac8cc5653f815b06738c54db9a15ab8b1e42", size = 47300, upload-time = "2025-06-15T09:35:04.433Z" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cc/f6/bf62ff950c317ed03e77f3f6ddd7e34aaa98fe89d79ebd660c55343d8054/graphene-3.4.3.tar.gz", hash = "sha256:2a3786948ce75fe7e078443d37f609cbe5bb36ad8d6b828740ad3b95ed1a0aaa", size = 44739, upload-time = "2024-11-09T20:44:25.757Z" } + +[[package]] +name = "greenlet" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/66/e0/61d8e98007182e6b2aca7cf65904721fb2e4bce0192272ab9cb6f69d8812/graphene-3.4.3-py2.py3-none-any.whl", hash = "sha256:820db6289754c181007a150db1f7fff544b94142b556d12e3ebc777a7bf36c71", size = 114894, upload-time = "2024-11-09T20:44:23.851Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" }, + { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" }, + { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c5/cc09412a29e43406eba18d61c70baa936e299bc27e074e2be3806ed29098/greenlet-3.3.2-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb", size = 626250, upload-time = "2026-02-20T21:02:46.596Z" }, + { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" }, + { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" }, + { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/cc802e067d02af8b60b6771cea7d57e21ef5e6659912814babb42b864713/greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f", size = 231081, upload-time = "2026-02-20T20:17:28.121Z" }, + { url = "https://files.pythonhosted.org/packages/58/2e/fe7f36ff1982d6b10a60d5e0740c759259a7d6d2e1dc41da6d96de32fff6/greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643", size = 230331, upload-time = "2026-02-20T20:17:23.34Z" }, ] [[package]] -name = "graphql-core" -version = "3.2.7" +name = "griffe" +version = "2.0.0" 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" } +dependencies = [ + { name = "griffecli" }, + { name = "griffelib" }, +] 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" }, + { url = "https://files.pythonhosted.org/packages/8b/94/ee21d41e7eb4f823b94603b9d40f86d3c7fde80eacc2c3c71845476dddaa/griffe-2.0.0-py3-none-any.whl", hash = "sha256:5418081135a391c3e6e757a7f3f156f1a1a746cc7b4023868ff7d5e2f9a980aa", size = 5214, upload-time = "2026-02-09T19:09:44.105Z" }, ] [[package]] -name = "graphql-relay" -version = "3.2.0" +name = "griffecli" +version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "graphql-core" }, + { name = "colorama" }, + { name = "griffelib" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ed/d93f7a447bbf7a935d8868e9617cbe1cadf9ee9ee6bd275d3040fbf93d60/griffecli-2.0.0-py3-none-any.whl", hash = "sha256:9f7cd9ee9b21d55e91689358978d2385ae65c22f307a63fb3269acf3f21e643d", size = 9345, upload-time = "2026-02-09T19:09:42.554Z" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d1/13/98fbf8d67552f102488ffc16c6f559ce71ea15f6294728d33928ab5ff14d/graphql-relay-3.2.0.tar.gz", hash = "sha256:1ff1c51298356e481a0be009ccdff249832ce53f30559c1338f22a0e0d17250c", size = 50027, upload-time = "2022-04-16T11:03:45.447Z" } + +[[package]] +name = "griffelib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/74/16/a4cf06adbc711bd364a73ce043b0b08d8fa5aae3df11b6ee4248bcdad2e0/graphql_relay-3.2.0-py3-none-any.whl", hash = "sha256:c9b22bd28b170ba1fe674c74384a8ff30a76c8e26f88ac3aa1584dd3179953e5", size = 16940, upload-time = "2022-04-16T11:03:43.895Z" }, + { url = "https://files.pythonhosted.org/packages/4d/51/c936033e16d12b627ea334aaaaf42229c37620d0f15593456ab69ab48161/griffelib-2.0.0-py3-none-any.whl", hash = "sha256:01284878c966508b6d6f1dbff9b6fa607bc062d8261c5c7253cb285b06422a7f", size = 142004, upload-time = "2026-02-09T19:09:40.561Z" }, ] [[package]] @@ -448,6 +689,28 @@ 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 = "h2" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hpack" }, + { name = "hyperframe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026, upload-time = "2025-08-23T18:12:19.778Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" }, +] + +[[package]] +name = "hpack" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276, upload-time = "2025-01-22T21:44:58.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357, upload-time = "2025-01-22T21:44:56.92Z" }, +] + [[package]] name = "httpcore" version = "1.0.9" @@ -476,6 +739,29 @@ 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.optional-dependencies] +http2 = [ + { name = "h2" }, +] + +[[package]] +name = "humanize" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/66/a3921783d54be8a6870ac4ccffcd15c4dc0dd7fcce51c6d63b8c63935276/humanize-4.15.0.tar.gz", hash = "sha256:1dd098483eb1c7ee8e32eb2e99ad1910baefa4b75c3aff3a82f4d78688993b10", size = 83599, upload-time = "2025-12-20T20:16:13.19Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/7b/bca5613a0c3b542420cf92bd5e5fb8ebd5435ce1011a091f66bb7693285e/humanize-4.15.0-py3-none-any.whl", hash = "sha256:b1186eb9f5a9749cd9cb8565aee77919dd7c8d076161cf44d70e59e3301e1769", size = 132203, upload-time = "2025-12-20T20:16:11.67Z" }, +] + +[[package]] +name = "hyperframe" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -546,6 +832,31 @@ requires-dist = [ { name = "polars", specifier = ">=1.29.0" }, ] +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jinja2-humanize-extension" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "humanize" }, + { name = "jinja2" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/77/0bba383819dd4e67566487c11c49479ced87e77c3285d8e7f7a3401cf882/jinja2_humanize_extension-0.4.0.tar.gz", hash = "sha256:e7d69b1c20f32815bbec722330ee8af14b1287bb1c2b0afa590dbf031cadeaa0", size = 4746, upload-time = "2023-09-01T12:52:42.781Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/b4/08c9d297edd5e1182506edecccbb88a92e1122a057953068cadac420ca5d/jinja2_humanize_extension-0.4.0-py3-none-any.whl", hash = "sha256:b6326e2da0f7d425338bebf58848e830421defbce785f12ae812e65128518156", size = 4769, upload-time = "2023-09-01T12:52:41.098Z" }, +] + [[package]] name = "jmespath" version = "1.1.0" @@ -555,6 +866,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, ] +[[package]] +name = "jsonpatch" +version = "1.33" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpointer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" }, +] + +[[package]] +name = "jsonpointer" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114, upload-time = "2024-06-10T19:24:42.462Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" }, +] + [[package]] name = "jsonschema" version = "4.26.0" @@ -582,6 +914,46 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, ] +[[package]] +name = "lupa" +version = "2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/1c/191c3e6ec6502e3dbe25a53e27f69a5daeac3e56de1f73c0138224171ead/lupa-2.6.tar.gz", hash = "sha256:9a770a6e89576be3447668d7ced312cd6fd41d3c13c2462c9dc2c2ab570e45d9", size = 7240282, upload-time = "2025-10-24T07:20:29.738Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/86/ce243390535c39d53ea17ccf0240815e6e457e413e40428a658ea4ee4b8d/lupa-2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47ce718817ef1cc0c40d87c3d5ae56a800d61af00fbc0fad1ca9be12df2f3b56", size = 951707, upload-time = "2025-10-24T07:18:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/cedea5e6cbeb54396fdcc55f6b741696f3f036d23cfaf986d50d680446da/lupa-2.6-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7aba985b15b101495aa4b07112cdc08baa0c545390d560ad5cfde2e9e34f4d58", size = 1916703, upload-time = "2025-10-24T07:18:05.6Z" }, + { url = "https://files.pythonhosted.org/packages/24/be/3d6b5f9a8588c01a4d88129284c726017b2089f3a3fd3ba8bd977292fea0/lupa-2.6-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:b766f62f95b2739f2248977d29b0722e589dcf4f0ccfa827ccbd29f0148bd2e5", size = 985152, upload-time = "2025-10-24T07:18:08.561Z" }, + { url = "https://files.pythonhosted.org/packages/eb/23/9f9a05beee5d5dce9deca4cb07c91c40a90541fc0a8e09db4ee670da550f/lupa-2.6-cp312-cp312-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:00a934c23331f94cb51760097ebfab14b005d55a6b30a2b480e3c53dd2fa290d", size = 1159599, upload-time = "2025-10-24T07:18:10.346Z" }, + { url = "https://files.pythonhosted.org/packages/40/4e/e7c0583083db9d7f1fd023800a9767d8e4391e8330d56c2373d890ac971b/lupa-2.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21de9f38bd475303e34a042b7081aabdf50bd9bafd36ce4faea2f90fd9f15c31", size = 1038686, upload-time = "2025-10-24T07:18:12.112Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/5a4f7d959d4feba5e203ff0c31889e74d1ca3153122be4a46dca7d92bf7c/lupa-2.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf3bda96d3fc41237e964a69c23647d50d4e28421111360274d4799832c560e9", size = 2071956, upload-time = "2025-10-24T07:18:14.572Z" }, + { url = "https://files.pythonhosted.org/packages/92/34/2f4f13ca65d01169b1720176aedc4af17bc19ee834598c7292db232cb6dc/lupa-2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a76ead245da54801a81053794aa3975f213221f6542d14ec4b859ee2e7e0323", size = 1057199, upload-time = "2025-10-24T07:18:16.379Z" }, + { url = "https://files.pythonhosted.org/packages/35/2a/5f7d2eebec6993b0dcd428e0184ad71afb06a45ba13e717f6501bfed1da3/lupa-2.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8dd0861741caa20886ddbda0a121d8e52fb9b5bb153d82fa9bba796962bf30e8", size = 1173693, upload-time = "2025-10-24T07:18:18.153Z" }, + { url = "https://files.pythonhosted.org/packages/e4/29/089b4d2f8e34417349af3904bb40bec40b65c8731f45e3fd8d497ca573e5/lupa-2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:239e63948b0b23023f81d9a19a395e768ed3da6a299f84e7963b8f813f6e3f9c", size = 2164394, upload-time = "2025-10-24T07:18:20.403Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1b/79c17b23c921f81468a111cad843b076a17ef4b684c4a8dff32a7969c3f0/lupa-2.6-cp312-cp312-win32.whl", hash = "sha256:325894e1099499e7a6f9c351147661a2011887603c71086d36fe0f964d52d1ce", size = 1420647, upload-time = "2025-10-24T07:18:23.368Z" }, + { url = "https://files.pythonhosted.org/packages/b8/15/5121e68aad3584e26e1425a5c9a79cd898f8a152292059e128c206ee817c/lupa-2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c735a1ce8ee60edb0fe71d665f1e6b7c55c6021f1d340eb8c865952c602cd36f", size = 1688529, upload-time = "2025-10-24T07:18:25.523Z" }, +] + +[[package]] +name = "mako" +version = "1.3.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, +] + +[[package]] +name = "markdown" +version = "3.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, +] + [[package]] name = "markdown-it-py" version = "4.0.0" @@ -595,21 +967,31 @@ wheels = [ ] [[package]] -name = "mdurl" -version = "0.1.2" +name = "markupsafe" +version = "3.0.3" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, ] [[package]] -name = "mock" -version = "4.0.3" +name = "mdurl" +version = "0.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e2/be/3ea39a8fd4ed3f9a25aae18a1bff2df7a610bca93c8ede7475e32d8b73a0/mock-4.0.3.tar.gz", hash = "sha256:7d3fbbde18228f4ff2f1f119a45cdffa458b4c0dee32eb4d2bb2f82554bac7bc", size = 72316, upload-time = "2020-12-10T07:33:13.043Z" } +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/5c/03/b7e605db4a57c0f6fba744b11ef3ddf4ddebcada35022927a2b5fc623fdf/mock-4.0.3-py3-none-any.whl", hash = "sha256:122fcb64ee37cfad5b3f48d7a7d51875d7031aaf3d8be7c42e2bee25044eee62", size = 28536, upload-time = "2020-12-10T07:33:11.564Z" }, + { 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]] @@ -630,27 +1012,21 @@ wheels = [ ] [[package]] -name = "multiprocess" -version = "0.70.19" +name = "mypy-boto3-s3" +version = "1.42.37" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "dill" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a2/f2/e783ac7f2aeeed14e9e12801f22529cc7e6b7ab80928d6dcce4e9f00922d/multiprocess-0.70.19.tar.gz", hash = "sha256:952021e0e6c55a4a9fe4cd787895b86e239a40e76802a789d6305398d3975897", size = 2079989, upload-time = "2026-01-19T06:47:39.744Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/41/44066f4cd3421bacb6aad4ec7b1da8d0f8858560e526166db64d95fa7ad7/mypy_boto3_s3-1.42.37.tar.gz", hash = "sha256:628a4652f727870a07e1c3854d6f30dc545a7dd5a4b719a2c59c32a95d92e4c1", size = 76317, upload-time = "2026-01-28T20:51:52.971Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/45/8004d1e6b9185c1a444d6b55ac5682acf9d98035e54386d967366035a03a/multiprocess-0.70.19-py310-none-any.whl", hash = "sha256:97404393419dcb2a8385910864eedf47a3cadf82c66345b44f036420eb0b5d87", size = 134948, upload-time = "2026-01-19T06:47:32.325Z" }, - { url = "https://files.pythonhosted.org/packages/86/c2/dec9722dc3474c164a0b6bcd9a7ed7da542c98af8cabce05374abab35edd/multiprocess-0.70.19-py311-none-any.whl", hash = "sha256:928851ae7973aea4ce0eaf330bbdafb2e01398a91518d5c8818802845564f45c", size = 144457, upload-time = "2026-01-19T06:47:33.711Z" }, - { url = "https://files.pythonhosted.org/packages/71/70/38998b950a97ea279e6bd657575d22d1a2047256caf707d9a10fbce4f065/multiprocess-0.70.19-py312-none-any.whl", hash = "sha256:3a56c0e85dd5025161bac5ce138dcac1e49174c7d8e74596537e729fd5c53c28", size = 150281, upload-time = "2026-01-19T06:47:35.037Z" }, - { url = "https://files.pythonhosted.org/packages/7e/82/69e539c4c2027f1e1697e09aaa2449243085a0edf81ae2c6341e84d769b6/multiprocess-0.70.19-py39-none-any.whl", hash = "sha256:0d4b4397ed669d371c81dcd1ef33fd384a44d6c3de1bd0ca7ac06d837720d3c5", size = 133477, upload-time = "2026-01-19T06:47:38.619Z" }, + { url = "https://files.pythonhosted.org/packages/94/06/cb6050ecd72f5fa449bac80ad1a4711719367c4f545201317f36e3999784/mypy_boto3_s3-1.42.37-py3-none-any.whl", hash = "sha256:7c118665f3f583dbfde1013ce47908749f9d2a760f28f59ec65732306ee9cec9", size = 83439, upload-time = "2026-01-28T20:51:49.99Z" }, ] [[package]] -name = "mypy-boto3-s3" -version = "1.42.37" +name = "mypy-boto3-ssm" +version = "1.42.54" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e6/41/44066f4cd3421bacb6aad4ec7b1da8d0f8858560e526166db64d95fa7ad7/mypy_boto3_s3-1.42.37.tar.gz", hash = "sha256:628a4652f727870a07e1c3854d6f30dc545a7dd5a4b719a2c59c32a95d92e4c1", size = 76317, upload-time = "2026-01-28T20:51:52.971Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/e9/cde8a9fe2bf061e595256e5542f4c803efdcb2f741611bcae9763f2af993/mypy_boto3_ssm-1.42.54.tar.gz", hash = "sha256:f4bc19a08635757808b66ef94a5b52c3729da998587745962626e60606a1be2c", size = 94255, upload-time = "2026-02-20T20:49:58.148Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/06/cb6050ecd72f5fa449bac80ad1a4711719367c4f545201317f36e3999784/mypy_boto3_s3-1.42.37-py3-none-any.whl", hash = "sha256:7c118665f3f583dbfde1013ce47908749f9d2a760f28f59ec65732306ee9cec9", size = 83439, upload-time = "2026-01-28T20:51:49.99Z" }, + { url = "https://files.pythonhosted.org/packages/ef/54/58fa9dba05049adbfd5ecbd755a2e730c118d682367d885e8b35e9b9f0cf/mypy_boto3_ssm-1.42.54-py3-none-any.whl", hash = "sha256:dfd70aa5f60be70437b53482fa6e183bafe922598a50fc6c51f6ad3bd70d8c04", size = 95951, upload-time = "2026-02-20T20:49:54.202Z" }, ] [[package]] @@ -682,16 +1058,48 @@ wheels = [ ] [[package]] -name = "omegaconf" -version = "2.3.0" +name = "oauthlib" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.39.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "antlr4-python3-runtime" }, - { name = "pyyaml" }, + { name = "importlib-metadata" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/09/48/6388f1bb9da707110532cb70ec4d2822858ddfb44f1cdf1233c20a80ea4b/omegaconf-2.3.0.tar.gz", hash = "sha256:d5d4b6d29955cc50ad50c46dc269bcd92c6e00f5f90d23ab5fee7bfca4ba4cc7", size = 3298120, upload-time = "2022-12-08T20:59:22.753Z" } +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/e3/94/1843518e420fa3ed6919835845df698c7e27e183cb997394e4a670973a65/omegaconf-2.3.0-py3-none-any.whl", hash = "sha256:7b4df175cdb08ba400f45cae3bdcae7ba8365db4d165fc65fd04b050ab63b46b", size = 79500, upload-time = "2022-12-08T20:59:19.686Z" }, + { 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 = "orjson" +version = "3.11.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/45/b268004f745ede84e5798b48ee12b05129d19235d0e15267aa57dcdb400b/orjson-3.11.7.tar.gz", hash = "sha256:9b1a67243945819ce55d24a30b59d6a168e86220452d2c96f4d1f093e71c0c49", size = 6144992, upload-time = "2026-02-02T15:38:49.29Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/bf/76f4f1665f6983385938f0e2a5d7efa12a58171b8456c252f3bae8a4cf75/orjson-3.11.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bd03ea7606833655048dab1a00734a2875e3e86c276e1d772b2a02556f0d895f", size = 228545, upload-time = "2026-02-02T15:37:46.376Z" }, + { url = "https://files.pythonhosted.org/packages/79/53/6c72c002cb13b5a978a068add59b25a8bdf2800ac1c9c8ecdb26d6d97064/orjson-3.11.7-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:89e440ebc74ce8ab5c7bc4ce6757b4a6b1041becb127df818f6997b5c71aa60b", size = 125224, upload-time = "2026-02-02T15:37:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/2c/83/10e48852865e5dd151bdfe652c06f7da484578ed02c5fca938e3632cb0b8/orjson-3.11.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ede977b5fe5ac91b1dffc0a517ca4542d2ec8a6a4ff7b2652d94f640796342a", size = 128154, upload-time = "2026-02-02T15:37:48.954Z" }, + { url = "https://files.pythonhosted.org/packages/6e/52/a66e22a2b9abaa374b4a081d410edab6d1e30024707b87eab7c734afe28d/orjson-3.11.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b7b1dae39230a393df353827c855a5f176271c23434cfd2db74e0e424e693e10", size = 123548, upload-time = "2026-02-02T15:37:50.187Z" }, + { url = "https://files.pythonhosted.org/packages/de/38/605d371417021359f4910c496f764c48ceb8997605f8c25bf1dfe58c0ebe/orjson-3.11.7-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed46f17096e28fb28d2975834836a639af7278aa87c84f68ab08fbe5b8bd75fa", size = 129000, upload-time = "2026-02-02T15:37:51.426Z" }, + { url = "https://files.pythonhosted.org/packages/44/98/af32e842b0ffd2335c89714d48ca4e3917b42f5d6ee5537832e069a4b3ac/orjson-3.11.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3726be79e36e526e3d9c1aceaadbfb4a04ee80a72ab47b3f3c17fefb9812e7b8", size = 141686, upload-time = "2026-02-02T15:37:52.607Z" }, + { url = "https://files.pythonhosted.org/packages/96/0b/fc793858dfa54be6feee940c1463370ece34b3c39c1ca0aa3845f5ba9892/orjson-3.11.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0724e265bc548af1dedebd9cb3d24b4e1c1e685a343be43e87ba922a5c5fff2f", size = 130812, upload-time = "2026-02-02T15:37:53.944Z" }, + { url = "https://files.pythonhosted.org/packages/dc/91/98a52415059db3f374757d0b7f0f16e3b5cd5976c90d1c2b56acaea039e6/orjson-3.11.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7745312efa9e11c17fbd3cb3097262d079da26930ae9ae7ba28fb738367cbad", size = 133440, upload-time = "2026-02-02T15:37:55.615Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/cb540117bda61791f46381f8c26c8f93e802892830a6055748d3bb1925ab/orjson-3.11.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f904c24bdeabd4298f7a977ef14ca2a022ca921ed670b92ecd16ab6f3d01f867", size = 138386, upload-time = "2026-02-02T15:37:56.814Z" }, + { url = "https://files.pythonhosted.org/packages/63/1a/50a3201c334a7f17c231eee5f841342190723794e3b06293f26e7cf87d31/orjson-3.11.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b9fc4d0f81f394689e0814617aadc4f2ea0e8025f38c226cbf22d3b5ddbf025d", size = 408853, upload-time = "2026-02-02T15:37:58.291Z" }, + { url = "https://files.pythonhosted.org/packages/87/cd/8de1c67d0be44fdc22701e5989c0d015a2adf391498ad42c4dc589cd3013/orjson-3.11.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:849e38203e5be40b776ed2718e587faf204d184fc9a008ae441f9442320c0cab", size = 144130, upload-time = "2026-02-02T15:38:00.163Z" }, + { url = "https://files.pythonhosted.org/packages/0f/fe/d605d700c35dd55f51710d159fc54516a280923cd1b7e47508982fbb387d/orjson-3.11.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4682d1db3bcebd2b64757e0ddf9e87ae5f00d29d16c5cdf3a62f561d08cc3dd2", size = 134818, upload-time = "2026-02-02T15:38:01.507Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e4/15ecc67edb3ddb3e2f46ae04475f2d294e8b60c1825fbe28a428b93b3fbd/orjson-3.11.7-cp312-cp312-win32.whl", hash = "sha256:f4f7c956b5215d949a1f65334cf9d7612dde38f20a95f2315deef167def91a6f", size = 127923, upload-time = "2026-02-02T15:38:02.75Z" }, + { url = "https://files.pythonhosted.org/packages/34/70/2e0855361f76198a3965273048c8e50a9695d88cd75811a5b46444895845/orjson-3.11.7-cp312-cp312-win_amd64.whl", hash = "sha256:bf742e149121dc5648ba0a08ea0871e87b660467ef168a3a5e53bc1fbd64bb74", size = 125007, upload-time = "2026-02-02T15:38:04.032Z" }, + { url = "https://files.pythonhosted.org/packages/68/40/c2051bd19fc467610fed469dc29e43ac65891571138f476834ca192bc290/orjson-3.11.7-cp312-cp312-win_arm64.whl", hash = "sha256:26c3b9132f783b7d7903bf1efb095fed8d4a3a85ec0d334ee8beff3d7a4749d5", size = 126089, upload-time = "2026-02-02T15:38:05.297Z" }, ] [[package]] @@ -802,18 +1210,35 @@ wheels = [ ] [[package]] -name = "pathos" -version = "0.3.5" +name = "pathspec" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, +] + +[[package]] +name = "pendulum" +version = "3.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "dill" }, - { name = "multiprocess" }, - { name = "pox" }, - { name = "ppft" }, + { name = "python-dateutil" }, + { name = "tzdata" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/32/37/0c730979d3890f8096a86af2683fac74edd4d15cb037391098dca70dcb1d/pathos-0.3.5.tar.gz", hash = "sha256:8fe041b8545c5d3880a038f866022bdebf935e5cf68f56ed3407cb7e65193a61", size = 166975, upload-time = "2026-01-20T00:06:57.848Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/72/9a51afa0a822b09e286c4cb827ed7b00bc818dac7bd11a5f161e493a217d/pendulum-3.2.0.tar.gz", hash = "sha256:e80feda2d10fa3ff8b1526715f7d33dcb7e08494b3088f2c8a3ac92d4a4331ce", size = 86912, upload-time = "2026-01-30T11:22:24.093Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/37/44/be2146c650ee9ca4d9a770c995f5c92c1ea52292dcf618ce1a336d3146dd/pathos-0.3.5-py3-none-any.whl", hash = "sha256:c95b04103c40a16c08db69cd4b5c52624d55208beadf1348681edece809ec4f8", size = 82248, upload-time = "2026-01-20T00:06:56.291Z" }, + { url = "https://files.pythonhosted.org/packages/41/56/dd0ea9f97d25a0763cda09e2217563b45714786118d8c68b0b745395d6eb/pendulum-3.2.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bf0b489def51202a39a2a665dcc4162d5e46934a740fe4c4fe3068979610156c", size = 337830, upload-time = "2026-01-30T11:21:08.298Z" }, + { url = "https://files.pythonhosted.org/packages/cf/98/83d62899bf7226fc12396de4bc1fb2b5da27e451c7c60790043aaf8b4731/pendulum-3.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:937a529aa302efa18dcf25e53834964a87ffb2df8f80e3669ab7757a6126beaf", size = 327574, upload-time = "2026-01-30T11:21:09.715Z" }, + { url = "https://files.pythonhosted.org/packages/76/fa/ff2aa992b23f0543c709b1a3f3f9ed760ec71fd02c8bb01f93bf008b52e4/pendulum-3.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85c7689defc65c4dc29bf257f7cca55d210fabb455de9476e1748d2ab2ae80d7", size = 339891, upload-time = "2026-01-30T11:21:11.089Z" }, + { url = "https://files.pythonhosted.org/packages/c5/4e/25b4fa11d19503d50d7b52d7ef943c0f20fd54422aaeb9e38f588c815c50/pendulum-3.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e216e5a412563ea2ecf5de467dcf3d02717947fcdabe6811d5ee360726b02b", size = 373726, upload-time = "2026-01-30T11:21:12.493Z" }, + { url = "https://files.pythonhosted.org/packages/4f/30/0acad6396c4e74e5c689aa4f0b0c49e2ecdcfce368e7b5bf35ca1c0fc61a/pendulum-3.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a2af22eeec438fbaac72bb7fba783e0950a514fba980d9a32db394b51afccec", size = 379827, upload-time = "2026-01-30T11:21:14.08Z" }, + { url = "https://files.pythonhosted.org/packages/3a/f7/e6a2fdf2a23d59b4b48b8fa89e8d4bf2dd371aea2c6ba8fcecec20a4acb9/pendulum-3.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3159cceb54f5aa8b85b141c7f0ce3fac8bdd1ffdc7c79e67dca9133eac7c4d11", size = 348921, upload-time = "2026-01-30T11:21:15.816Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f2/c15fa7f9ad4e181aa469b6040b574988bd108ccdf4ae509ad224f9e4db44/pendulum-3.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c39ea5e9ffa20ea8bae986d00e0908bd537c8468b71d6b6503ab0b4c3d76e0ea", size = 517188, upload-time = "2026-01-30T11:21:17.835Z" }, + { url = "https://files.pythonhosted.org/packages/47/c7/5f80b12ee88ec26e930c3a5a602608a63c29cf60c81a0eb066d583772550/pendulum-3.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e5afc753e570cce1f44197676371f68953f7d4f022303d141bb09f804d5fe6d7", size = 561833, upload-time = "2026-01-30T11:21:19.232Z" }, + { url = "https://files.pythonhosted.org/packages/90/15/1ac481626cb63db751f6281e294661947c1f0321ebe5d1c532a3b51a8006/pendulum-3.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:fd55c12560816d9122ca2142d9e428f32c0c083bf77719320b1767539c7a3a3b", size = 258725, upload-time = "2026-01-30T11:21:20.558Z" }, + { url = "https://files.pythonhosted.org/packages/40/ae/50b0398d7d027eb70a3e1e336de7b6e599c6b74431cb7d3863287e1292bb/pendulum-3.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:faef52a7ed99729f0838353b956f3fabf6c550c062db247e9e2fc2b48fcb9457", size = 253089, upload-time = "2026-01-30T11:21:22.497Z" }, + { url = "https://files.pythonhosted.org/packages/02/fb/d65db067a67df7252f18b0cb7420dda84078b9e8bfb375215469c14a50be/pendulum-3.2.0-py3-none-any.whl", hash = "sha256:f3a9c18a89b4d9ef39c5fa6a78722aaff8d5be2597c129a3b16b9f40a561acf3", size = 114111, upload-time = "2026-01-30T11:22:22.361Z" }, ] [[package]] @@ -825,15 +1250,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/44/3c/d717024885424591d5376220b5e836c2d5293ce2011523c9de23ff7bf068/pip-25.3-py3-none-any.whl", hash = "sha256:9655943313a94722b7774661c21049070f6bbb0a1516bf02f7c8d5d9201514cd", size = 1778622, upload-time = "2025-10-25T00:55:39.247Z" }, ] -[[package]] -name = "platformdirs" -version = "4.9.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1b/04/fea538adf7dbbd6d186f551d595961e564a3b6715bdf276b477460858672/platformdirs-4.9.2.tar.gz", hash = "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291", size = 28394, upload-time = "2026-02-16T03:56:10.574Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" }, -] - [[package]] name = "pluggy" version = "1.6.0" @@ -903,21 +1319,78 @@ requires-dist = [ ] [[package]] -name = "pox" -version = "0.3.7" +name = "prefect" +version = "3.6.19" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/44/58/4385741dea1d74fe9dfed7ff42975266634ef8000f2c8e96717079c916b1/pox-0.3.7.tar.gz", hash = "sha256:0652f6f2103fe6d4ba638beb6fa8d3e8a68fd44bcb63315c614118515bcc3afb", size = 119442, upload-time = "2026-01-19T02:09:12.573Z" } +dependencies = [ + { name = "aiosqlite" }, + { name = "alembic" }, + { name = "amplitude-analytics" }, + { name = "anyio" }, + { name = "apprise" }, + { name = "asgi-lifespan" }, + { name = "asyncpg" }, + { name = "cachetools" }, + { name = "click" }, + { name = "cloudpickle" }, + { name = "coolname" }, + { name = "cryptography" }, + { name = "dateparser" }, + { name = "docker" }, + { name = "exceptiongroup" }, + { name = "fastapi" }, + { name = "fsspec" }, + { name = "graphviz" }, + { name = "griffe" }, + { name = "httpcore" }, + { name = "httpx", extra = ["http2"] }, + { name = "humanize" }, + { name = "jinja2" }, + { name = "jinja2-humanize-extension" }, + { name = "jsonpatch" }, + { name = "jsonschema" }, + { name = "opentelemetry-api" }, + { name = "orjson" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pendulum" }, + { name = "pluggy" }, + { name = "prometheus-client" }, + { name = "pydantic" }, + { name = "pydantic-core" }, + { name = "pydantic-extra-types" }, + { name = "pydantic-settings" }, + { name = "pydocket" }, + { name = "python-dateutil" }, + { name = "python-slugify" }, + { name = "pytz" }, + { name = "pyyaml" }, + { name = "readchar" }, + { name = "rfc3339-validator" }, + { name = "rich" }, + { name = "ruamel-yaml" }, + { name = "ruamel-yaml-clib", marker = "platform_python_implementation == 'CPython'" }, + { name = "semver" }, + { name = "sniffio" }, + { name = "sqlalchemy", extra = ["asyncio"] }, + { name = "toml" }, + { name = "typer" }, + { name = "typing-extensions" }, + { name = "uvicorn" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/25/aa44e02889c26ed38dcd3ce1572c64f477d29a5eee0289415ba0ae576afb/prefect-3.6.19.tar.gz", hash = "sha256:277020b73a3fff57b013756083be34747a7e4ed7be49c2ae267f85a9af7651a4", size = 11223526, upload-time = "2026-02-24T18:36:54.412Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/ac/4d5f104edf2aae2fec85567ec1d1969010de8124c5c45514f25e14900b65/pox-0.3.7-py3-none-any.whl", hash = "sha256:82a495249d13371314c1a5b5626a115e067ef5215d49530bf5efa37fbc25b56a", size = 29402, upload-time = "2026-01-19T02:09:11.024Z" }, + { url = "https://files.pythonhosted.org/packages/35/6b/c8dac4d7d9b3ff74fdc554b405f794975e69adc6cb4748d8a472ce8421aa/prefect-3.6.19-py3-none-any.whl", hash = "sha256:6ba4a8a65366de48f653b841627d0f4c9fae28ac8b2843c3eb9a3ac0c0c1a74c", size = 11983967, upload-time = "2026-02-24T18:36:51.969Z" }, ] [[package]] -name = "ppft" -version = "1.7.8" +name = "prometheus-client" +version = "0.24.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8b/d2/281aa3466e948283d51b83238fb456f65e14f8ade5f8627822578cd2708f/ppft-1.7.8.tar.gz", hash = "sha256:5f696d4f397ae9b0af39b1faffb31957c51dfbc5a3815856472d4f4e872937ee", size = 136349, upload-time = "2026-01-19T03:03:13.439Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/58/a794d23feb6b00fc0c72787d7e87d872a6730dd9ed7c7b3e954637d8f280/prometheus_client-0.24.1.tar.gz", hash = "sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9", size = 85616, upload-time = "2026-01-14T15:26:26.965Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/e1/d1b380af6443e7c33aeb40617ebdc17c39dc30095235643cc518e3908203/ppft-1.7.8-py3-none-any.whl", hash = "sha256:d3e0e395215b14afc3dd5adfc032ccecfda2d4ed50dc7ded076cd1d215442843", size = 56759, upload-time = "2026-01-19T03:03:11.896Z" }, + { url = "https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl", hash = "sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055", size = 64057, upload-time = "2026-01-14T15:26:24.42Z" }, ] [[package]] @@ -934,22 +1407,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/cb/e3065b447186cb70aa65acc70c86baf482d82bf75625bf5a2c4f6919c6a3/protobuf-5.29.6-py3-none-any.whl", hash = "sha256:6b9edb641441b2da9fa8f428760fc136a49cf97a52076010cf22a2ff73438a86", size = 173126, upload-time = "2026-02-04T22:54:39.462Z" }, ] -[[package]] -name = "psutil" -version = "7.2.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, - { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, - { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, - { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, - { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, - { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, - { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, - { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, -] - [[package]] name = "pulumi" version = "3.223.0" @@ -1023,6 +1480,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/86/9d/ba374c13459e0b2dba11d3757922e482f806481e7b88fec0cc1a734c2f54/pulumi_tls-5.3.0-py3-none-any.whl", hash = "sha256:267ecebb2ac5a41fe764c1e08aa9f724a88e0615e731a809e8253a1b50b8f5c8", size = 35517, upload-time = "2026-01-31T04:24:15.242Z" }, ] +[[package]] +name = "py-key-value-aio" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beartype" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/3c/0397c072a38d4bc580994b42e0c90c5f44f679303489e4376289534735e5/py_key_value_aio-0.4.4.tar.gz", hash = "sha256:e3012e6243ed7cc09bb05457bd4d03b1ba5c2b1ca8700096b3927db79ffbbe55", size = 92300, upload-time = "2026-02-16T21:21:43.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/69/f1b537ee70b7def42d63124a539ed3026a11a3ffc3086947a1ca6e861868/py_key_value_aio-0.4.4-py3-none-any.whl", hash = "sha256:18e17564ecae61b987f909fc2cd41ee2012c84b4b1dcb8c055cf8b4bc1bf3f5d", size = 152291, upload-time = "2026-02-16T21:21:44.241Z" }, +] + +[package.optional-dependencies] +memory = [ + { name = "cachetools" }, +] +redis = [ + { name = "redis" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + [[package]] name = "pydantic" version = "2.12.5" @@ -1067,6 +1554,56 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, ] +[[package]] +name = "pydantic-extra-types" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/35/2fee58b1316a73e025728583d3b1447218a97e621933fc776fb8c0f2ebdd/pydantic_extra_types-2.11.0.tar.gz", hash = "sha256:4e9991959d045b75feb775683437a97991d02c138e00b59176571db9ce634f0e", size = 157226, upload-time = "2025-12-31T16:18:27.944Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/17/fabd56da47096d240dd45ba627bead0333b0cf0ee8ada9bec579287dadf3/pydantic_extra_types-2.11.0-py3-none-any.whl", hash = "sha256:84b864d250a0fc62535b7ec591e36f2c5b4d1325fa0017eb8cda9aeb63b374a6", size = 74296, upload-time = "2025-12-31T16:18:26.38Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, +] + +[[package]] +name = "pydocket" +version = "0.17.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cloudpickle" }, + { name = "croniter" }, + { name = "fakeredis", extra = ["lua"] }, + { name = "opentelemetry-api" }, + { name = "prometheus-client" }, + { name = "py-key-value-aio", extra = ["memory", "redis"] }, + { name = "python-json-logger" }, + { name = "redis" }, + { name = "rich" }, + { name = "typer" }, + { name = "typing-extensions" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/e9/08c8642607b1b4b4f92798c04da625d763ad2b585ced7d91cc593d301ed3/pydocket-0.17.9.tar.gz", hash = "sha256:4b98b9951303fba2b77649969539d501500cd0b0e5accc27e03b16c25a76f3e6", size = 348534, upload-time = "2026-02-20T20:53:42.868Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/79/886e4db80730935f87176657aadf22f51ec9952d36ae34df9d257a9ca93d/pydocket-0.17.9-py3-none-any.whl", hash = "sha256:3f48f40d6250a33c70622b0d6c3841ed23feb3997f8e4440acd5073cd43fa044", size = 94908, upload-time = "2026-02-20T20:53:41.509Z" }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -1104,6 +1641,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "python-json-logger" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/bf/eca6a3d43db1dae7070f70e160ab20b807627ba953663ba07928cdd3dc58/python_json_logger-4.0.0.tar.gz", hash = "sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f", size = 17683, upload-time = "2025-10-06T04:15:18.984Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl", hash = "sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2", size = 15548, upload-time = "2025-10-06T04:15:17.553Z" }, +] + +[[package]] +name = "python-slugify" +version = "8.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "text-unidecode" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/c7/5e1547c44e31da50a460df93af11a535ace568ef89d7a811069ead340c4a/python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856", size = 10921, upload-time = "2024-02-08T18:32:45.488Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/62/02da182e544a51a5c3ccf4b03ab79df279f9c60c5e82d5e8bec7ca26ac11/python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8", size = 10051, upload-time = "2024-02-08T18:32:43.911Z" }, +] + [[package]] name = "pytz" version = "2025.2" @@ -1141,6 +1708,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, ] +[[package]] +name = "readchar" +version = "4.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dd/f8/8657b8cbb4ebeabfbdf991ac40eca8a1d1bd012011bd44ad1ed10f5cb494/readchar-4.2.1.tar.gz", hash = "sha256:91ce3faf07688de14d800592951e5575e9c7a3213738ed01d394dcc949b79adb", size = 9685, upload-time = "2024-11-04T18:28:07.757Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/10/e4b1e0e5b6b6745c8098c275b69bc9d73e9542d5c7da4f137542b499ed44/readchar-4.2.1-py3-none-any.whl", hash = "sha256:a769305cd3994bb5fa2764aa4073452dc105a4ec39068ffe6efd3c20c60acc77", size = 9350, upload-time = "2024-11-04T18:28:02.859Z" }, +] + +[[package]] +name = "redis" +version = "7.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/31/1476f206482dd9bc53fdbbe9f6fbd5e05d153f18e54667ce839df331f2e6/redis-7.2.1.tar.gz", hash = "sha256:6163c1a47ee2d9d01221d8456bc1c75ab953cbda18cfbc15e7140e9ba16ca3a5", size = 4906735, upload-time = "2026-02-25T20:05:18.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/98/1dd1a5c060916cf21d15e67b7d6a7078e26e2605d5c37cbc9f4f5454c478/redis-7.2.1-py3-none-any.whl", hash = "sha256:49e231fbc8df2001436ae5252b3f0f3dc930430239bfeb6da4c7ee92b16e5d33", size = 396057, upload-time = "2026-02-25T20:05:16.533Z" }, +] + [[package]] name = "referencing" version = "0.37.0" @@ -1155,6 +1740,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, ] +[[package]] +name = "regex" +version = "2026.2.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/c0/d8079d4f6342e4cec5c3e7d7415b5cd3e633d5f4124f7a4626908dbe84c7/regex-2026.2.19.tar.gz", hash = "sha256:6fb8cb09b10e38f3ae17cc6dc04a1df77762bd0351b6ba9041438e7cc85ec310", size = 414973, upload-time = "2026-02-19T19:03:47.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/73/13b39c7c9356f333e564ab4790b6cb0df125b8e64e8d6474e73da49b1955/regex-2026.2.19-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c1665138776e4ac1aa75146669236f7a8a696433ec4e525abf092ca9189247cc", size = 489541, upload-time = "2026-02-19T19:00:52.728Z" }, + { url = "https://files.pythonhosted.org/packages/15/77/fcc7bd9a67000d07fbcc11ed226077287a40d5c84544e62171d29d3ef59c/regex-2026.2.19-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d792b84709021945597e05656aac059526df4e0c9ef60a0eaebb306f8fafcaa8", size = 291414, upload-time = "2026-02-19T19:00:54.51Z" }, + { url = "https://files.pythonhosted.org/packages/f9/87/3997fc72dc59233426ef2e18dfdd105bb123812fff740ee9cc348f1a3243/regex-2026.2.19-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:db970bcce4d63b37b3f9eb8c893f0db980bbf1d404a1d8d2b17aa8189de92c53", size = 289140, upload-time = "2026-02-19T19:00:56.841Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d0/b7dd3883ed1cff8ee0c0c9462d828aaf12be63bf5dc55453cbf423523b13/regex-2026.2.19-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03d706fbe7dfec503c8c3cb76f9352b3e3b53b623672aa49f18a251a6c71b8e6", size = 798767, upload-time = "2026-02-19T19:00:59.014Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7e/8e2d09103832891b2b735a2515abf377db21144c6dd5ede1fb03c619bf09/regex-2026.2.19-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8dbff048c042beef60aa1848961384572c5afb9e8b290b0f1203a5c42cf5af65", size = 864436, upload-time = "2026-02-19T19:01:00.772Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2e/afea8d23a6db1f67f45e3a0da3057104ce32e154f57dd0c8997274d45fcd/regex-2026.2.19-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccaaf9b907ea6b4223d5cbf5fa5dff5f33dc66f4907a25b967b8a81339a6e332", size = 912391, upload-time = "2026-02-19T19:01:02.865Z" }, + { url = "https://files.pythonhosted.org/packages/59/3c/ea5a4687adaba5e125b9bd6190153d0037325a0ba3757cc1537cc2c8dd90/regex-2026.2.19-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:75472631eee7898e16a8a20998d15106cb31cfde21cdf96ab40b432a7082af06", size = 803702, upload-time = "2026-02-19T19:01:05.298Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c5/624a0705e8473a26488ec1a3a4e0b8763ecfc682a185c302dfec71daea35/regex-2026.2.19-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d89f85a5ccc0cec125c24be75610d433d65295827ebaf0d884cbe56df82d4774", size = 775980, upload-time = "2026-02-19T19:01:07.047Z" }, + { url = "https://files.pythonhosted.org/packages/4d/4b/ed776642533232b5599b7c1f9d817fe11faf597e8a92b7a44b841daaae76/regex-2026.2.19-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0d9f81806abdca3234c3dd582b8a97492e93de3602c8772013cb4affa12d1668", size = 788122, upload-time = "2026-02-19T19:01:08.744Z" }, + { url = "https://files.pythonhosted.org/packages/8c/58/e93e093921d13b9784b4f69896b6e2a9e09580a265c59d9eb95e87d288f2/regex-2026.2.19-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9dadc10d1c2bbb1326e572a226d2ec56474ab8aab26fdb8cf19419b372c349a9", size = 858910, upload-time = "2026-02-19T19:01:10.488Z" }, + { url = "https://files.pythonhosted.org/packages/85/77/ff1d25a0c56cd546e0455cbc93235beb33474899690e6a361fa6b52d265b/regex-2026.2.19-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6bc25d7e15f80c9dc7853cbb490b91c1ec7310808b09d56bd278fe03d776f4f6", size = 764153, upload-time = "2026-02-19T19:01:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ef/8ec58df26d52d04443b1dc56f9be4b409f43ed5ae6c0248a287f52311fc4/regex-2026.2.19-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:965d59792f5037d9138da6fed50ba943162160443b43d4895b182551805aff9c", size = 850348, upload-time = "2026-02-19T19:01:14.147Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b3/c42fd5ed91639ce5a4225b9df909180fc95586db071f2bf7c68d2ccbfbe6/regex-2026.2.19-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:38d88c6ed4a09ed61403dbdf515d969ccba34669af3961ceb7311ecd0cef504a", size = 789977, upload-time = "2026-02-19T19:01:15.838Z" }, + { url = "https://files.pythonhosted.org/packages/b6/22/bc3b58ebddbfd6ca5633e71fd41829ee931963aad1ebeec55aad0c23044e/regex-2026.2.19-cp312-cp312-win32.whl", hash = "sha256:5df947cabab4b643d4791af5e28aecf6bf62e6160e525651a12eba3d03755e6b", size = 266381, upload-time = "2026-02-19T19:01:17.952Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4a/6ff550b63e67603ee60e69dc6bd2d5694e85046a558f663b2434bdaeb285/regex-2026.2.19-cp312-cp312-win_amd64.whl", hash = "sha256:4146dc576ea99634ae9c15587d0c43273b4023a10702998edf0fa68ccb60237a", size = 277274, upload-time = "2026-02-19T19:01:19.826Z" }, + { url = "https://files.pythonhosted.org/packages/cc/29/9ec48b679b1e87e7bc8517dff45351eab38f74fbbda1fbcf0e9e6d4e8174/regex-2026.2.19-cp312-cp312-win_arm64.whl", hash = "sha256:cdc0a80f679353bd68450d2a42996090c30b2e15ca90ded6156c31f1a3b63f3b", size = 270509, upload-time = "2026-02-19T19:01:22.075Z" }, +] + [[package]] name = "requests" version = "2.32.5" @@ -1170,6 +1779,31 @@ 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 = "requests-oauthlib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "oauthlib" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, +] + +[[package]] +name = "rfc3339-validator" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" }, +] + [[package]] name = "rich" version = "13.9.4" @@ -1207,81 +1841,42 @@ wheels = [ ] [[package]] -name = "s3transfer" -version = "0.16.0" +name = "ruamel-yaml" +version = "0.19.1" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "botocore" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/05/04/74127fc843314818edfa81b5540e26dd537353b123a4edc563109d8f17dd/s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920", size = 153827, upload-time = "2025-12-01T02:30:59.114Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709, upload-time = "2026-01-02T16:50:31.84Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", size = 86830, upload-time = "2025-12-01T02:30:57.729Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102, upload-time = "2026-01-02T16:50:29.201Z" }, ] [[package]] -name = "sagemaker" -version = "2.257.0" +name = "ruamel-yaml-clib" +version = "0.2.15" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "boto3" }, - { name = "cloudpickle" }, - { name = "docker" }, - { name = "fastapi" }, - { name = "google-pasta" }, - { name = "graphene" }, - { name = "importlib-metadata" }, - { name = "jsonschema" }, - { name = "numpy" }, - { name = "omegaconf" }, - { name = "packaging" }, - { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'win32'" }, - { name = "pandas", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'win32'" }, - { name = "pathos" }, - { name = "platformdirs" }, - { name = "protobuf" }, - { name = "psutil" }, - { name = "pyyaml" }, - { name = "requests" }, - { name = "sagemaker-core" }, - { name = "schema" }, - { name = "smdebug-rulesconfig" }, - { name = "tblib" }, - { name = "tqdm" }, - { name = "urllib3" }, - { name = "uvicorn" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c5/57/627ac9bb2500edab68a6ad04dd34738e88e98b704af49a0b9d339ecbafcc/sagemaker-2.257.0.tar.gz", hash = "sha256:be74eeda41c972a52afe0ba01624d8aae951a3656c21b07870e76eaa7d4816f8", size = 1277278, upload-time = "2026-02-03T21:23:05.485Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/97/60fda20e2fb54b83a61ae14648b0817c8f5d84a3821e40bfbdae1437026a/ruamel_yaml_clib-0.2.15.tar.gz", hash = "sha256:46e4cc8c43ef6a94885f72512094e482114a8a706d3c555a34ed4b0d20200600", size = 225794, upload-time = "2025-11-16T16:12:59.761Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/b8/644b67cecdf34a63c3758ff5a2b6cd207482fbd19d5d02e15c8db068eb41/sagemaker-2.257.0-py3-none-any.whl", hash = "sha256:c45b9d6fd2ce0289b1823b77ae9aabd394d8168d6ed789efb39a6cfe4e1ea671", size = 1689830, upload-time = "2026-02-03T21:23:02.458Z" }, + { url = "https://files.pythonhosted.org/packages/72/4b/5fde11a0722d676e469d3d6f78c6a17591b9c7e0072ca359801c4bd17eee/ruamel_yaml_clib-0.2.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb15a2e2a90c8475df45c0949793af1ff413acfb0a716b8b94e488ea95ce7cff", size = 149088, upload-time = "2025-11-16T16:13:22.836Z" }, + { url = "https://files.pythonhosted.org/packages/85/82/4d08ac65ecf0ef3b046421985e66301a242804eb9a62c93ca3437dc94ee0/ruamel_yaml_clib-0.2.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:64da03cbe93c1e91af133f5bec37fd24d0d4ba2418eaf970d7166b0a26a148a2", size = 134553, upload-time = "2025-11-16T16:13:24.151Z" }, + { url = "https://files.pythonhosted.org/packages/b9/cb/22366d68b280e281a932403b76da7a988108287adff2bfa5ce881200107a/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f6d3655e95a80325b84c4e14c080b2470fe4f33b6846f288379ce36154993fb1", size = 737468, upload-time = "2025-11-16T20:22:47.335Z" }, + { url = "https://files.pythonhosted.org/packages/71/73/81230babf8c9e33770d43ed9056f603f6f5f9665aea4177a2c30ae48e3f3/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71845d377c7a47afc6592aacfea738cc8a7e876d586dfba814501d8c53c1ba60", size = 753349, upload-time = "2025-11-16T16:13:26.269Z" }, + { url = "https://files.pythonhosted.org/packages/61/62/150c841f24cda9e30f588ef396ed83f64cfdc13b92d2f925bb96df337ba9/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e5499db1ccbc7f4b41f0565e4f799d863ea720e01d3e99fa0b7b5fcd7802c9", size = 788211, upload-time = "2025-11-16T16:13:27.441Z" }, + { url = "https://files.pythonhosted.org/packages/30/93/e79bd9cbecc3267499d9ead919bd61f7ddf55d793fb5ef2b1d7d92444f35/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4b293a37dc97e2b1e8a1aec62792d1e52027087c8eea4fc7b5abd2bdafdd6642", size = 743203, upload-time = "2025-11-16T16:13:28.671Z" }, + { url = "https://files.pythonhosted.org/packages/8d/06/1eb640065c3a27ce92d76157f8efddb184bd484ed2639b712396a20d6dce/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:512571ad41bba04eac7268fe33f7f4742210ca26a81fe0c75357fa682636c690", size = 747292, upload-time = "2025-11-16T20:22:48.584Z" }, + { url = "https://files.pythonhosted.org/packages/a5/21/ee353e882350beab65fcc47a91b6bdc512cace4358ee327af2962892ff16/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5e9f630c73a490b758bf14d859a39f375e6999aea5ddd2e2e9da89b9953486a", size = 771624, upload-time = "2025-11-16T16:13:29.853Z" }, + { url = "https://files.pythonhosted.org/packages/57/34/cc1b94057aa867c963ecf9ea92ac59198ec2ee3a8d22a126af0b4d4be712/ruamel_yaml_clib-0.2.15-cp312-cp312-win32.whl", hash = "sha256:f4421ab780c37210a07d138e56dd4b51f8642187cdfb433eb687fe8c11de0144", size = 100342, upload-time = "2025-11-16T16:13:31.067Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e5/8925a4208f131b218f9a7e459c0d6fcac8324ae35da269cb437894576366/ruamel_yaml_clib-0.2.15-cp312-cp312-win_amd64.whl", hash = "sha256:2b216904750889133d9222b7b873c199d48ecbb12912aca78970f84a5aa1a4bc", size = 119013, upload-time = "2025-11-16T16:13:32.164Z" }, ] [[package]] -name = "sagemaker-core" -version = "1.0.77" +name = "s3transfer" +version = "0.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "boto3" }, - { name = "importlib-metadata" }, - { name = "jsonschema" }, - { name = "mock" }, - { name = "platformdirs" }, - { name = "pydantic" }, - { name = "pyyaml" }, - { name = "rich" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6a/df/cbe5cd498ad517cdc324626c3249f979811a7a3f7eb42aa84c047d8e2951/sagemaker_core-1.0.77.tar.gz", hash = "sha256:1e49ff58cbc6091ffc3120e7926ec951a7b099efc24b1c39b5cc873372a778d1", size = 425611, upload-time = "2026-02-16T10:39:27.941Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/cb/3f7520427fbd871596a754614ab73bdcf1a68397b367dd765bcda76f20ce/sagemaker_core-1.0.77-py3-none-any.whl", hash = "sha256:9a5d3f19fc2011bdf64909863c778cc3154b58a522ed511a637727e28ae25bf4", size = 440601, upload-time = "2026-02-16T10:39:26.563Z" }, + { name = "botocore" }, ] - -[[package]] -name = "schema" -version = "0.7.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fb/2e/8da627b65577a8f130fe9dfa88ce94fcb24b1f8b59e0fc763ee61abef8b8/schema-0.7.8.tar.gz", hash = "sha256:e86cc08edd6fe6e2522648f4e47e3a31920a76e82cce8937535422e310862ab5", size = 45540, upload-time = "2025-10-11T13:15:40.281Z" } +sdist = { url = "https://files.pythonhosted.org/packages/05/04/74127fc843314818edfa81b5540e26dd537353b123a4edc563109d8f17dd/s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920", size = 153827, upload-time = "2025-12-01T02:30:59.114Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/75/aad85817266ac5285c93391711d231ca63e9ae7d42cd3ca37549e24ebe52/schema-0.7.8-py2.py3-none-any.whl", hash = "sha256:00bd977fadc7d9521bf289850cd8a8aa5f4948f575476b8daaa5c1b57af2dce1", size = 19108, upload-time = "2025-10-11T17:13:07.323Z" }, + { url = "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", size = 86830, upload-time = "2025-12-01T02:30:57.729Z" }, ] [[package]] @@ -1311,6 +1906,15 @@ fastapi = [ { name = "fastapi" }, ] +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + [[package]] name = "six" version = "1.17.0" @@ -1321,12 +1925,46 @@ wheels = [ ] [[package]] -name = "smdebug-rulesconfig" -version = "1.0.1" +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.47" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cc/7d/8ad6a2098e03c1f811d1277a2cedb81265828f144f6d323b83a2392e8bb9/smdebug_rulesconfig-1.0.1.tar.gz", hash = "sha256:7a19e6eb2e6bcfefbc07e4a86ef7a88f32495001a038bf28c7d8e77ab793fcd6", size = 12060, upload-time = "2020-12-18T23:54:52.357Z" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/4b/1e00561093fe2cd8eef09d406da003c8a118ff02d6548498c1ae677d68d9/sqlalchemy-2.0.47.tar.gz", hash = "sha256:e3e7feb57b267fe897e492b9721ae46d5c7de6f9e8dee58aacf105dc4e154f3d", size = 9886323, upload-time = "2026-02-24T16:34:27.947Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/a1/45a13a05198bbe9527bab2c5e5daa8bd02678aa825eec14783e767bfa7d1/smdebug_rulesconfig-1.0.1-py2.py3-none-any.whl", hash = "sha256:104da3e6931ecf879dfc687ca4bbb3bee5ea2bc27f4478e9dbb3ee3655f1ae61", size = 20282, upload-time = "2020-12-18T23:54:51.267Z" }, + { url = "https://files.pythonhosted.org/packages/80/88/74eb470223ff88ea6572a132c0b8de8c1d8ed7b843d3b44a8a3c77f31d39/sqlalchemy-2.0.47-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4fa91b19d6b9821c04cc8f7aa2476429cc8887b9687c762815aa629f5c0edec1", size = 2155687, upload-time = "2026-02-24T17:05:46.451Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ba/1447d3d558971b036cb93b557595cb5dcdfe728f1c7ac4dec16505ef5756/sqlalchemy-2.0.47-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c5bbbd14eff577c8c79cbfe39a0771eecd20f430f3678533476f0087138f356", size = 3336978, upload-time = "2026-02-24T17:18:04.597Z" }, + { url = "https://files.pythonhosted.org/packages/8a/07/b47472d2ffd0776826f17ccf0b4d01b224c99fbd1904aeb103dffbb4b1cc/sqlalchemy-2.0.47-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5a6c555da8d4280a3c4c78c5b7a3f990cee2b2884e5f934f87a226191682ff7", size = 3349939, upload-time = "2026-02-24T17:27:18.937Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c6/95fa32b79b57769da3e16f054cf658d90940317b5ca0ec20eac84aa19c4f/sqlalchemy-2.0.47-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ed48a1701d24dff3bb49a5bce94d6bc84cbe33d98af2aa2d3cdcce3dea1709ec", size = 3279648, upload-time = "2026-02-24T17:18:07.038Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c8/3d07e7c73928dc59a0bed40961ca4e313e797bce650b088e8d5fdd3ad939/sqlalchemy-2.0.47-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4f3178c920ad98158f0b6309382194df04b14808fa6052ae07099fdde29d5602", size = 3314695, upload-time = "2026-02-24T17:27:20.93Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d2/ed32b1611c1e19fdb028eee1adc5a9aa138c2952d09ae11f1670170f80ae/sqlalchemy-2.0.47-cp312-cp312-win32.whl", hash = "sha256:b9c11ac9934dd59ece9619fe42780a08abe2faab7b0543bb00d5eabea4f421b9", size = 2115502, upload-time = "2026-02-24T17:22:52.546Z" }, + { url = "https://files.pythonhosted.org/packages/fd/52/9de590356a4dd8e9ef5a881dbba64b2bbc4cbc71bf02bc68e775fb9b1899/sqlalchemy-2.0.47-cp312-cp312-win_amd64.whl", hash = "sha256:db43b72cf8274a99e089755c9c1e0b947159b71adbc2c83c3de2e38d5d607acb", size = 2142435, upload-time = "2026-02-24T17:22:54.268Z" }, + { url = "https://files.pythonhosted.org/packages/15/9f/7c378406b592fcf1fc157248607b495a40e3202ba4a6f1372a2ba6447717/sqlalchemy-2.0.47-py3-none-any.whl", hash = "sha256:e2647043599297a1ef10e720cf310846b7f31b6c841fee093d2b09d81215eb93", size = 1940159, upload-time = "2026-02-24T17:15:07.158Z" }, +] + +[package.optional-dependencies] +asyncio = [ + { name = "greenlet" }, ] [[package]] @@ -1360,12 +1998,12 @@ wheels = [ ] [[package]] -name = "tblib" -version = "3.2.2" +name = "text-unidecode" +version = "1.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f4/8a/14c15ae154895cc131174f858c707790d416c444fc69f93918adfd8c4c0b/tblib-3.2.2.tar.gz", hash = "sha256:e9a652692d91bf4f743d4a15bc174c0b76afc750fe8c7b6d195cc1c1d6d2ccec", size = 35046, upload-time = "2025-11-12T12:21:16.572Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/e2/e9a00f0ccb71718418230718b3d900e71a5d16e701a3dae079a21e9cd8f8/text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93", size = 76885, upload-time = "2019-08-30T21:36:45.405Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl", hash = "sha256:26bdccf339bcce6a88b2b5432c988b266ebbe63a4e593f6b578b1d2e723d2b76", size = 12893, upload-time = "2025-11-12T12:21:14.407Z" }, + { url = "https://files.pythonhosted.org/packages/a6/a5/c0b6468d3824fe3fde30dbb5e1f687b291608f9473681bbf7dabbf5a87d7/text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", size = 78154, upload-time = "2019-08-30T21:37:03.543Z" }, ] [[package]] @@ -1377,6 +2015,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1a/1e/034a535398b1fca9e9a0ece0bc870c122c79c80866eca3ed802bfea7b0df/tinygrad-0.12.0-py3-none-any.whl", hash = "sha256:275c01961e0959579ba99c6ce1ed70a2c2f1a408ed8cfb5c4de8ad1355e66611", size = 1673238, upload-time = "2026-01-12T17:05:06.727Z" }, ] +[[package]] +name = "toml" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, +] + [[package]] name = "tools" version = "0.0.1" @@ -1384,8 +2031,8 @@ source = { editable = "tools" } dependencies = [ { name = "boto3" }, { name = "polars" }, + { name = "prefect" }, { name = "requests" }, - { name = "sagemaker" }, { name = "structlog" }, ] @@ -1398,8 +2045,8 @@ dev = [ requires-dist = [ { name = "boto3", specifier = ">=1.40.74" }, { name = "polars", specifier = ">=1.29.0" }, + { name = "prefect", specifier = ">=3.0.0,<4.0.0" }, { name = "requests", specifier = ">=2.32.5" }, - { name = "sagemaker", specifier = ">=2.256.0,<3.0.0" }, { name = "structlog", specifier = ">=25.5.0" }, ] @@ -1407,27 +2054,30 @@ requires-dist = [ dev = [{ name = "boto3-stubs", extras = ["s3"], specifier = ">=1.38.0" }] [[package]] -name = "tqdm" -version = "4.67.3" +name = "typeguard" +version = "4.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/e8/66e25efcc18542d58706ce4e50415710593721aae26e794ab1dec34fb66f/typeguard-4.5.1.tar.gz", hash = "sha256:f6f8ecbbc819c9bc749983cc67c02391e16a9b43b8b27f15dc70ed7c4a007274", size = 80121, upload-time = "2026-02-19T16:09:03.392Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, + { url = "https://files.pythonhosted.org/packages/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.1-py3-none-any.whl", hash = "sha256:44d2bf329d49a244110a090b55f5f91aa82d9a9834ebfd30bcc73651e4a8cc40", size = 36745, upload-time = "2026-02-19T16:09:01.6Z" }, ] [[package]] -name = "typeguard" -version = "4.5.1" +name = "typer" +version = "0.24.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2b/e8/66e25efcc18542d58706ce4e50415710593721aae26e794ab1dec34fb66f/typeguard-4.5.1.tar.gz", hash = "sha256:f6f8ecbbc819c9bc749983cc67c02391e16a9b43b8b27f15dc70ed7c4a007274", size = 80121, upload-time = "2026-02-19T16:09:03.392Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.1-py3-none-any.whl", hash = "sha256:44d2bf329d49a244110a090b55f5f91aa82d9a9834ebfd30bcc73651e4a8cc40", size = 36745, upload-time = "2026-02-19T16:09:01.6Z" }, + { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, ] [[package]] @@ -1491,6 +2141,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, ] +[[package]] +name = "tzlocal" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" }, +] + [[package]] name = "urllib3" version = "2.6.3"