diff --git a/crates/rattler_build_jinja/src/variable.rs b/crates/rattler_build_jinja/src/variable.rs index b4a958f4e..0cef66165 100644 --- a/crates/rattler_build_jinja/src/variable.rs +++ b/crates/rattler_build_jinja/src/variable.rs @@ -62,6 +62,36 @@ impl Variable { pub fn from_string(value: &str) -> Self { Variable(Value::from_safe_string(value.to_string())) } + + /// Try to extract as a boolean value using serde deserialization + pub fn as_bool(&self) -> Option { + bool::deserialize(self.0.clone()).ok() + } + + /// Try to extract as an i64 value using serde deserialization + pub fn as_i64(&self) -> Option { + i64::deserialize(self.0.clone()).ok() + } + + /// Check if this variable is a number + pub fn is_number(&self) -> bool { + self.0.is_number() + } + + /// Try to extract as a string slice + pub fn as_str(&self) -> Option<&str> { + self.0.as_str() + } + + /// Check if this is a sequence/list + pub fn is_sequence(&self) -> bool { + self.0.len().is_some() && self.0.as_str().is_none() + } + + /// Try to iterate over the variable if it's a sequence + pub fn try_iter(&self) -> Result + '_, minijinja::Error> { + self.0.try_iter() + } } impl Display for Variable { diff --git a/crates/rattler_build_script/Cargo.toml b/crates/rattler_build_script/Cargo.toml index 90bb5c35b..6be370eaf 100644 --- a/crates/rattler_build_script/Cargo.toml +++ b/crates/rattler_build_script/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors.workspace = true edition.workspace = true license.workspace = true -readme = "README.md" +readme.workspace = true repository.workspace = true description = "Script execution and data model for rattler-build" diff --git a/py-rattler-build/Cargo.lock b/py-rattler-build/Cargo.lock index c7335cffe..96c7a57db 100644 --- a/py-rattler-build/Cargo.lock +++ b/py-rattler-build/Cargo.lock @@ -1464,16 +1464,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "diffy" -version = "0.4.2" -source = "git+https://github.com/prefix-dev/diffy.git?branch=master#f916e25c31a8d9e7483116c9e8aa6a36e20f947a" -dependencies = [ - "nu-ansi-term", - "strsim", - "thiserror 2.0.17", -] - [[package]] name = "digest" version = "0.10.7" @@ -1960,15 +1950,6 @@ dependencies = [ "slab", ] -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -2078,7 +2059,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.3.1", - "indexmap 2.11.4", + "indexmap 2.12.0", "slab", "tokio", "tokio-util", @@ -2253,7 +2234,8 @@ dependencies = [ [[package]] name = "http-range-client" version = "0.9.1" -source = "git+https://github.com/pka/http-range-client.git#fccfa852dbe7e875a50802c55ee6c5f69c634827" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5f72a73344fc9576e74e42a0f1d7a05d7c6c50950a15f44faea133a91f622b" dependencies = [ "async-trait", "byteorder", @@ -2542,9 +2524,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.4" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", "hashbrown 0.16.0", @@ -3319,6 +3301,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + [[package]] name = "ordered-multimap" version = "0.7.3" @@ -3424,13 +3415,13 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "path_resolver" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67957e099f5b76f4cad98e0d41dd53746ab3a2debc1da12ee5b1cccb1d7b8dc8" +checksum = "3604448dbdd00c6884479c066f040f503bf26f6b58de5358b5d2376ef4e78d11" dependencies = [ + "ahash", "fs-err", - "fxhash", - "indexmap 2.11.4", + "indexmap 2.12.0", "itertools 0.14.0", "proptest", "tempfile", @@ -3457,7 +3448,7 @@ checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" dependencies = [ "fixedbitset", "hashbrown 0.15.5", - "indexmap 2.11.4", + "indexmap 2.12.0", "serde", ] @@ -3533,7 +3524,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ "base64", - "indexmap 2.11.4", + "indexmap 2.12.0", "quick-xml 0.38.3", "serde", "time", @@ -3641,22 +3632,33 @@ dependencies = [ [[package]] name = "py-rattler-build" -version = "0.48.1" +version = "0.49.0" dependencies = [ "chrono", "clap", + "fs-err", + "indexmap 2.12.0", + "indicatif", "miette", "pyo3", "pyo3-build-config", "pythonize", "rattler-build", + "rattler_build_jinja", + "rattler_build_recipe", + "rattler_build_types", + "rattler_build_variant_config", "rattler_conda_types", "rattler_config", "rattler_networking", + "rattler_solve", "rattler_upload", + "rattler_virtual_packages", "serde_json", "thiserror 2.0.17", "tokio", + "tracing", + "tracing-subscriber", "url", ] @@ -3904,9 +3906,9 @@ dependencies = [ [[package]] name = "rattler" -version = "0.37.7" +version = "0.38.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83d5fa5c5cb08baeeed48085bbe553b418f532907dbb10a23fac4961a309efcb" +checksum = "97a2c731b4989ca74615b05f308a63664b6e7592a621a58fb5267b51a8239273" dependencies = [ "anyhow", "clap", @@ -3916,7 +3918,7 @@ dependencies = [ "fs-err", "futures", "humantime", - "indexmap 2.11.4", + "indexmap 2.12.0", "indicatif", "itertools 0.14.0", "memchr", @@ -3949,9 +3951,8 @@ dependencies = [ [[package]] name = "rattler-build" -version = "0.48.1" +version = "0.49.0" dependencies = [ - "anyhow", "async-once-cell", "async-recursion", "base64", @@ -3965,7 +3966,6 @@ dependencies = [ "console", "content_inspector", "dialoguer", - "diffy", "dunce", "flate2", "fs-err", @@ -3975,25 +3975,25 @@ dependencies = [ "hex", "http-range-client", "ignore", - "indexmap 2.11.4", + "indexmap 2.12.0", "indicatif", "itertools 0.14.0", "lazy_static", "line-ending", - "marked-yaml", "memchr", "memmap2", "miette", "minijinja", "num_cpus", - "opendal", "pathdiff", "petgraph", "rattler", + "rattler_build_diffy", "rattler_build_jinja", "rattler_build_networking", "rattler_build_recipe", "rattler_build_recipe_generator", + "rattler_build_script", "rattler_build_source_cache", "rattler_build_types", "rattler_build_variant_config", @@ -4006,6 +4006,7 @@ dependencies = [ "rattler_menuinst", "rattler_networking", "rattler_package_streaming", + "rattler_prefix_guard", "rattler_redaction", "rattler_repodata_gateway", "rattler_shell", @@ -4045,16 +4046,25 @@ dependencies = [ "walkdir", "which", "xz2", - "zip 6.0.0", + "zip", "zstd", ] +[[package]] +name = "rattler_build_diffy" +version = "0.4.2" +dependencies = [ + "nu-ansi-term", + "strsim", + "thiserror 2.0.17", +] + [[package]] name = "rattler_build_jinja" version = "0.1.0" dependencies = [ "fs-err", - "indexmap 2.11.4", + "indexmap 2.12.0", "itertools 0.14.0", "lazy_static", "minijinja", @@ -4085,21 +4095,27 @@ version = "0.1.0" dependencies = [ "fs-err", "globset", - "indexmap 2.11.4", + "indexmap 2.12.0", "itertools 0.14.0", "marked-yaml", "miette", "minijinja", "pathdiff", + "petgraph", "rattler_build_jinja", + "rattler_build_script", "rattler_build_types", + "rattler_build_variant_config", + "rattler_build_yaml_parser", "rattler_conda_types", "rattler_digest", "regex", "serde", + "serde-value", "serde_json", "serde_with", "serde_yaml", + "sha1", "spdx", "thiserror 2.0.17", "url", @@ -4114,7 +4130,7 @@ dependencies = [ "clap", "flate2", "fs-err", - "indexmap 2.11.4", + "indexmap 2.12.0", "itertools 0.14.0", "miette", "rattler_conda_types", @@ -4131,7 +4147,28 @@ dependencies = [ "toml", "tracing", "url", - "zip 6.0.0", + "zip", +] + +[[package]] +name = "rattler_build_script" +version = "0.1.0" +dependencies = [ + "clap", + "console", + "fs-err", + "futures", + "indexmap 2.12.0", + "itertools 0.14.0", + "minijinja", + "rattler_conda_types", + "rattler_shell", + "serde", + "thiserror 2.0.17", + "tokio", + "tokio-util", + "tracing", + "which", ] [[package]] @@ -4166,7 +4203,7 @@ dependencies = [ "url", "walkdir", "xz2", - "zip 6.0.0", + "zip", "zstd", ] @@ -4174,6 +4211,7 @@ dependencies = [ name = "rattler_build_types" version = "0.1.0" dependencies = [ + "globset", "itertools 0.14.0", "rattler_conda_types", "serde", @@ -4193,6 +4231,7 @@ dependencies = [ "minijinja", "rattler_build_jinja", "rattler_build_types", + "rattler_build_yaml_parser", "rattler_conda_types", "serde", "serde_yaml", @@ -4200,12 +4239,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "rattler_build_yaml_parser" +version = "0.1.0" +dependencies = [ + "marked-yaml", + "miette", + "rattler_build_jinja", + "serde", + "thiserror 2.0.17", +] + [[package]] name = "rattler_cache" -version = "0.3.37" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8ad71f498394644bea221feb51e9d8b2a03deaba4790d6419e018c19edddf4d" +checksum = "94e25b6c05b42228a55cd58a11d91af92819b03c9c604cbc7a83568ced7e2db2" dependencies = [ + "ahash", "anyhow", "dashmap", "digest", @@ -4213,7 +4264,6 @@ dependencies = [ "fs-err", "fs4", "futures", - "fxhash", "itertools 0.14.0", "parking_lot 0.12.5", "rattler_conda_types", @@ -4235,19 +4285,19 @@ dependencies = [ [[package]] name = "rattler_conda_types" -version = "0.40.0" +version = "0.40.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3af51ac4106816d06479ece090aa4842d94c1ef1f2dfb746a2568f2ea23ec642" +checksum = "0c4a129c8b0f43f700b43af71fcbf17fae058e0840c8a610d354355a552eb36e" dependencies = [ + "ahash", "chrono", "core-foundation 0.10.1", "dirs", "file_url", "fs-err", - "fxhash", "glob", "hex", - "indexmap 2.11.4", + "indexmap 2.12.0", "itertools 0.14.0", "lazy-regex", "memmap2", @@ -4277,13 +4327,13 @@ dependencies = [ [[package]] name = "rattler_config" -version = "0.2.11" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bcfbee1e19ccbb950f3d29ed6957d4f5ef9c5b2043f44e38bb671734a47bbdd" +checksum = "c6ecabc1466577e0d2d8c6d66c3e2bd64992f51d49dabe1815fedbf13ed6b930" dependencies = [ "console", "fs-err", - "indexmap 2.11.4", + "indexmap 2.12.0", "rattler_conda_types", "serde", "serde_json", @@ -4295,9 +4345,9 @@ dependencies = [ [[package]] name = "rattler_digest" -version = "1.1.5" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40b746768824bc3306dcb597e549e836eda023dab8d7407d32b9f342c70cc5d" +checksum = "886e9a6254e74a830c2b8555e14862d6b2f4cc498b43767d4adf1c71421a4796" dependencies = [ "blake2", "digest", @@ -4305,6 +4355,7 @@ dependencies = [ "hex", "md-5", "serde", + "serde_bytes", "serde_with", "sha2", "tokio", @@ -4332,10 +4383,11 @@ dependencies = [ [[package]] name = "rattler_index" -version = "0.25.5" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5851820dea17939da687656ec73fcc30e61a942f88cddcfdcedf4311d0b7fa8c" +checksum = "2eb036f08ec548e6809060d9f989264624fa098a2d5ad13e829b02377d90cd68" dependencies = [ + "ahash", "anyhow", "bytes", "chrono", @@ -4344,7 +4396,7 @@ dependencies = [ "console", "fs-err", "futures", - "fxhash", + "indexmap 2.12.0", "indicatif", "opendal", "rattler_conda_types", @@ -4353,6 +4405,7 @@ dependencies = [ "rattler_package_streaming", "rattler_s3", "reqwest", + "retry-policies 0.4.0", "rmp-serde", "serde", "serde_json", @@ -4377,9 +4430,9 @@ dependencies = [ [[package]] name = "rattler_menuinst" -version = "0.2.29" +version = "0.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c88b95e91763fb996f6c7bb68ffc6b2e9e5050f71eae66552db1b246e0cf1839" +checksum = "84487b67092b6624ade384f4a22adb287fee9042d2e18c50714ca947d69d3879" dependencies = [ "chrono", "configparser", @@ -4407,9 +4460,9 @@ dependencies = [ [[package]] name = "rattler_networking" -version = "0.25.16" +version = "0.25.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b84d760de2bc2fd6c4385ef8f0fd4017b8b96e961c76b5a91b46dbbf775b92" +checksum = "e6937ea4ebf9b46051a96fc061199e7a81e1debfd6a7558e110f400bf8dc054b" dependencies = [ "anyhow", "async-once-cell", @@ -4439,9 +4492,9 @@ dependencies = [ [[package]] name = "rattler_package_streaming" -version = "0.23.7" +version = "0.23.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f9b332c3e7ee6f9ee631aab31aa725022828928a61d5cb5755f2d6df36778f1" +checksum = "0778a02ff0cddc56836f325bd5365cdf3d7357570a8b9f21f6fd590452b39814" dependencies = [ "bzip2", "chrono", @@ -4463,7 +4516,7 @@ dependencies = [ "tokio-util", "tracing", "url", - "zip 4.0.0", + "zip", "zstd", ] @@ -4481,13 +4534,14 @@ dependencies = [ [[package]] name = "rattler_pty" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a7edbf8f23a099ccb32cc4f024886e35a873c462ca83f86c286556c275bf06" +checksum = "86a84e6b351c46c7588bb8f5c90994be23d5c3c31cf87f9ec903cd91d9fc4246" dependencies = [ "libc", "nix 0.30.1", "signal-hook", + "tokio", ] [[package]] @@ -4503,9 +4557,9 @@ dependencies = [ [[package]] name = "rattler_repodata_gateway" -version = "0.24.7" +version = "0.24.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad405e92e9d9152b6cf94094ddbb669789002693f133d1929de21256587bcce" +checksum = "410e796a90104cb4416f24955816dd2cdd9727b08b18972a32dde730e13264b3" dependencies = [ "anyhow", "async-compression", @@ -4563,9 +4617,9 @@ dependencies = [ [[package]] name = "rattler_s3" -version = "0.1.5" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa31673d20d2ff299917bf24b9b7d8c3ed5b670c2d39efee5d1680c8113854f8" +checksum = "9af38d8d347a5cfe4af89cb7146006691237d051c17c7915a977575116a8a1b2" dependencies = [ "aws-config", "aws-credential-types", @@ -4580,14 +4634,14 @@ dependencies = [ [[package]] name = "rattler_shell" -version = "0.25.2" +version = "0.25.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d07812fc58b55efd6142e8142eaa2088792a3e9b1396b087d964dc34b11d1f" +checksum = "be1ebf3ab404de7616429f74f5d5fca00eaaf9df535706613bab42b44bdacd13" dependencies = [ "anyhow", "enum_dispatch", "fs-err", - "indexmap 2.11.4", + "indexmap 2.12.0", "itertools 0.14.0", "rattler_conda_types", "rattler_pty", @@ -4601,9 +4655,9 @@ dependencies = [ [[package]] name = "rattler_solve" -version = "3.0.5" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe4775890c2be1a927a17b8a4a6383358a403563a0283e35f2fff460de44def" +checksum = "44ebabdec0f11bfe534f6a63f5d49cd4e7647ea029c751821c2ab36e558a97a0" dependencies = [ "chrono", "futures", @@ -4619,9 +4673,9 @@ dependencies = [ [[package]] name = "rattler_upload" -version = "0.3.4" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "771707ed3bf3e0962802193a7dada6cb9e71614def41fb3a72f88011cb136a90" +checksum = "2fa36a0f044d805785aafc77e7eec392f8c3e65441166893934a3b95cb735329" dependencies = [ "base64", "clap", @@ -4655,9 +4709,9 @@ dependencies = [ [[package]] name = "rattler_virtual_packages" -version = "2.2.1" +version = "2.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "299c16885487278a7d305634e4a58512bf0fc4b052623d033cc05fa6cd1255e9" +checksum = "2debe854ed9ecdf446b976ceac16c4cce14b8c69dd0731a5d9579ee59ec731b7" dependencies = [ "archspec", "libloading", @@ -4925,7 +4979,7 @@ dependencies = [ "elsa", "event-listener", "futures", - "indexmap 2.11.4", + "indexmap 2.12.0", "itertools 0.14.0", "petgraph", "tracing", @@ -5295,6 +5349,26 @@ dependencies = [ "typeid", ] +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -5321,7 +5395,7 @@ version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.12.0", "itoa", "memchr", "ryu", @@ -5371,7 +5445,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.11.4", + "indexmap 2.12.0", "schemars 0.9.0", "schemars 1.0.4", "serde_core", @@ -5398,7 +5472,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.12.0", "itoa", "ryu", "serde", @@ -5978,7 +6052,7 @@ version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.12.0", "serde_core", "serde_spanned", "toml_datetime", @@ -6002,7 +6076,7 @@ version = "0.23.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.12.0", "toml_datetime", "toml_parser", "winnow", @@ -7313,21 +7387,6 @@ dependencies = [ "syn", ] -[[package]] -name = "zip" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "153a6fff49d264c4babdcfa6b4d534747f520e56e8f0f384f3b808c4b64cc1fd" -dependencies = [ - "arbitrary", - "crc32fast", - "flate2", - "indexmap 2.11.4", - "memchr", - "time", - "zopfli", -] - [[package]] name = "zip" version = "6.0.0" @@ -7337,8 +7396,9 @@ dependencies = [ "arbitrary", "crc32fast", "flate2", - "indexmap 2.11.4", + "indexmap 2.12.0", "memchr", + "time", "zopfli", ] diff --git a/py-rattler-build/Cargo.toml b/py-rattler-build/Cargo.toml index e3e582f46..52f65a696 100644 --- a/py-rattler-build/Cargo.toml +++ b/py-rattler-build/Cargo.toml @@ -16,6 +16,11 @@ rustls-tls = ["rattler-build/rustls-tls"] [dependencies] rattler-build = { path = "../" } +rattler_build_recipe = { path = "../crates/rattler_build_recipe" } +rattler_build_jinja = { path = "../crates/rattler_build_jinja" } +rattler_build_types = { path = "../crates/rattler_build_types" } +rattler_build_variant_config = { path = "../crates/rattler_build_variant_config" } +indexmap = "2.11.4" pyo3 = { version = "0.26.0", features = [ "abi3-py38", "extension-module", @@ -31,6 +36,8 @@ tokio = { version = "1.47.1", features = [ rattler_conda_types = "0.40.0" rattler_config = "0.2.11" rattler_networking = { version = "0.25.15" } +rattler_solve = "3.0.9" +rattler_virtual_packages = "2.2.5" clap = "4.5.48" url = "2.5.7" chrono = "0.4.42" @@ -39,6 +46,10 @@ miette = "7.6.0" serde_json = "1.0.145" pythonize = "0.26.0" thiserror = "2.0.17" +fs-err = "3.1.3" +tracing = "0.1" +tracing-subscriber = "0.3" +indicatif = "0.18" [build-dependencies] pyo3-build-config = "0.26.0" diff --git a/py-rattler-build/examples/build_with_progress.py b/py-rattler-build/examples/build_with_progress.py new file mode 100644 index 000000000..4e98d1c98 --- /dev/null +++ b/py-rattler-build/examples/build_with_progress.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +""" +Example: Building a recipe with progress reporting using Rich. + +This example demonstrates how to use rattler-build's progress reporting +capabilities with the Rich library for beautiful terminal output. + +Usage: + python build_with_progress.py recipe.yaml + +Requirements: + pip install rich +""" + +import sys +from pathlib import Path +import tempfile + +# Import rattler_build components +from rattler_build.stage0 import Recipe +from rattler_build.variant_config import VariantConfig +from rattler_build.render import RenderConfig, render_recipe +from rattler_build.progress import RichProgressCallback, SimpleProgressCallback + + +def build_recipe_with_rich_progress(recipe_path: Path): + """Build a recipe with Rich progress display. + + Args: + recipe_path: Path to the recipe YAML file + """ + print(f"šŸ” Loading recipe from {recipe_path}") + + # Load the recipe + recipe = Recipe.from_file(str(recipe_path)) + print(f"āœ… Loaded recipe: {recipe.package.name} {recipe.package.version}") + + # Configure variant rendering + variant_config = VariantConfig() + # Set recipe_path so the build can find license files, etc. + render_config = RenderConfig(recipe_path=str(recipe_path.parent)) + + print("\nšŸ“‹ Rendering recipe variants...") + rendered_variants = render_recipe(recipe, variant_config, render_config) + print(f"āœ… Rendered {len(rendered_variants)} variant(s)") + + # Build each variant with progress reporting + for i, variant in enumerate(rendered_variants, 1): + print(f"\nšŸ”Ø Building variant {i}/{len(rendered_variants)}") + stage1_recipe = variant.recipe() + package = stage1_recipe.package + build = stage1_recipe.build + print(f" Package: {package.name}") + print(f" Version: {package.version}") + print(f" Build string: {build.string}") + + # Use Rich progress callback for beautiful output + # Set show_logs=True to see all log messages (recommended!) + with RichProgressCallback(show_logs=True) as callback: + print("\n" + "=" * 60) + print("Starting build with progress reporting...") + print("=" * 60 + "\n") + + # Build with real progress callbacks! + import tempfile + + with tempfile.TemporaryDirectory() as tmpdir: + variant.run_build( + progress_callback=callback, keep_build=False, output_dir=Path(tmpdir), recipe_path=recipe_path + ) + + print("\nāœ… Build complete!") + + +def build_recipe_with_simple_progress(recipe_path: Path): + """Build a recipe with simple console progress display. + + Args: + recipe_path: Path to the recipe YAML file + """ + print(f"šŸ” Loading recipe from {recipe_path}") + + # Load the recipe + recipe = Recipe.from_file(str(recipe_path)) + print(f"āœ… Loaded recipe: {recipe.package.name} {recipe.package.version}") + + # Configure and render + variant_config = VariantConfig() + # Set recipe_path so the build can find license files, etc. + render_config = RenderConfig(recipe_path=str(recipe_path.parent)) + + print("\nšŸ“‹ Rendering recipe variants...") + rendered_variants = render_recipe(recipe, variant_config, render_config) + print(f"āœ… Rendered {len(rendered_variants)} variant(s)") + + # Build with simple callback + callback = SimpleProgressCallback() + + for i, variant in enumerate(rendered_variants, 1): + print(f"\nšŸ”Ø Building variant {i}/{len(rendered_variants)}") + + # Build with real progress callbacks! + with tempfile.TemporaryDirectory() as tmpdir: + variant.run_build( + progress_callback=callback, keep_build=False, output_dir=Path(tmpdir), recipe_dir=recipe_path.parent + ) + + print("\nāœ… Build complete!") + + +def main(): + """Main entry point.""" + if len(sys.argv) < 2: + print("Usage: python build_with_progress.py [--simple]") + print("\nOptions:") + print(" --simple Use simple console output instead of Rich") + print("\nExamples:") + print(" python build_with_progress.py recipe.yaml") + print(" python build_with_progress.py recipe.yaml --simple") + sys.exit(1) + + recipe_path = Path(sys.argv[1]) + if not recipe_path.exists(): + print(f"Error: Recipe file not found: {recipe_path}") + sys.exit(1) + + use_simple = "--simple" in sys.argv + + try: + if use_simple: + build_recipe_with_simple_progress(recipe_path) + else: + try: + build_recipe_with_rich_progress(recipe_path) + except ImportError as e: + print(f"Rich library not available: {e}") + print("Falling back to simple progress...") + build_recipe_with_simple_progress(recipe_path) + + except Exception as e: + print(f"āŒ Error: {e}") + import traceback + + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/py-rattler-build/examples/recipe/simple.yaml b/py-rattler-build/examples/recipe/simple.yaml new file mode 100644 index 000000000..f1eb5c628 --- /dev/null +++ b/py-rattler-build/examples/recipe/simple.yaml @@ -0,0 +1,7 @@ +package: + name: foobar + version: 1.2.3 + +build: + script: + - echo "Building foobar version 1.2.3" diff --git a/py-rattler-build/pixi.lock b/py-rattler-build/pixi.lock index fe5c78561..44a642b6e 100644 --- a/py-rattler-build/pixi.lock +++ b/py-rattler-build/pixi.lock @@ -747,28 +747,45 @@ environments: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py38h17151c0_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.asyncio.runner-1.2.0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py310h8cfb67f_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.10.5-hbd8a1cb_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2024.8.30-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.0-py38heb5c249_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.1.7-unix_pyh707e725_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.1.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.0.0-pyh9f0ad1d_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.0.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.10.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py310he7384ee_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.3.0-pyh707e725_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.17-py310h25320af_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.11-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/inline-snapshot-0.31.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.1.0-pyha191276_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-8.37.0-pyh8f84b5b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1aa0949_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h9ec8514_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-h767d61c_7.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_7.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-h767d61c_7.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-devel-5.8.1-hb9d3cd8_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.20-h4ab18f5_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h8f9b012_7.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-h4852527_7.conda @@ -776,226 +793,339 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lua-5.4.8-h03e1676_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/mypy-1.5.1-py38h01eb140_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/mypy-1.5.1-py310h2372a71_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.4-h26f9b46_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-6.0.0-py38hfb59056_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.18.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha2e5f31_6.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-7.4.4-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-asyncio-0.21.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xprocess-0.23.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.8.20-h4a871b0_2_cpython.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.8-8_cp38.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-pyhd8ed1ab_1004.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.5.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.1.2-py310h139afa4_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.4.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-asyncio-1.2.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xprocess-1.0.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.10.19-h3c07f61_2_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.10-8_cp310.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-27.1.0-py310h4f33d48_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/rich-13.9.4-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typer-0.12.5-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-0.12.5-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-standard-0.12.5-hd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/types-requests-2.32.0.20241016-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/xz-5.8.1-hbcc6ac9_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/xz-gpl-tools-5.8.1-hbcc6ac9_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/xz-tools-5.8.1-hb9d3cd8_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.19.0-py38h0a891b7_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.2-py310h7c4b9e2_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typer-0.20.0-pyhdb1f59b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-0.20.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-standard-0.20.0-h65a100f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/types-requests-2.32.0.20250602-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.14-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h387f397_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.25.0-py310h139afa4_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda - conda: . subdir: linux-64 - - pypi: https://files.pythonhosted.org/packages/98/5d/5738903efe0ecb73e51eb44feafba32bdba2081263d40c5043568ff60faf/numpy-1.24.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/31/c1/d73ff5900c6b462879039ac92f89424ad1eb544b1f6bd77f12f9c3013e20/types_networkx-3.4.2.20241227-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/9a/41/cdd9498383b30290cce9de6dbed75fa75d6dc06fe4b47d6da6de4b156aa0/types_networkx-3.5.0.20251001-py3-none-any.whl osx-64: - - conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.1.0-py38h940360d_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.asyncio.runner-1.2.0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.2.0-py310h67da81d_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h500dc9f_8.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.10.5-hbd8a1cb_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2024.8.30-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/cffi-1.17.0-py38hc8bcfa4_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.1.7-unix_pyh707e725_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/h2-4.1.0-py38h50d1736_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.0.0-pyh9f0ad1d_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.0.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.10.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/cffi-2.0.0-py310h062c7ae_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.3.0-pyh707e725_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/debugpy-1.8.17-py310h26e2fd1_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.11-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/inline-snapshot-0.31.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.1.0-pyh5552912_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-8.37.0-pyh8f84b5b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/krb5-1.21.3-h37d8d59_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-21.1.4-h3d58e20_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libedit-3.1.20250104-pl5321ha958ccf_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.7.1-h21dd04a_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.5.2-h750e83c_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.1-hd471939_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-devel-5.8.1-hd471939_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libsodium-1.0.20-hfdf4475_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.50.4-h39a8b3b_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.1-hd23fc13_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/lua-5.4.8-h07ffd45_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/mypy-1.5.1-py38hcafd530_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/mypy-1.5.1-py310h6729b98_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.5-h0622a9a_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.5.4-h230baf5_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/psutil-6.0.0-py38hc718529_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.18.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha2e5f31_6.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-7.4.4-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-asyncio-0.21.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xprocess-0.23.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.8.20-h4f978b9_2_cpython.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.8-8_cp38.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-pyhd8ed1ab_1004.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.5.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/psutil-7.1.2-py310h3aa7efa_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.4.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-asyncio-1.2.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xprocess-1.0.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.10.19-h988dfef_2_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.10-8_cp310.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pyzmq-27.1.0-py310hbbd5e6a_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h7cca4af_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/rich-13.9.4-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-hf689a15_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typer-0.12.5-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-0.12.5-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-standard-0.12.5-hd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/types-requests-2.32.0.20241016-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/xz-5.8.1-h357f2ed_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/xz-gpl-tools-5.8.1-h357f2ed_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/xz-tools-5.8.1-hd471939_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/zstandard-0.23.0-py38hdb7df32_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.6-h915ae27_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/tornado-6.5.2-py310h1b7cace_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typer-0.20.0-pyhdb1f59b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-0.20.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-standard-0.20.0-h65a100f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/types-requests-2.32.0.20250602-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.14-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/zeromq-4.3.5-h6c33b1e_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/zstandard-0.25.0-py310h3aa7efa_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h8210216_2.conda - conda: . subdir: osx-64 - - pypi: https://files.pythonhosted.org/packages/11/10/943cfb579f1a02909ff96464c69893b1d25be3731b5d3652c2e0cf1281ea/numpy-1.24.4-cp38-cp38-macosx_10_9_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/31/c1/d73ff5900c6b462879039ac92f89424ad1eb544b1f6bd77f12f9c3013e20/types_networkx-3.4.2.20241227-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/9a/41/cdd9498383b30290cce9de6dbed75fa75d6dc06fe4b47d6da6de4b156aa0/types_networkx-3.5.0.20251001-py3-none-any.whl osx-arm64: - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.1.0-py38he333c0f_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.asyncio.runner-1.2.0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py310haaa1140_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_8.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.10.5-hbd8a1cb_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2024.8.30-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-1.17.0-py38h858044d_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.1.7-unix_pyh707e725_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.1.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.0.0-pyh9f0ad1d_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.0.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.10.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-2.0.0-py310hf5b66c1_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.3.0-pyh707e725_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/debugpy-1.8.17-py310h7b404bc_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.11-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/inline-snapshot-0.31.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.1.0-pyh5552912_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-8.37.0-pyh8f84b5b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.21.3-h237132a_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-21.1.4-hf598326_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.1-hec049ff_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-he5f378a_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.1-h39f12f2_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-devel-5.8.1-h39f12f2_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.20-h99b78c6_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.50.4-h4237e3c_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lua-5.4.8-h15fa0ee_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/mypy-1.5.1-py38hb192615_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/mypy-1.5.1-py310h2aa6e3c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.5.4-h5503f6c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-6.0.0-py38h3237794_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.18.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha2e5f31_6.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-7.4.4-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-asyncio-0.21.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xprocess-0.23.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.8.20-h7d35d02_2_cpython.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.8-8_cp38.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-pyhd8ed1ab_1004.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.5.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.1.2-py310hf151d32_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.4.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-asyncio-1.2.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xprocess-1.0.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.10.19-hcd7f573_2_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.10-8_cp310.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyzmq-27.1.0-py310hc4a7dca_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h1d1bf99_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/rich-13.9.4-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typer-0.12.5-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-0.12.5-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-standard-0.12.5-hd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/types-requests-2.32.0.20241016-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-5.8.1-h9a6d368_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-gpl-tools-5.8.1-h9a6d368_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-tools-5.8.1-h39f12f2_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstandard-0.23.0-py38h43bb1b3_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.6-hb46c0d2_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.2-py310h7bdd564_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typer-0.20.0-pyhdb1f59b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-0.20.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-standard-0.20.0-h65a100f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/types-requests-2.32.0.20250602-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.14-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h888dc83_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstandard-0.25.0-py310hf151d32_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-h6491c7d_2.conda - conda: . subdir: osx-arm64 - - pypi: https://files.pythonhosted.org/packages/a7/ae/f53b7b265fdc701e663fbb322a8e9d4b14d9cb7b2385f45ddfabfc4327e4/numpy-1.24.4-cp38-cp38-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/31/c1/d73ff5900c6b462879039ac92f89424ad1eb544b1f6bd77f12f9c3013e20/types_networkx-3.4.2.20241227-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/9a/41/cdd9498383b30290cce9de6dbed75fa75d6dc06fe4b47d6da6de4b156aa0/types_networkx-3.5.0.20251001-py3-none-any.whl win-64: - - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.1.0-py38hd3f51b4_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.asyncio.runner-1.2.0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py310h8abc2a3_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_8.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.10.5-h4c7d964_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2024.8.30-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-1.17.0-py38h4cb3324_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.1.7-win_pyh7428d3b_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.1.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.0.0-pyh9f0ad1d_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.0.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.10.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.0.0-py310h29418f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.3.0-pyh7428d3b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.17-py310h699e580_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.11-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/inline-snapshot-0.31.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.1.0-pyh6dadd2b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-8.37.0-pyha7b4d00_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyh6dadd2b_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.21.3-hdf4eb48_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.1-hac47afa_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h52bdfb6_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.1-h2466b09_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-devel-5.8.1-h2466b09_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.20-hc70643c_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.50.4-hf5d6505_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lua-5.4.8-h1839187_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/mypy-1.5.1-py38h91455d4_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/mypy-1.5.1-py310h8d17308_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.5.4-h725018a_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-6.0.0-py38h4cb3324_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.18.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh0701188_6.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-7.4.4-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-asyncio-0.21.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xprocess-0.23.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.8.20-hfaddaf0_2_cpython.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.8-8_cp38.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/rich-13.9.4-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-pyhd8ed1ab_1004.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.5.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.1.2-py310h1637853_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.4.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-asyncio-1.2.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xprocess-1.0.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.10.19-hc20f281_2_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.10-8_cp310.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-311-py310h282bd7d_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-27.1.0-py310h535538e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h2c6b04d_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typer-0.12.5-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-0.12.5-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-standard-0.12.5-hd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/types-requests-2.32.0.20241016-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.2-py310h29418f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typer-0.20.0-pyhdb1f59b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-0.20.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-standard-0.20.0-h65a100f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/types-requests-2.32.0.20250602-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h2b53caa_32.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_32.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_32.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_7.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/xz-5.8.1-h208afaa_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/xz-tools-5.8.1-h2466b09_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/zstandard-0.23.0-py38hf92978b_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.6-h0ea2cb4_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.14-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h5bddc39_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zstandard-0.25.0-py310h1637853_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-hbeecb71_2.conda - conda: . subdir: win-64 - - pypi: https://files.pythonhosted.org/packages/69/65/0d47953afa0ad569d12de5f65d964321c208492064c38fe3b0b9744f8d44/numpy-1.24.4-cp38-cp38-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/31/c1/d73ff5900c6b462879039ac92f89424ad1eb544b1f6bd77f12f9c3013e20/types_networkx-3.4.2.20241227-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/9a/41/cdd9498383b30290cce9de6dbed75fa75d6dc06fe4b47d6da6de4b156aa0/types_networkx-3.5.0.20251001-py3-none-any.whl packages: - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 sha256: fe51de6107f9edc7aa4f786a70f4a883943bc9d39b3bb7307c04c41410990726 @@ -1032,6 +1162,30 @@ packages: license_family: BSD size: 49468 timestamp: 1718213032772 +- conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda + sha256: 8f032b140ea4159806e4969a68b4a3c0a7cab1ad936eb958a2b5ffe5335e19bf + md5: 54898d0f524c9dee622d44bbb081a8ab + depends: + - python >=3.9 + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/appnope?source=hash-mapping + size: 10076 + timestamp: 1733332433806 +- conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.0-pyhd8ed1ab_1.conda + sha256: 93b14414b3b3ed91e286e1cbe4e7a60c4e1b1c730b0814d1e452a8ac4b9af593 + md5: 8f587de4bcf981e26228f268df374a9b + depends: + - python >=3.9 + constrains: + - astroid >=2,<4 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/asttokens?source=hash-mapping + size: 28206 + timestamp: 1733250564754 - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda sha256: 1c656a35800b7f57f7371605bc6507c8d3ad60fbaaec65876fce7f73df1fc8ac md5: 0a01c169f0ab0f91b26e77a3301fbfe4 @@ -1040,8 +1194,24 @@ packages: - pytz >=2015.7 license: BSD-3-Clause license_family: BSD + purls: + - pkg:pypi/babel?source=hash-mapping size: 6938256 timestamp: 1738490268466 +- conda: https://conda.anaconda.org/conda-forge/noarch/backports.asyncio.runner-1.2.0-pyhe01879c_2.conda + sha256: 58e37414e7e0eafc71062a944925c750a25f56a402d6dd5cd9b201b263c5f325 + md5: 6ad331fe989595229e3272d81bb12b7f + depends: + - python >=3.9 + - python + constrains: + - python >=3.9,<3.11 + license: PSF-2.0 + license_family: PSF + purls: + - pkg:pypi/backports-asyncio-runner?source=hash-mapping + size: 43275 + timestamp: 1753456387170 - conda: https://conda.anaconda.org/conda-forge/noarch/backrefs-5.8-pyhd8ed1ab_0.conda sha256: 3a0af23d357a07154645c41d035a4efbd15b7a642db397fa9ea0193fd58ae282 md5: b16e2595d3a9042aa9d570375978835f @@ -1062,22 +1232,23 @@ packages: license_family: GPL size: 3715763 timestamp: 1761335193784 -- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py38h17151c0_1.conda - sha256: f932ae77f10885dd991b0e1f56f6effea9f19b169e8606dab0bdafd0e44db3c9 - md5: 7a5a699c8992fc51ef25e980f4502c2a +- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py310h8cfb67f_0.conda + sha256: ec60f83061182a5587bf0c249dbaa28426c7ddd2d16f0a91735767faf7173941 + md5: 12f24867bc0ec4e15c89cdff988c500e depends: - - libgcc-ng >=12 - - libstdcxx-ng >=12 - - python >=3.8,<3.9.0a0 - - python_abi 3.8.* *_cp38 + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + - python >=3.10,<3.11.0a0 + - python_abi 3.10.* *_cp310 constrains: - - libbrotlicommon 1.1.0 hd590300_1 + - libbrotlicommon 1.2.0 h09219d5_0 license: MIT license_family: MIT purls: - pkg:pypi/brotli?source=hash-mapping - size: 350830 - timestamp: 1695990250755 + size: 367955 + timestamp: 1761592267690 - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py312h67db365_0.conda sha256: 1acccd5464d81184ead80c017b4a7320c59c2774eb914f14d60ca8b4c55754e9 md5: 7c9245551ebbe6b6068aeda04060afaa @@ -1108,21 +1279,22 @@ packages: license_family: MIT size: 368319 timestamp: 1761592337171 -- conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.1.0-py38h940360d_1.conda - sha256: 0a088bff62ddd2e505bdc80cc16da009c134b9ccfa6352b0cfe9d4eeed27d8c2 - md5: ad8d4ae4e8351a2fc0fe92f13bd266d8 +- conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.2.0-py310h67da81d_0.conda + sha256: fe6b7b42e5c1f9b01addfc390c511f6379bdf32760c55afbaf9f051de8871930 + md5: 7efadd87624c40def266764a8e14d294 depends: - - libcxx >=15.0.7 - - python >=3.8,<3.9.0a0 - - python_abi 3.8.* *_cp38 + - __osx >=10.13 + - libcxx >=19 + - python >=3.10,<3.11.0a0 + - python_abi 3.10.* *_cp310 constrains: - - libbrotlicommon 1.1.0 h0dc2134_1 + - libbrotlicommon 1.2.0 h105ed1c_0 license: MIT license_family: MIT purls: - pkg:pypi/brotli?source=hash-mapping - size: 366343 - timestamp: 1695990788245 + size: 389150 + timestamp: 1761593458217 - conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.2.0-py312hbe43a26_0.conda sha256: e875e1ed3dff4aff75a98550a41b35ecabb441b4e63bb54c430ae2ec24df1b61 md5: 14f2d0cfd53648f191c3928cf34e8563 @@ -1151,22 +1323,23 @@ packages: license_family: MIT size: 389830 timestamp: 1761593069187 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.1.0-py38he333c0f_1.conda - sha256: 3fd1e0a4b7ea1b20f69bbc2d74c798f3eebd775ccbcdee170f68b1871f8bbb74 - md5: 29160c74d5977b1c5ecd654b00d576f0 - depends: - - libcxx >=15.0.7 - - python >=3.8,<3.9.0a0 - - python >=3.8,<3.9.0a0 *_cpython - - python_abi 3.8.* *_cp38 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py310haaa1140_0.conda + sha256: b2a5680a869062bb8b4afe714b5b11a131d3a0c01919569c0c2fdb1ae21565b8 + md5: 140410c8c38b32299441d892336bbfdd + depends: + - __osx >=11.0 + - libcxx >=19 + - python >=3.10,<3.11.0a0 + - python >=3.10,<3.11.0a0 *_cpython + - python_abi 3.10.* *_cp310 constrains: - - libbrotlicommon 1.1.0 hb547adb_1 + - libbrotlicommon 1.2.0 h87ba0bc_0 license: MIT license_family: MIT purls: - pkg:pypi/brotli?source=hash-mapping - size: 343036 - timestamp: 1695990970956 + size: 359380 + timestamp: 1761592820004 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py312hcae0c51_0.conda sha256: 82bbb9a4430d639f0dc251d0a0a20dd661731dc6798481951dfa2e94ca3f191d md5: 17e32d91d89e91631f97c217f192483b @@ -1197,23 +1370,23 @@ packages: license_family: MIT size: 359535 timestamp: 1761592749203 -- conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.1.0-py38hd3f51b4_1.conda - sha256: a292d6b3118ef284cc03a99a6efe5e08ca3a6d0e37eff78eb8d87cfca3830d7b - md5: 72708ea626a2530148ea49eb743576f4 +- conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py310h8abc2a3_0.conda + sha256: cf035a1ed88651130c4dc76de4578c51d867f7b5bd3f41eddceb9c9440c63527 + md5: cae22b07f9c82ec3762e8c5140e3b580 depends: - - python >=3.8,<3.9.0a0 - - python_abi 3.8.* *_cp38 + - python >=3.10,<3.11.0a0 + - python_abi 3.10.* *_cp310 - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 constrains: - - libbrotlicommon 1.1.0 hcfcfb64_1 + - libbrotlicommon 1.2.0 hc82b238_0 license: MIT license_family: MIT purls: - - pkg:pypi/brotli?source=hash-mapping - size: 321650 - timestamp: 1695990817828 + - pkg:pypi/brotli?source=compressed-mapping + size: 335512 + timestamp: 1761593007322 - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py312h9d5906e_0.conda sha256: 48ffd069cab4b3b294daeb90e2536dafed5fe0a8476bc9fdcaa9924b691568f8 md5: 33b94eb79455950e69771bdd22db2988 @@ -1410,40 +1583,32 @@ packages: license_family: LGPL size: 44030 timestamp: 1735230287782 -- conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2024.8.30-pyhd8ed1ab_0.conda - sha256: 7020770df338c45ac6b560185956c32f0a5abf4b76179c037f115fc7d687819f - md5: 12f7d00853807b0531775e9be891cb11 - depends: - - python >=3.7 - license: ISC - purls: - - pkg:pypi/certifi?source=hash-mapping - size: 163752 - timestamp: 1725278204397 - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.10.5-pyhd8ed1ab_0.conda sha256: 955bac31be82592093f6bc006e09822cd13daf52b28643c9a6abd38cd5f4a306 md5: 257ae203f1d204107ba389607d375ded depends: - python >=3.10 license: ISC + purls: + - pkg:pypi/certifi?source=hash-mapping size: 160248 timestamp: 1759648987029 -- conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.0-py38heb5c249_0.conda - sha256: 04f394dbebcf09f845b95e63691aa20d8fc0e65cc94c78eb334c184335788cf8 - md5: b41a6ee3a5b044185b5f46aaf6082388 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py310he7384ee_1.conda + sha256: bf76ead6d59b70f3e901476a73880ac92011be63b151972d135eec55bbbe6091 + md5: 803e2d778b8dcccdc014127ec5001681 depends: - __glibc >=2.17,<3.0.a0 - - libffi >=3.4,<4.0a0 - - libgcc-ng >=12 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 - pycparser - - python >=3.8,<3.9.0a0 - - python_abi 3.8.* *_cp38 + - python >=3.10,<3.11.0a0 + - python_abi 3.10.* *_cp310 license: MIT license_family: MIT purls: - - pkg:pypi/cffi?source=hash-mapping - size: 240463 - timestamp: 1723018485509 + - pkg:pypi/cffi?source=compressed-mapping + size: 244766 + timestamp: 1761203011221 - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py312h460c074_1.conda sha256: 7dafe8173d5f94e46cf9cd597cc8ff476a8357fbbd4433a8b5697b2864845d9c md5: 648ee28dcd4e07a1940a17da62eccd40 @@ -1472,21 +1637,21 @@ packages: license_family: MIT size: 300271 timestamp: 1761203085220 -- conda: https://conda.anaconda.org/conda-forge/osx-64/cffi-1.17.0-py38hc8bcfa4_0.conda - sha256: dfa9d415301f9431bc298869687d76abbeded8aff27a258ce28acaf175a47e1e - md5: d2a8f77ea8b928779611d1072e9ff89d +- conda: https://conda.anaconda.org/conda-forge/osx-64/cffi-2.0.0-py310h062c7ae_1.conda + sha256: 0a3356efb56eab922d212bbe1448077a9108b809ea8b7270f69c329cae279c48 + md5: c78bd9e0015204ae349a555f957b544d depends: - __osx >=10.13 - - libffi >=3.4,<4.0a0 + - libffi >=3.5.2,<3.6.0a0 - pycparser - - python >=3.8,<3.9.0a0 - - python_abi 3.8.* *_cp38 + - python >=3.10,<3.11.0a0 + - python_abi 3.10.* *_cp310 license: MIT license_family: MIT purls: - pkg:pypi/cffi?source=hash-mapping - size: 228572 - timestamp: 1723018629051 + size: 236897 + timestamp: 1761203176395 - conda: https://conda.anaconda.org/conda-forge/osx-64/cffi-2.0.0-py312he90777b_1.conda sha256: e2888785e50ef99c63c29fb3cfbfb44cdd50b3bb7cd5f8225155e362c391936f md5: cf70c8244e7ceda7e00b1881ad7697a9 @@ -1513,22 +1678,22 @@ packages: license_family: MIT size: 293633 timestamp: 1761203106369 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-1.17.0-py38h858044d_0.conda - sha256: c3c1486d52eac829dfca9d7d3d5596fea66e52dccc9b33d5d7b9acee276935af - md5: 792d275788105bf78189bb55dc1e9c76 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-2.0.0-py310hf5b66c1_1.conda + sha256: 9a629f09b734795127b63b4880172e243fb2539107bbdd0203f3cd638fa131e3 + md5: 4e0516a8b6f96414d867af0228237a43 depends: - __osx >=11.0 - - libffi >=3.4,<4.0a0 + - libffi >=3.5.2,<3.6.0a0 - pycparser - - python >=3.8,<3.9.0a0 - - python >=3.8,<3.9.0a0 *_cpython - - python_abi 3.8.* *_cp38 + - python >=3.10,<3.11.0a0 + - python >=3.10,<3.11.0a0 *_cpython + - python_abi 3.10.* *_cp310 license: MIT license_family: MIT purls: - pkg:pypi/cffi?source=hash-mapping - size: 227934 - timestamp: 1723018694952 + size: 236349 + timestamp: 1761203587122 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-2.0.0-py312h1b4d9a2_1.conda sha256: 597e986ac1a1bd1c9b29d6850e1cdea4a075ce8292af55568952ec670e7dd358 md5: 503ac138ad3cfc09459738c0f5750705 @@ -1557,22 +1722,22 @@ packages: license_family: MIT size: 292983 timestamp: 1761203354051 -- conda: https://conda.anaconda.org/conda-forge/win-64/cffi-1.17.0-py38h4cb3324_0.conda - sha256: 09e73cc77c995d608647cf0e999790155c6adfd69554b148550d6acd8dc5fefc - md5: da2c9ba02e759024e994d0ecce7c860b +- conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.0.0-py310h29418f3_1.conda + sha256: abd04b75ee9a04a2f00dc102b4dc126f393fde58536ca4eaf1a72bb7d60dadf4 + md5: 269ba3d69bf6569296a29425a26400df depends: - pycparser - - python >=3.8,<3.9.0a0 - - python_abi 3.8.* *_cp38 + - python >=3.10,<3.11.0a0 + - python_abi 3.10.* *_cp310 - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 license: MIT license_family: MIT purls: - - pkg:pypi/cffi?source=hash-mapping - size: 236272 - timestamp: 1723018796385 + - pkg:pypi/cffi?source=compressed-mapping + size: 239862 + timestamp: 1761203282977 - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.0.0-py312he06e257_1.conda sha256: 3e3bdcb85a2e79fe47d9c8ce64903c76f663b39cb63b8e761f6f884e76127f82 md5: 46f7dccfee37a52a97c0ed6f33fcf0a3 @@ -1601,17 +1766,6 @@ packages: license_family: MIT size: 294731 timestamp: 1761203441365 -- conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.0-pyhd8ed1ab_0.conda - sha256: 1873ac45ea61f95750cb0b4e5e675d1c5b3def937e80c7eebb19297f76810be8 - md5: a374efa97290b8799046df7c5ca17164 - depends: - - python >=3.7 - license: MIT - license_family: MIT - purls: - - pkg:pypi/charset-normalizer?source=hash-mapping - size: 47314 - timestamp: 1728479405343 - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.4-pyhd8ed1ab_0.conda sha256: b32f8362e885f1b8417bac2b3da4db7323faa12d5db62b7fd6691c02d60d6f59 md5: a22d1fd9bf98827e280a02875d9a007a @@ -1619,33 +1773,10 @@ packages: - python >=3.10 license: MIT license_family: MIT + purls: + - pkg:pypi/charset-normalizer?source=compressed-mapping size: 50965 timestamp: 1760437331772 -- conda: https://conda.anaconda.org/conda-forge/noarch/click-8.1.7-unix_pyh707e725_0.conda - sha256: f0016cbab6ac4138a429e28dbcb904a90305b34b3fe41a9b89d697c90401caec - md5: f3ad426304898027fc619827ff428eca - depends: - - __unix - - python >=3.8 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/click?source=hash-mapping - size: 84437 - timestamp: 1692311973840 -- conda: https://conda.anaconda.org/conda-forge/noarch/click-8.1.7-win_pyh7428d3b_0.conda - sha256: 90236b113b9a20041736e80b80ee965167f9aac0468315c55e2bad902d673fb0 - md5: 3549ecbceb6cd77b91a105511b7d0786 - depends: - - __win - - colorama - - python >=3.8 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/click?source=hash-mapping - size: 85051 - timestamp: 1692312207348 - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.3.0-pyh707e725_0.conda sha256: c6567ebc27c4c071a353acaf93eb82bb6d9a6961e40692a359045a89a61d02c0 md5: e76c4ba9e1837847679421b8d549b784 @@ -1654,6 +1785,8 @@ packages: - python >=3.10 license: BSD-3-Clause license_family: BSD + purls: + - pkg:pypi/click?source=compressed-mapping size: 91622 timestamp: 1758270534287 - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.3.0-pyh7428d3b_0.conda @@ -1665,19 +1798,10 @@ packages: - python >=3.10 license: BSD-3-Clause license_family: BSD + purls: + - pkg:pypi/click?source=hash-mapping size: 92148 timestamp: 1758270588199 -- conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2 - sha256: 2c1b2e9755ce3102bca8d69e8f26e4f087ece73f50418186aee7c74bef8e1698 - md5: 3faab06a954c2a04039983f2c4a50d99 - depends: - - python >=3.7 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/colorama?source=hash-mapping - size: 25170 - timestamp: 1666700778190 - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda sha256: ab29d57dc70786c1269633ba3dff20288b81664d3ff8d21af995742e2bb03287 md5: 962b9857ee8e7018c22f2776ffa0b2d7 @@ -1685,8 +1809,22 @@ packages: - python >=3.9 license: BSD-3-Clause license_family: BSD + purls: + - pkg:pypi/colorama?source=hash-mapping size: 27011 timestamp: 1733218222191 +- conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + sha256: 576a44729314ad9e4e5ebe055fbf48beb8116b60e58f9070278985b2b634f212 + md5: 2da13f2b299d8e1995bafbbe9689a2f7 + depends: + - python >=3.9 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/comm?source=hash-mapping + size: 14690 + timestamp: 1753453984907 - conda: https://conda.anaconda.org/conda-forge/noarch/cssselect2-0.8.0-pyhd8ed1ab_0.conda sha256: 0a6728d77e337fd5b543765b0cd05eda996b63f4ef0c1bb34a02d78a7d123a68 md5: 504bf822bea0f84547fb31e41de19714 @@ -1698,6 +1836,80 @@ packages: license_family: BSD size: 20425 timestamp: 1751498485591 +- conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.17-py310h25320af_0.conda + sha256: fa33b347b22f94cb5814dc263755ad6c3d50e1b3046c8629aec87c867e46b636 + md5: df12e1e922f79a4a407bc9566e9fba3f + depends: + - python + - libstdcxx >=14 + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python_abi 3.10.* *_cp310 + license: MIT + license_family: MIT + purls: + - pkg:pypi/debugpy?source=hash-mapping + size: 2231656 + timestamp: 1758162044990 +- conda: https://conda.anaconda.org/conda-forge/osx-64/debugpy-1.8.17-py310h26e2fd1_0.conda + sha256: 55d8affcff3f15ee83a35b481f35db772e54d99485a6adec0a4561750f283d08 + md5: e6498f2198c60a2950a3a666af3738d2 + depends: + - python + - __osx >=10.13 + - libcxx >=19 + - python_abi 3.10.* *_cp310 + license: MIT + license_family: MIT + purls: + - pkg:pypi/debugpy?source=hash-mapping + size: 2226758 + timestamp: 1758162102722 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/debugpy-1.8.17-py310h7b404bc_0.conda + sha256: 4269e7416b3df61f6798d49784b9d8c5eb097b51d1bce97910174add3d9ceeaa + md5: c0dd237d0d775eb7a1291aa44e17d2df + depends: + - python + - python 3.10.* *_cpython + - __osx >=11.0 + - libcxx >=19 + - python_abi 3.10.* *_cp310 + license: MIT + license_family: MIT + purls: + - pkg:pypi/debugpy?source=hash-mapping + size: 2222667 + timestamp: 1758162070598 +- conda: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.17-py310h699e580_0.conda + sha256: c042d64a510cd3fb95431e5cc21e8d3c7adcaeac75f46f84b6f67acc9a0f1d33 + md5: c5f45e2388843736453e689720338930 + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.10.* *_cp310 + license: MIT + license_family: MIT + purls: + - pkg:pypi/debugpy?source=hash-mapping + size: 3479629 + timestamp: 1758162070795 +- conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + sha256: c17c6b9937c08ad63cb20a26f403a3234088e57d4455600974a0ce865cb14017 + md5: 9ce473d1d1be1cc3810856a48b3fab32 + depends: + - python >=3.9 + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/decorator?source=hash-mapping + size: 14129 + timestamp: 1740385067843 - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 sha256: 9717a059677553562a8f38ff07f3b9f61727bd614f505658b0a5ecbcf8df89be md5: 961b3a227b437d82ad7054484cfa71b2 @@ -1705,18 +1917,32 @@ packages: - python >=3.6 license: PSF-2.0 license_family: PSF + purls: + - pkg:pypi/defusedxml?source=hash-mapping size: 24062 timestamp: 1615232388757 -- conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_0.conda - sha256: e0edd30c4b7144406bb4da975e6bb97d6bc9c0e999aa4efe66ae108cada5d5b5 - md5: d02ae936e42063ca46af6cdad2dbd1e0 +- conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.0-pyhd8ed1ab_0.conda + sha256: ce61f4f99401a4bd455b89909153b40b9c823276aefcbb06f2044618696009ca + md5: 72e42d28960d875c7654614f8b50939a depends: - - python >=3.7 + - python >=3.9 + - typing_extensions >=4.6.0 license: MIT and PSF-2.0 purls: - pkg:pypi/exceptiongroup?source=hash-mapping - size: 20418 - timestamp: 1720869435725 + size: 21284 + timestamp: 1746947398083 +- conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + sha256: 210c8165a58fdbf16e626aac93cc4c14dbd551a01d1516be5ecad795d2422cad + md5: ff9efb7f7469aed3c4a8106ffa29593c + depends: + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/executing?source=hash-mapping + size: 30753 + timestamp: 1756729456476 - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 sha256: 58d7f40d2940dd0a8aa28651239adbf5613254df0f75789919c4e6762054403b md5: 0c96522c6bdaed4b1566d11387caaf45 @@ -1889,19 +2115,6 @@ packages: license: ISC size: 110009 timestamp: 1757138166768 -- conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.1.0-pyhd8ed1ab_0.tar.bz2 - sha256: bfc6a23849953647f4e255c782e74a0e18fe16f7e25c7bb0bc57b83bb6762c7a - md5: b748fbf7060927a6e82df7cb5ee8f097 - depends: - - hpack >=4.0,<5 - - hyperframe >=6.0,<7 - - python >=3.6.1 - license: MIT - license_family: MIT - purls: - - pkg:pypi/h2?source=hash-mapping - size: 46754 - timestamp: 1634280590080 - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda sha256: 84c64443368f84b600bfecc529a1194a3b14c3656ee2e832d15a20e0329b6da3 md5: 164fc43f0b53b6e3a7bc7dce5e4f1dc9 @@ -1912,33 +2125,10 @@ packages: - python license: MIT license_family: MIT + purls: + - pkg:pypi/h2?source=compressed-mapping size: 95967 timestamp: 1756364871835 -- conda: https://conda.anaconda.org/conda-forge/osx-64/h2-4.1.0-py38h50d1736_0.tar.bz2 - sha256: 604652f41e0dd787fb1afebec7931d8b7a884619a9192914e3fba0a924e81a87 - md5: 908f24caf3e1cf2de08627281914678b - depends: - - hpack >=4.0,<5 - - hyperframe >=6.0,<7 - - python >=3.8,<3.9.0a0 - - python_abi 3.8.* *_cp38 - license: MIT - license_family: MIT - purls: - - pkg:pypi/h2?source=hash-mapping - size: 78843 - timestamp: 1633503129133 -- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.0.0-pyh9f0ad1d_0.tar.bz2 - sha256: 5dec948932c4f740674b1afb551223ada0c55103f4c7bf86a110454da3d27cb8 - md5: 914d6646c4dbb1fd3ff539830a12fd71 - depends: - - python - license: MIT - license_family: MIT - purls: - - pkg:pypi/hpack?source=hash-mapping - size: 25341 - timestamp: 1598856368685 - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda sha256: 6ad78a180576c706aabeb5b4c8ceb97c0cb25f1e112d76495bff23e3779948ba md5: 0a802cb9888dd14eeefc611f05c40b6e @@ -1946,19 +2136,10 @@ packages: - python >=3.9 license: MIT license_family: MIT + purls: + - pkg:pypi/hpack?source=hash-mapping size: 30731 timestamp: 1737618390337 -- conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.0.1-pyhd8ed1ab_0.tar.bz2 - sha256: e374a9d0f53149328134a8d86f5d72bca4c6dcebed3c0ecfa968c02996289330 - md5: 9f765cbfab6870c8435b9eefecd7a1f4 - depends: - - python >=3.6 - license: MIT - license_family: MIT - purls: - - pkg:pypi/hyperframe?source=hash-mapping - size: 14646 - timestamp: 1619110249723 - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda sha256: 77af6f5fe8b62ca07d09ac60127a30d9069fdc3c68d6b256754d0ffb1f7779f8 md5: 8e6923fc12f1fe8f8c4e5c9f343256ac @@ -1966,6 +2147,8 @@ packages: - python >=3.9 license: MIT license_family: MIT + purls: + - pkg:pypi/hyperframe?source=hash-mapping size: 17397 timestamp: 1737618427549 - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda @@ -2009,17 +2192,6 @@ packages: license_family: MIT size: 14544252 timestamp: 1720853966338 -- conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_0.conda - sha256: 8c57fd68e6be5eecba4462e983aed7e85761a519aab80e834bbd7794d4b545b2 - md5: 7ba2ede0e7c795ff95088daf0dc59753 - depends: - - python >=3.6 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/idna?source=hash-mapping - size: 49837 - timestamp: 1726459583613 - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.11-pyhd8ed1ab_0.conda sha256: ae89d0299ada2a3162c2614a9d26557a92aa6a77120ce142f8e0109bbf0342b0 md5: 53abe63df7e10a6ba605dc5f9f961d36 @@ -2027,6 +2199,8 @@ packages: - python >=3.10 license: BSD-3-Clause license_family: BSD + purls: + - pkg:pypi/idna?source=hash-mapping size: 50721 timestamp: 1760286526795 - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda @@ -2038,19 +2212,181 @@ packages: - python license: Apache-2.0 license_family: APACHE + purls: + - pkg:pypi/importlib-metadata?source=hash-mapping size: 34641 timestamp: 1747934053147 -- conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda - sha256: 38740c939b668b36a50ef455b077e8015b8c9cf89860d421b3fff86048f49666 - md5: f800d2da156d08e289b14e87e43c1ae5 +- conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + sha256: e1a9e3b1c8fe62dc3932a616c284b5d8cbe3124bbfbedcf4ce5c828cb166ee19 + md5: 9614359868482abba1bd15ce465e3c42 depends: - - python >=3.7 + - python >=3.10 license: MIT license_family: MIT purls: - - pkg:pypi/iniconfig?source=hash-mapping - size: 11101 - timestamp: 1673103208955 + - pkg:pypi/iniconfig?source=compressed-mapping + size: 13387 + timestamp: 1760831448842 +- conda: https://conda.anaconda.org/conda-forge/noarch/inline-snapshot-0.31.0-pyhcf101f3_0.conda + sha256: c1859345fca88d791d508cb76eac1608419d38dc34a7504d44c84ba59b6ba060 + md5: 870173caaa6c482f9ff982cda1056a0e + depends: + - python >=3.10 + - asttokens >=2.0.5 + - executing >=2.2.0 + - rich >=13.7.1 + - tomli >=2.0.0 + - pytest >=8.3.4 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/inline-snapshot?source=hash-mapping + size: 59957 + timestamp: 1761568023473 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.1.0-pyh5552912_0.conda + sha256: b5f7eaba3bb109be49d00a0a8bda267ddf8fa66cc1b54fc5944529ed6f3e8503 + md5: 1849eec35b60082d2bd66b4e36dec2b6 + depends: + - appnope + - __osx + - comm >=0.1.1 + - debugpy >=1.6.5 + - ipython >=7.23.1 + - jupyter_client >=8.0.0 + - jupyter_core >=4.12,!=5.0.* + - matplotlib-inline >=0.1 + - nest-asyncio >=1.4 + - packaging >=22 + - psutil >=5.7 + - python >=3.10 + - pyzmq >=25 + - tornado >=6.2 + - traitlets >=5.4.0 + - python + constrains: + - appnope >=0.1.2 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/ipykernel?source=compressed-mapping + size: 132289 + timestamp: 1761567969884 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.1.0-pyh6dadd2b_0.conda + sha256: 75e42103bc3350422896f727041e24767795b214a20f50bf39c371626b8aae8b + md5: f22cb16c5ad68fd33d0f65c8739b6a06 + depends: + - python + - __win + - comm >=0.1.1 + - debugpy >=1.6.5 + - ipython >=7.23.1 + - jupyter_client >=8.0.0 + - jupyter_core >=4.12,!=5.0.* + - matplotlib-inline >=0.1 + - nest-asyncio >=1.4 + - packaging >=22 + - psutil >=5.7 + - python >=3.10 + - pyzmq >=25 + - tornado >=6.2 + - traitlets >=5.4.0 + - python + constrains: + - appnope >=0.1.2 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/ipykernel?source=hash-mapping + size: 132418 + timestamp: 1761567966860 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.1.0-pyha191276_0.conda + sha256: a9d6b74115dbd62e19017ff8fa4885b07b5164427f262cc15b5307e5aaf3ee73 + md5: c6f63cfe66adaa5650788e3106b6683a + depends: + - python + - __linux + - comm >=0.1.1 + - debugpy >=1.6.5 + - ipython >=7.23.1 + - jupyter_client >=8.0.0 + - jupyter_core >=4.12,!=5.0.* + - matplotlib-inline >=0.1 + - nest-asyncio >=1.4 + - packaging >=22 + - psutil >=5.7 + - python >=3.10 + - pyzmq >=25 + - tornado >=6.2 + - traitlets >=5.4.0 + - python + constrains: + - appnope >=0.1.2 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/ipykernel?source=hash-mapping + size: 133820 + timestamp: 1761567932044 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipython-8.37.0-pyh8f84b5b_0.conda + sha256: e43fa762183b49c3c3b811d41259e94bb14b7bff4a239b747ef4e1c6bbe2702d + md5: 177cfa19fe3d74c87a8889286dc64090 + depends: + - __unix + - pexpect >4.3 + - decorator + - exceptiongroup + - jedi >=0.16 + - matplotlib-inline + - pickleshare + - prompt-toolkit >=3.0.41,<3.1.0 + - pygments >=2.4.0 + - python >=3.10 + - stack_data + - traitlets >=5.13.0 + - typing_extensions >=4.6 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/ipython?source=hash-mapping + size: 639160 + timestamp: 1748711175284 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipython-8.37.0-pyha7b4d00_0.conda + sha256: 4812e69a1c9d6d43746fa7e8efaf9127d257508249e7192e68cd163511a751ee + md5: 2ffea44095ca39b38b67599e8091bca3 + depends: + - __win + - colorama + - decorator + - exceptiongroup + - jedi >=0.16 + - matplotlib-inline + - pickleshare + - prompt-toolkit >=3.0.41,<3.1.0 + - pygments >=2.4.0 + - python >=3.10 + - stack_data + - traitlets >=5.13.0 + - typing_extensions >=4.6 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/ipython?source=hash-mapping + size: 638940 + timestamp: 1748711254071 +- conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + sha256: 92c4d217e2dc68983f724aa983cca5464dcb929c566627b26a2511159667dba8 + md5: a4f4c5dc9b80bc50e0d3dc4e6e8f1bd9 + depends: + - parso >=0.8.3,<0.9.0 + - python >=3.9 + license: Apache-2.0 AND MIT + purls: + - pkg:pypi/jedi?source=hash-mapping + size: 843646 + timestamp: 1733300981994 - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda sha256: f1ac18b11637ddadc05642e8185a851c7fab5998c6f5470d716812fae943b2af md5: 446bd6c8cb26050d528881df495ce646 @@ -2059,8 +2395,63 @@ packages: - python >=3.9 license: BSD-3-Clause license_family: BSD + purls: + - pkg:pypi/jinja2?source=hash-mapping size: 112714 timestamp: 1741263433881 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_1.conda + sha256: 19d8bd5bb2fde910ec59e081eeb59529491995ce0d653a5209366611023a0b3a + md5: 4ebae00eae9705b0c3d6d1018a81d047 + depends: + - importlib-metadata >=4.8.3 + - jupyter_core >=4.12,!=5.0.* + - python >=3.9 + - python-dateutil >=2.8.2 + - pyzmq >=23.0 + - tornado >=6.2 + - traitlets >=5.3 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyter-client?source=hash-mapping + size: 106342 + timestamp: 1733441040958 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyh6dadd2b_0.conda + sha256: ed709a6c25b731e01563521ef338b93986cd14b5bc17f35e9382000864872ccc + md5: a8db462b01221e9f5135be466faeb3e0 + depends: + - __win + - pywin32 + - platformdirs >=2.5 + - python >=3.10 + - traitlets >=5.3 + - python + constrains: + - pywin32 >=300 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyter-core?source=hash-mapping + size: 64679 + timestamp: 1760643889625 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + sha256: 1d34b80e5bfcd5323f104dbf99a2aafc0e5d823019d626d0dce5d3d356a2a52a + md5: b38fe4e78ee75def7e599843ef4c1ab0 + depends: + - __unix + - python + - platformdirs >=2.5 + - python >=3.10 + - traitlets >=5.3 + - python + constrains: + - pywin32 >=300 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyter-core?source=hash-mapping + size: 65503 + timestamp: 1760643864586 - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_8.conda sha256: 305c22a251db227679343fd73bfde121e555d466af86e537847f4c8b9436be0d md5: ff007ab0f0fdc53d245972bba8a6d40c @@ -2070,6 +2461,72 @@ packages: license_family: GPL size: 1272697 timestamp: 1752669126073 +- conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda + sha256: 0960d06048a7185d3542d850986d807c6e37ca2e644342dd0c72feefcf26c2a4 + md5: b38117a3c920364aff79f870c984b4a3 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: LGPL-2.1-or-later + purls: [] + size: 134088 + timestamp: 1754905959823 +- conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda + sha256: 99df692f7a8a5c27cd14b5fb1374ee55e756631b9c3d659ed3ee60830249b238 + md5: 3f43953b7d3fb3aaa1d0d0723d91e368 + depends: + - keyutils >=1.6.1,<2.0a0 + - libedit >=3.1.20191231,<3.2.0a0 + - libedit >=3.1.20191231,<4.0a0 + - libgcc-ng >=12 + - libstdcxx-ng >=12 + - openssl >=3.3.1,<4.0a0 + license: MIT + license_family: MIT + purls: [] + size: 1370023 + timestamp: 1719463201255 +- conda: https://conda.anaconda.org/conda-forge/osx-64/krb5-1.21.3-h37d8d59_0.conda + sha256: 83b52685a4ce542772f0892a0f05764ac69d57187975579a0835ff255ae3ef9c + md5: d4765c524b1d91567886bde656fb514b + depends: + - __osx >=10.13 + - libcxx >=16 + - libedit >=3.1.20191231,<3.2.0a0 + - libedit >=3.1.20191231,<4.0a0 + - openssl >=3.3.1,<4.0a0 + license: MIT + license_family: MIT + purls: [] + size: 1185323 + timestamp: 1719463492984 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.21.3-h237132a_0.conda + sha256: 4442f957c3c77d69d9da3521268cad5d54c9033f1a73f99cde0a3658937b159b + md5: c6dc8a0fdec13a0565936655c33069a1 + depends: + - __osx >=11.0 + - libcxx >=16 + - libedit >=3.1.20191231,<3.2.0a0 + - libedit >=3.1.20191231,<4.0a0 + - openssl >=3.3.1,<4.0a0 + license: MIT + license_family: MIT + purls: [] + size: 1155530 + timestamp: 1719463474401 +- conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.21.3-hdf4eb48_0.conda + sha256: 18e8b3430d7d232dad132f574268f56b3eb1a19431d6d5de8c53c29e6c18fa81 + md5: 31aec030344e962fbd7dbbbbd68e60a9 + depends: + - openssl >=3.3.1,<4.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: MIT + license_family: MIT + purls: [] + size: 712034 + timestamp: 1719463874284 - conda: https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.17-h717163a_0.conda sha256: d6a61830a354da022eae93fa896d0991385a875c6bba53c82263a289deda9db8 md5: 000e85703f0fd9594c81710dd5066471 @@ -2231,6 +2688,43 @@ packages: license_family: MIT size: 156292 timestamp: 1747040812624 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda + sha256: d789471216e7aba3c184cd054ed61ce3f6dac6f87a50ec69291b9297f8c18724 + md5: c277e0a4d549b03ac1e9d6cbbe3d017b + depends: + - ncurses + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - ncurses >=6.5,<7.0a0 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 134676 + timestamp: 1738479519902 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libedit-3.1.20250104-pl5321ha958ccf_0.conda + sha256: 6cc49785940a99e6a6b8c6edbb15f44c2dd6c789d9c283e5ee7bdfedd50b4cd6 + md5: 1f4ed31220402fcddc083b4bff406868 + depends: + - ncurses + - __osx >=10.13 + - ncurses >=6.5,<7.0a0 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 115563 + timestamp: 1738479554273 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda + sha256: 66aa216a403de0bb0c1340a88d1a06adaff66bae2cfd196731aa24db9859d631 + md5: 44083d2d2c2025afca315c7a172eab2b + depends: + - ncurses + - __osx >=11.0 + - ncurses >=6.5,<7.0a0 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 107691 + timestamp: 1738479560845 - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda sha256: da2080da8f0288b95dd86765c801c6e166c4619b910b11f9a8446fb852438dc2 md5: 4211416ecba1866fab0c6470986c22d6 @@ -2241,6 +2735,7 @@ packages: - expat 2.7.1.* license: MIT license_family: MIT + purls: [] size: 74811 timestamp: 1752719572741 - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.7.1-h21dd04a_0.conda @@ -2252,6 +2747,7 @@ packages: - expat 2.7.1.* license: MIT license_family: MIT + purls: [] size: 72450 timestamp: 1752719744781 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.1-hec049ff_0.conda @@ -2263,6 +2759,7 @@ packages: - expat 2.7.1.* license: MIT license_family: MIT + purls: [] size: 65971 timestamp: 1752719657566 - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.1-hac47afa_0.conda @@ -2276,6 +2773,7 @@ packages: - expat 2.7.1.* license: MIT license_family: MIT + purls: [] size: 141322 timestamp: 1752719767870 - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h9ec8514_0.conda @@ -2667,67 +3165,24 @@ packages: depends: - __osx >=11.0 constrains: - - xz 5.8.1.* - license: 0BSD - purls: [] - size: 92286 - timestamp: 1749230283517 -- conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.1-h2466b09_2.conda - sha256: 55764956eb9179b98de7cc0e55696f2eff8f7b83fc3ebff5e696ca358bca28cc - md5: c15148b2e18da456f5108ccb5e411446 - depends: - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - constrains: - - xz 5.8.1.* - license: 0BSD - purls: [] - size: 104935 - timestamp: 1749230611612 -- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-devel-5.8.1-hb9d3cd8_2.conda - sha256: 329e66330a8f9cbb6a8d5995005478188eb4ba8a6b6391affa849744f4968492 - md5: f61edadbb301530bd65a32646bd81552 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - liblzma 5.8.1 hb9d3cd8_2 - license: 0BSD - purls: [] - size: 439868 - timestamp: 1749230061968 -- conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-devel-5.8.1-hd471939_2.conda - sha256: a020ad9f1e27d4f7a522cbbb9613b99f64a5cc41f80caf62b9fdd1cf818acf18 - md5: 2e16f5b4f6c92b96f6a346f98adc4e3e - depends: - - __osx >=10.13 - - liblzma 5.8.1 hd471939_2 - license: 0BSD - purls: [] - size: 116356 - timestamp: 1749230171181 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-devel-5.8.1-h39f12f2_2.conda - sha256: 974804430e24f0b00f3a48b67ec10c9f5441c9bb3d82cc0af51ba45b8a75a241 - md5: 1201137f1a5ec9556032ffc04dcdde8d - depends: - - __osx >=11.0 - - liblzma 5.8.1 h39f12f2_2 + - xz 5.8.1.* license: 0BSD purls: [] - size: 116244 - timestamp: 1749230297170 -- conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-devel-5.8.1-h2466b09_2.conda - sha256: 1ccff927a2d768403bad85e36ca3e931d96890adb4f503e1780c3412dd1e1298 - md5: 42c90c4941c59f1b9f8fab627ad8ae76 + size: 92286 + timestamp: 1749230283517 +- conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.1-h2466b09_2.conda + sha256: 55764956eb9179b98de7cc0e55696f2eff8f7b83fc3ebff5e696ca358bca28cc + md5: c15148b2e18da456f5108ccb5e411446 depends: - - liblzma 5.8.1 h2466b09_2 - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 + constrains: + - xz 5.8.1.* license: 0BSD purls: [] - size: 129344 - timestamp: 1749230637001 + size: 104935 + timestamp: 1749230611612 - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda sha256: 3aa92d4074d4063f2a162cd8ecb45dccac93e543e565c01a787e16a43501f7ee md5: c7e925f37e3b40d893459e625f6a53f1 @@ -2831,6 +3286,44 @@ packages: license_family: GPL size: 5133768 timestamp: 1759968130105 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.20-h4ab18f5_0.conda + sha256: 0105bd108f19ea8e6a78d2d994a6d4a8db16d19a41212070d2d1d48a63c34161 + md5: a587892d3c13b6621a6091be690dbca2 + depends: + - libgcc-ng >=12 + license: ISC + purls: [] + size: 205978 + timestamp: 1716828628198 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libsodium-1.0.20-hfdf4475_0.conda + sha256: d3975cfe60e81072666da8c76b993af018cf2e73fe55acba2b5ba0928efaccf5 + md5: 6af4b059e26492da6013e79cbcb4d069 + depends: + - __osx >=10.13 + license: ISC + purls: [] + size: 210249 + timestamp: 1716828641383 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.20-h99b78c6_0.conda + sha256: fade8223e1e1004367d7101dd17261003b60aa576df6d7802191f8972f7470b1 + md5: a7ce36e284c5faaf93c220dfc39e3abd + depends: + - __osx >=11.0 + license: ISC + purls: [] + size: 164972 + timestamp: 1716828607917 +- conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.20-hc70643c_0.conda + sha256: 7bcb3edccea30f711b6be9601e083ecf4f435b9407d70fc48fbcf9e5d69a0fc6 + md5: 198bb594f202b205c7d18b936fa4524f + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: ISC + purls: [] + size: 202344 + timestamp: 1716828757533 - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda sha256: 6d9c32fc369af5a84875725f7ddfbfc2ace795c28f246dc70055a79f9b2003da md5: 0b367fad34931cb79e0d6b7e5c06bb1c @@ -3210,18 +3703,18 @@ packages: license_family: BSD size: 80879 timestamp: 1757093529525 -- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_0.conda - sha256: c041b0eaf7a6af3344d5dd452815cdc148d6284fec25a4fa3f4263b3a021e962 - md5: 93a8e71256479c62074356ef6ebf501b +- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda + sha256: 7b1da4b5c40385791dbc3cc85ceea9fad5da680a27d5d3cb8bfaa185e304a89e + md5: 5b5203189eb668f042ac2b0826244964 depends: - mdurl >=0.1,<1 - - python >=3.8 + - python >=3.10 license: MIT license_family: MIT purls: - pkg:pypi/markdown-it-py?source=hash-mapping - size: 64356 - timestamp: 1686175179621 + size: 64736 + timestamp: 1754951288511 - conda: https://conda.anaconda.org/conda-forge/noarch/markupsafe-3.0.3-pyh7db6752_0.conda sha256: e0cbfea51a19b3055ca19428bd9233a25adca956c208abb9d00b21e7259c7e03 md5: fab1be106a50e20f10fe5228fd1d1651 @@ -3235,17 +3728,29 @@ packages: license_family: BSD size: 15499 timestamp: 1759055275624 -- conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_0.conda - sha256: 64073dfb6bb429d52fff30891877b48c7ec0f89625b1bf844905b66a81cce6e1 - md5: 776a8dd9e824f77abac30e6ef43a8f7a +- conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + sha256: 9d690334de0cd1d22c51bc28420663f4277cfa60d34fa5cad1ce284a13f1d603 + md5: 00e120ce3e40bad7bfc78861ce3c4a25 depends: - - python >=3.6 + - python >=3.10 + - traitlets + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/matplotlib-inline?source=compressed-mapping + size: 15175 + timestamp: 1761214578417 +- conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + sha256: 78c1bbe1723449c52b7a9df1af2ee5f005209f67e40b6e1d3c7619127c43b1c7 + md5: 592132998493b3ff25fd7479396e8351 + depends: + - python >=3.9 license: MIT license_family: MIT purls: - pkg:pypi/mdurl?source=hash-mapping - size: 14680 - timestamp: 1704317789138 + size: 14465 + timestamp: 1733255681319 - conda: https://conda.anaconda.org/conda-forge/noarch/mdx_truly_sane_lists-1.3-pyh29332c3_2.conda sha256: d80a751810d0c77e51e8c4f4583654638f5e7b80ef797f6bbce01c61a436223c md5: 85d61372a28fda69cf2c1df646caa975 @@ -3377,64 +3882,64 @@ packages: license: ISC size: 63301 timestamp: 1756492720079 -- conda: https://conda.anaconda.org/conda-forge/linux-64/mypy-1.5.1-py38h01eb140_1.conda - sha256: 00eb9b837d1b6727d08ed304605469150cccc45e0b42ab0bf8e478c581336f8b - md5: 6313e5ee96b444fcd5338085813477ca +- conda: https://conda.anaconda.org/conda-forge/linux-64/mypy-1.5.1-py310h2372a71_1.conda + sha256: 5361d19d13d5b9cef112ed3f0fc798885b2f15c652b04e6e8cccf0995e53bf23 + md5: 84960056ea7ed3d4135bdbcae1b0b95e depends: - libgcc-ng >=12 - mypy_extensions >=1.0.0 - psutil >=4.0 - - python >=3.8,<3.9.0a0 - - python_abi 3.8.* *_cp38 + - python >=3.10,<3.11.0a0 + - python_abi 3.10.* *_cp310 - tomli >=1.1.0 - typing_extensions >=4.1.0 license: MIT license_family: MIT purls: - pkg:pypi/mypy?source=hash-mapping - size: 16146866 - timestamp: 1695442675379 -- conda: https://conda.anaconda.org/conda-forge/osx-64/mypy-1.5.1-py38hcafd530_1.conda - sha256: 89099ba03417a8a68d9cbf8345b20c9b14633b972d6bd77146092be35e8ce4a1 - md5: a8de48d3c37e63914dbb3341dc4990ba + size: 16632004 + timestamp: 1695442369481 +- conda: https://conda.anaconda.org/conda-forge/osx-64/mypy-1.5.1-py310h6729b98_1.conda + sha256: fb6717336e42129da8c30fb7fed6229966a6eafd01b241a56096701acff5888e + md5: 8417379bd7648c4a53bc1a25f8d17e00 depends: - mypy_extensions >=1.0.0 - psutil >=4.0 - - python >=3.8,<3.9.0a0 - - python_abi 3.8.* *_cp38 + - python >=3.10,<3.11.0a0 + - python_abi 3.10.* *_cp310 - tomli >=1.1.0 - typing_extensions >=4.1.0 license: MIT license_family: MIT purls: - pkg:pypi/mypy?source=hash-mapping - size: 11027655 - timestamp: 1695442955398 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/mypy-1.5.1-py38hb192615_1.conda - sha256: e7008ea5f4da4ce761070e5b2d0c6b038092aa87c54eec086e6bb5eaf1df6498 - md5: 8ecb5bafe2068a472aa2adaa97fe38db + size: 11070820 + timestamp: 1695442404949 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/mypy-1.5.1-py310h2aa6e3c_1.conda + sha256: 6c3053ae073ccc033b716664f02ab5ad86d0147735ba1b69019fe435e8adb0e6 + md5: d46f120a1bd581edb8c45b74cdb8bef1 depends: - mypy_extensions >=1.0.0 - psutil >=4.0 - - python >=3.8,<3.9.0a0 - - python >=3.8,<3.9.0a0 *_cpython - - python_abi 3.8.* *_cp38 + - python >=3.10,<3.11.0a0 + - python >=3.10,<3.11.0a0 *_cpython + - python_abi 3.10.* *_cp310 - tomli >=1.1.0 - typing_extensions >=4.1.0 license: MIT license_family: MIT purls: - pkg:pypi/mypy?source=hash-mapping - size: 8908642 - timestamp: 1695442718816 -- conda: https://conda.anaconda.org/conda-forge/win-64/mypy-1.5.1-py38h91455d4_1.conda - sha256: 142e125d201faae4a7430c2a519bb70348b773e8a5d0249ec70d5b1f7c243f98 - md5: b50c3acf3bbadccda20ebf7fcedd5c65 + size: 8923491 + timestamp: 1695442599440 +- conda: https://conda.anaconda.org/conda-forge/win-64/mypy-1.5.1-py310h8d17308_1.conda + sha256: 5a07593989e850a2ac589ed0293ef16e485bc6a2cfa3d5128f7b9c7b6d3ab684 + md5: 4a6cce71b112befb10fa4da09d20ec4d depends: - mypy_extensions >=1.0.0 - psutil >=4.0 - - python >=3.8,<3.9.0a0 - - python_abi 3.8.* *_cp38 + - python >=3.10,<3.11.0a0 + - python_abi 3.10.* *_cp310 - tomli >=1.1.0 - typing_extensions >=4.1.0 - ucrt >=10.0.20348.0 @@ -3444,19 +3949,19 @@ packages: license_family: MIT purls: - pkg:pypi/mypy?source=hash-mapping - size: 8981331 - timestamp: 1695442268532 -- conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_0.conda - sha256: f240217476e148e825420c6bc3a0c0efb08c0718b7042fae960400c02af858a3 - md5: 4eccaeba205f0aed9ac3a9ea58568ca3 + size: 8953039 + timestamp: 1695442366782 +- conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + sha256: 6ed158e4e5dd8f6a10ad9e525631e35cee8557718f83de7a4e3966b1f772c4b1 + md5: e9c622e0d00fa24a6292279af3ab6d06 depends: - - python >=3.5 + - python >=3.9 license: MIT license_family: MIT purls: - pkg:pypi/mypy-extensions?source=hash-mapping - size: 10492 - timestamp: 1675543414256 + size: 11766 + timestamp: 1745776666688 - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda sha256: 3fde293232fa3fca98635e1167de6b7c7fda83caf24b9d6c91ec9eefb4f4d586 md5: 47e340acb35de30501a76c7c799c41d7 @@ -3485,26 +3990,37 @@ packages: purls: [] size: 797030 timestamp: 1738196177597 -- pypi: https://files.pythonhosted.org/packages/11/10/943cfb579f1a02909ff96464c69893b1d25be3731b5d3652c2e0cf1281ea/numpy-1.24.4-cp38-cp38-macosx_10_9_x86_64.whl +- conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + sha256: bb7b21d7fd0445ddc0631f64e66d91a179de4ba920b8381f29b9d006a42788c0 + md5: 598fd7d4d0de2455fb74f56063969a97 + depends: + - python >=3.9 + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/nest-asyncio?source=hash-mapping + size: 11543 + timestamp: 1733325673691 +- pypi: https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl name: numpy - version: 1.24.4 - sha256: 1452241c290f3e2a312c137a9999cdbf63f78864d63c79039bda65ee86943f61 - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/69/65/0d47953afa0ad569d12de5f65d964321c208492064c38fe3b0b9744f8d44/numpy-1.24.4-cp38-cp38-win_amd64.whl + version: 2.2.6 + sha256: 8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl name: numpy - version: 1.24.4 - sha256: 692f2e0f55794943c5bfff12b3f56f99af76f902fc47487bdfe97856de51a706 - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/98/5d/5738903efe0ecb73e51eb44feafba32bdba2081263d40c5043568ff60faf/numpy-1.24.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + version: 2.2.6 + sha256: b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl name: numpy - version: 1.24.4 - sha256: dd80e219fd4c71fc3699fc1dadac5dcf4fd882bfc6f7ec53d30fa197b8ee22dc - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/a7/ae/f53b7b265fdc701e663fbb322a8e9d4b14d9cb7b2385f45ddfabfc4327e4/numpy-1.24.4-cp38-cp38-macosx_11_0_arm64.whl + version: 2.2.6 + sha256: f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl name: numpy - version: 1.24.4 - sha256: 04640dab83f7c6c85abf9cd729c5b65f1ebd0ccf9de90b270cd61935eef0197f - requires_python: '>=3.8' + version: 2.2.6 + sha256: fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915 + requires_python: '>=3.10' - conda: https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.4-h55fea9a_0.conda sha256: 3900f9f2dbbf4129cf3ad6acf4e4b6f7101390b53843591c53b00f034343bc4d md5: 11b3379b191f63139e29c0d19dee24cd @@ -3627,6 +4143,18 @@ packages: license_family: MIT size: 18865 timestamp: 1734618649164 +- conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.5-pyhcf101f3_0.conda + sha256: 30de7b4d15fbe53ffe052feccde31223a236dae0495bab54ab2479de30b2990f + md5: a110716cdb11cf51482ff4000dc253d7 + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/parso?source=hash-mapping + size: 81562 + timestamp: 1755974222274 - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda sha256: 9f64009cdf5b8e529995f18e03665b03f5d07c0b17445b8badef45bde76249ee md5: 617f15191456cc6a13db418a275435e5 @@ -3683,6 +4211,28 @@ packages: license_family: BSD size: 1034703 timestamp: 1756743085974 +- conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda + sha256: 202af1de83b585d36445dc1fda94266697341994d1a3328fabde4989e1b3d07a + md5: d0d408b1f18883a944376da5cf8101ea + depends: + - ptyprocess >=0.5 + - python >=3.9 + license: ISC + purls: + - pkg:pypi/pexpect?source=hash-mapping + size: 53561 + timestamp: 1733302019362 +- conda: https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-pyhd8ed1ab_1004.conda + sha256: e2ac3d66c367dada209fc6da43e645672364b9fd5f9d28b9f016e24b81af475b + md5: 11a9d1d09a3615fc07c3faf79bc0b943 + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pickleshare?source=hash-mapping + size: 11748 + timestamp: 1733327448200 - conda: https://conda.anaconda.org/conda-forge/linux-64/pillow-12.0.0-py314h72745e2_0.conda sha256: 1dec7a825154fce8705892a4cc178f8edfa78253c56de06000b409f6cfe2cea9 md5: 47fdb59e9753d0af064c25247ab4f47c @@ -3824,74 +4374,94 @@ packages: - python license: MIT license_family: MIT + purls: + - pkg:pypi/platformdirs?source=hash-mapping size: 23625 timestamp: 1759953252315 -- conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda - sha256: 33eaa3359948a260ebccf9cdc2fd862cea5a6029783289e13602d8e634cd9a26 - md5: d3483c8fc2dc2cc3f5cf43e26d60cabf +- conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhd8ed1ab_0.conda + sha256: a8eb555eef5063bbb7ba06a379fa7ea714f57d9741fe0efdb9442dbbc2cccbcc + md5: 7da7ccd349dbf6487a7778579d2bb971 depends: - - python >=3.8 + - python >=3.9 license: MIT license_family: MIT purls: - pkg:pypi/pluggy?source=hash-mapping - size: 23815 - timestamp: 1713667175451 -- conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-6.0.0-py38hfb59056_0.conda - sha256: d6d5f1ac1dc3bbddb50c093f89a425edae695754ad1ab1bc78bf720be11315ea - md5: 14d8661ec0011b79081f8429c716f46f + size: 24246 + timestamp: 1747339794916 +- conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda + sha256: 4817651a276016f3838957bfdf963386438c70761e9faec7749d411635979bae + md5: edb16f14d920fb3faf17f5ce582942d6 depends: - - libgcc-ng >=12 - - python >=3.8,<3.9.0a0 - - python_abi 3.8.* *_cp38 + - python >=3.10 + - wcwidth + constrains: + - prompt_toolkit 3.0.52 license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/psutil?source=hash-mapping - size: 366341 - timestamp: 1719274737975 -- conda: https://conda.anaconda.org/conda-forge/osx-64/psutil-6.0.0-py38hc718529_0.conda - sha256: b6006e8d25ae4c8d43ca07f5ff15b2ca51bfceb92168b8f73e376d30078bb944 - md5: fc486245b64b4e03c5968915c277aabf + - pkg:pypi/prompt-toolkit?source=hash-mapping + size: 273927 + timestamp: 1756321848365 +- conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.1.2-py310h139afa4_0.conda + sha256: 2f631edfb32a2a3b99be176aebda98cd56492b849e5486c9a4583bff4c8768a8 + md5: ee005b66348b73dede98088db6a5b4fc + depends: + - python + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python_abi 3.10.* *_cp310 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/psutil?source=compressed-mapping + size: 373918 + timestamp: 1761666689326 +- conda: https://conda.anaconda.org/conda-forge/osx-64/psutil-7.1.2-py310h3aa7efa_0.conda + sha256: c4e813325b9636cf61fb1c3fd6b88515b13a5b4911b3fd4ea7102f3c146fd79d + md5: 6293f3016812913b81efcca0963db615 depends: + - python - __osx >=10.13 - - python >=3.8,<3.9.0a0 - - python_abi 3.8.* *_cp38 + - python_abi 3.10.* *_cp310 license: BSD-3-Clause license_family: BSD purls: - pkg:pypi/psutil?source=hash-mapping - size: 372234 - timestamp: 1719274817130 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-6.0.0-py38h3237794_0.conda - sha256: 76e30573405195dbcedff472f7706e09765b2d49112209e7f81dfb8436e73235 - md5: f14d02d525fd9f62172c717979e2d849 + size: 384891 + timestamp: 1761666822900 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.1.2-py310hf151d32_0.conda + sha256: b13b586c7090704aa12ab21331c29cbc4b86b3b9d475c06ea0b517153a2a745c + md5: 1745e5050ad9c4fcc698d80841b17fe5 depends: + - python + - python 3.10.* *_cpython - __osx >=11.0 - - python >=3.8,<3.9.0a0 - - python >=3.8,<3.9.0a0 *_cpython - - python_abi 3.8.* *_cp38 + - python_abi 3.10.* *_cp310 license: BSD-3-Clause license_family: BSD purls: - pkg:pypi/psutil?source=hash-mapping - size: 374902 - timestamp: 1719274926745 -- conda: https://conda.anaconda.org/conda-forge/win-64/psutil-6.0.0-py38h4cb3324_0.conda - sha256: b624f4be2d0e7b956835ea8822cb9502c861819e5402fe5d02f27d8b4289e392 - md5: 00cc8acaf6d7eec51d009a0662a0cc03 - depends: - - python >=3.8,<3.9.0a0 - - python_abi 3.8.* *_cp38 + size: 387735 + timestamp: 1761666787533 +- conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.1.2-py310h1637853_0.conda + sha256: 9d1215092d4aa75fd06a6f2e48430356314b43d83039c7f37d8eead254f1ac48 + md5: 43af0cd123cf8bf7ad0e9bba40a086b6 + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.10.* *_cp310 license: BSD-3-Clause license_family: BSD purls: - pkg:pypi/psutil?source=hash-mapping - size: 383894 - timestamp: 1719275206477 + size: 392584 + timestamp: 1761666736729 - conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda sha256: 9c88f8c64590e9567c6c80823f0328e58d3b1efb0e1c539c0315ceca764e0973 md5: b3c17d95b5a10c6e64a21fa17573e70e @@ -3931,62 +4501,83 @@ packages: license_family: MIT size: 9389 timestamp: 1726802555076 +- conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda + sha256: a7713dfe30faf17508ec359e0bc7e0983f5d94682492469bd462cdaae9c64d83 + md5: 7d9daffbb8d8e0af0f769dbbcd173a54 + depends: + - python >=3.9 + license: ISC + purls: + - pkg:pypi/ptyprocess?source=hash-mapping + size: 19457 + timestamp: 1733302371990 +- conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + sha256: 71bd24600d14bb171a6321d523486f6a06f855e75e547fa0cb2a0953b02047f0 + md5: 3bfdfb8dbcdc4af1ae3f9a8eb3948f04 + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pure-eval?source=hash-mapping + size: 16668 + timestamp: 1733569518868 - conda: . name: py-rattler-build version: 0.48.0 - build: py38h9af789f_0 + build: py310h7c29940_0 subdir: linux-64 depends: - - python >=3.8 - - python_abi 3.8.* *_cp38 + - python >=3.10 + - python_abi 3.10.* *_cp310 constrains: - __glibc >=2.17 license: BSD-3-Clause input: - hash: 750be6b0351f15dd166a306610b756eddddc00df36d1d18019d7430625795e98 + hash: 43f3512c0dd01df2d701c387e3968ef8f4657e03c51741b9ec4e7a9265ec75fe globs: - pyproject.toml - conda: . name: py-rattler-build version: 0.48.0 - build: py38h9af789f_0 + build: py310h7c29940_0 subdir: osx-64 depends: - - python >=3.8 - - python_abi 3.8.* *_cp38 + - python >=3.10 + - python_abi 3.10.* *_cp310 constrains: - __osx >=10.13 license: BSD-3-Clause input: - hash: 750be6b0351f15dd166a306610b756eddddc00df36d1d18019d7430625795e98 + hash: 43f3512c0dd01df2d701c387e3968ef8f4657e03c51741b9ec4e7a9265ec75fe globs: - pyproject.toml - conda: . name: py-rattler-build version: 0.48.0 - build: py38h9af789f_0 + build: py310h7c29940_0 subdir: osx-arm64 depends: - - python >=3.8 - - python_abi 3.8.* *_cp38 + - python >=3.10 + - python_abi 3.10.* *_cp310 constrains: - __osx >=11.0 license: BSD-3-Clause input: - hash: 750be6b0351f15dd166a306610b756eddddc00df36d1d18019d7430625795e98 + hash: 43f3512c0dd01df2d701c387e3968ef8f4657e03c51741b9ec4e7a9265ec75fe globs: - pyproject.toml - conda: . name: py-rattler-build version: 0.48.0 - build: py38h9af789f_0 + build: py310h7c29940_0 subdir: win-64 depends: - - python >=3.8 - - python_abi 3.8.* *_cp38 + - python >=3.10 + - python_abi 3.10.* *_cp310 license: BSD-3-Clause input: - hash: 750be6b0351f15dd166a306610b756eddddc00df36d1d18019d7430625795e98 + hash: 43f3512c0dd01df2d701c387e3968ef8f4657e03c51741b9ec4e7a9265ec75fe globs: - pyproject.toml - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda @@ -3997,30 +4588,10 @@ packages: - python license: BSD-3-Clause license_family: BSD - size: 110100 - timestamp: 1733195786147 -- conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyhd8ed1ab_0.conda - sha256: 406001ebf017688b1a1554b49127ca3a4ac4626ec0fd51dc75ffa4415b720b64 - md5: 844d9eb3b43095b031874477f7d70088 - depends: - - python >=3.8 - license: BSD-3-Clause - license_family: BSD purls: - pkg:pypi/pycparser?source=hash-mapping - size: 105098 - timestamp: 1711811634025 -- conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.18.0-pyhd8ed1ab_0.conda - sha256: 78267adf4e76d0d64ea2ffab008c501156c108bb08fecb703816fb63e279780b - md5: b7f5c092b8f9800150d998a71b76d5a1 - depends: - - python >=3.8 - license: BSD-2-Clause - license_family: BSD - purls: - - pkg:pypi/pygments?source=hash-mapping - size: 879295 - timestamp: 1714846885370 + size: 110100 + timestamp: 1733195786147 - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda sha256: 5577623b9f6685ece2697c6eb7511b4c9ac5fb607c9babc2646c811b428fd46a md5: 6b6ece66ebcae2d5f326c77ef2c5a066 @@ -4028,6 +4599,8 @@ packages: - python >=3.9 license: BSD-2-Clause license_family: BSD + purls: + - pkg:pypi/pygments?source=hash-mapping size: 889287 timestamp: 1750615908735 - conda: https://conda.anaconda.org/conda-forge/noarch/pymdown-extensions-10.16.1-pyhd8ed1ab_0.conda @@ -4041,19 +4614,6 @@ packages: license_family: MIT size: 170121 timestamp: 1753743741894 -- conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh0701188_6.tar.bz2 - sha256: b3a612bc887f3dd0fb7c4199ad8e342bd148cf69a9b74fd9468a18cf2bef07b7 - md5: 56cd9fe388baac0e90c7149cfac95b60 - depends: - - __win - - python >=3.8 - - win_inet_pton - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/pysocks?source=hash-mapping - size: 19348 - timestamp: 1661605138291 - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda sha256: d016e04b0e12063fbee4a2d5fbb9b39a8d191b5a0042f0b8459188aedeabb0ca md5: e2fd202833c4a981ce8a65974fe4abd1 @@ -4063,20 +4623,10 @@ packages: - win_inet_pton license: BSD-3-Clause license_family: BSD - size: 21784 - timestamp: 1733217448189 -- conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha2e5f31_6.tar.bz2 - sha256: a42f826e958a8d22e65b3394f437af7332610e43ee313393d1cf143f0a2d274b - md5: 2a7de29fb590ca14b5243c4c812c8025 - depends: - - __unix - - python >=3.8 - license: BSD-3-Clause - license_family: BSD purls: - pkg:pypi/pysocks?source=hash-mapping - size: 18981 - timestamp: 1661604969727 + size: 21784 + timestamp: 1733217448189 - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda sha256: ba3b032fa52709ce0d9fd388f63d330a026754587a2f461117cac9ab73d8d0d8 md5: 461219d1a5bd61342293efa2c0c90eac @@ -4085,53 +4635,86 @@ packages: - python >=3.9 license: BSD-3-Clause license_family: BSD + purls: + - pkg:pypi/pysocks?source=hash-mapping size: 21085 timestamp: 1733217331982 -- conda: https://conda.anaconda.org/conda-forge/noarch/pytest-7.4.4-pyhd8ed1ab_0.conda - sha256: 8979721b7f86b183d21103f3ec2734783847d317c1b754f462f407efc7c60886 - md5: a9d145de8c5f064b5fa68fb34725d9f4 +- conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.4.2-pyhd8ed1ab_0.conda + sha256: 41053d9893e379a3133bb9b557b98a3d2142fca474fb6b964ba5d97515f78e2d + md5: 1f987505580cb972cf28dc5f74a0f81b depends: - - colorama - - exceptiongroup >=1.0.0rc8 - - iniconfig - - packaging - - pluggy >=0.12,<2.0 - - python >=3.7 - - tomli >=1.0.0 + - colorama >=0.4 + - exceptiongroup >=1 + - iniconfig >=1 + - packaging >=20 + - pluggy >=1.5,<2 + - pygments >=2.7.2 + - python >=3.10 + - tomli >=1 constrains: - pytest-faulthandler >=2 license: MIT license_family: MIT purls: - pkg:pypi/pytest?source=hash-mapping - size: 244564 - timestamp: 1704035308916 -- conda: https://conda.anaconda.org/conda-forge/noarch/pytest-asyncio-0.21.1-pyhd8ed1ab_0.conda - sha256: f061bd0f8a41886331d852f7b3c4b395ac40bc404283fd061dcf992f197e3bb1 - md5: c8fd31da9b1059c4440f31f6ab4d620c - depends: - - pytest >=7.0.0,<8.0.0a0 - - python >=3.7 - - typing_extensions >=3.7.2 + size: 276734 + timestamp: 1757011891753 +- conda: https://conda.anaconda.org/conda-forge/noarch/pytest-asyncio-1.2.0-pyhcf101f3_0.conda + sha256: 24f7783ff3eb87a81f02a717fe71b62c54817fad6225a81d6d3c4c429bd5dd58 + md5: abf005353f4902bbfa7f31d71efcfbf9 + depends: + - pytest >=8.2,<9 + - python >=3.10 + - typing_extensions >=4.12 + - backports.asyncio.runner >=1.1,<2 + - python license: Apache-2.0 license_family: APACHE purls: - pkg:pypi/pytest-asyncio?source=hash-mapping - size: 26068 - timestamp: 1689174411610 -- conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xprocess-0.23.0-pyhd8ed1ab_0.conda - sha256: 29bddec9fe144cd94f6d89ff91e26ec217fbabb839f8dc991d518a43f14d1413 - md5: c9178b28f47fd191ac0a668b84e8fa18 + size: 39228 + timestamp: 1757682391060 +- conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xprocess-1.0.2-pyhd8ed1ab_1.conda + sha256: 8bc8e82da035fe5dc1966c9d5f5d9d290f5c77075e2b3b6873e0b270e8c61853 + md5: 8c469458938c01ef5c9cd1357bbc62ac depends: - psutil - pytest >=2.8 - - python >=3.8 + - python >=3.9 license: MIT license_family: MIT purls: - pkg:pypi/pytest-xprocess?source=hash-mapping - size: 19270 - timestamp: 1695495262467 + size: 19006 + timestamp: 1733327154188 +- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.10.19-h3c07f61_2_cpython.conda + build_number: 2 + sha256: 6e3b6b69b3cacfc7610155d58407a003820eaacd50fbe039abff52b5e70b1e9b + md5: 27ac896a8b4970f8977503a9e70dc745 + depends: + - __glibc >=2.17,<3.0.a0 + - bzip2 >=1.0.8,<2.0a0 + - ld_impl_linux-64 >=2.36.1 + - libexpat >=2.7.1,<3.0a0 + - libffi >=3.4,<4.0a0 + - libgcc >=14 + - liblzma >=5.8.1,<6.0a0 + - libnsl >=2.0.1,<2.1.0a0 + - libsqlite >=3.50.4,<4.0a0 + - libuuid >=2.41.2,<3.0a0 + - libxcrypt >=4.4.36 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.5.4,<4.0a0 + - readline >=8.2,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + constrains: + - python_abi 3.10.* *_cp310 + license: Python-2.0 + purls: [] + size: 25311690 + timestamp: 1761173015969 - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.12-hd63d673_1_cpython.conda build_number: 1 sha256: 39898d24769a848c057ab861052e50bdc266310a7509efa3514b840e85a2ae98 @@ -4186,32 +4769,29 @@ packages: size: 36681389 timestamp: 1761176838143 python_site_packages_path: lib/python3.14/site-packages -- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.8.20-h4a871b0_2_cpython.conda +- conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.10.19-h988dfef_2_cpython.conda build_number: 2 - sha256: 8043dcdb29e1e026d0def1056620d81b24c04f71fd98cc45888c58373b479845 - md5: 05ffff2f44ad60b94ecb53d029c6bdf7 + sha256: cda6726872b13f92d4dea6bf1aa4cbc594e7de008e37df28da05b94d0d18f489 + md5: f46421dd285f5cb0213c0fdce20ab196 depends: - - __glibc >=2.17,<3.0.a0 + - __osx >=10.13 - bzip2 >=1.0.8,<2.0a0 - - ld_impl_linux-64 >=2.36.1 + - libexpat >=2.7.1,<3.0a0 - libffi >=3.4,<4.0a0 - - libgcc >=13 - - libnsl >=2.0.1,<2.1.0a0 - - libsqlite >=3.46.1,<4.0a0 - - libuuid >=2.38.1,<3.0a0 - - libxcrypt >=4.4.36 + - liblzma >=5.8.1,<6.0a0 + - libsqlite >=3.50.4,<4.0a0 - libzlib >=1.3.1,<2.0a0 - ncurses >=6.5,<7.0a0 - - openssl >=3.3.2,<4.0a0 + - openssl >=3.5.4,<4.0a0 - readline >=8.2,<9.0a0 - tk >=8.6.13,<8.7.0a0 - - xz >=5.2.6,<6.0a0 + - tzdata constrains: - - python_abi 3.8.* *_cp38 + - python_abi 3.10.* *_cp310 license: Python-2.0 purls: [] - size: 22176012 - timestamp: 1727719857908 + size: 13135247 + timestamp: 1761173952753 - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.12.12-h74c2667_1_cpython.conda build_number: 1 sha256: 7d711e7a5085c05d186e1dbc86b8f10fb3d88fb3ce3034944ededef39173ff32 @@ -4258,27 +4838,29 @@ packages: size: 14427639 timestamp: 1761177864469 python_site_packages_path: lib/python3.14/site-packages -- conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.8.20-h4f978b9_2_cpython.conda +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.10.19-hcd7f573_2_cpython.conda build_number: 2 - sha256: 839c786f6f46eceb4b197d84ff96b134c273d60af4e55e9cbbdc08e489b6d78b - md5: a6263abf89e3162d11e63141bf25d91f + sha256: 7bac6cc075d1d7897f06fa14c1bc87eb16b9524c6002e0c72b0ed3326af51695 + md5: feb559b139819a7326f992711cb50872 depends: - - __osx >=10.13 + - __osx >=11.0 - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.7.1,<3.0a0 - libffi >=3.4,<4.0a0 - - libsqlite >=3.46.1,<4.0a0 + - liblzma >=5.8.1,<6.0a0 + - libsqlite >=3.50.4,<4.0a0 - libzlib >=1.3.1,<2.0a0 - ncurses >=6.5,<7.0a0 - - openssl >=3.3.2,<4.0a0 + - openssl >=3.5.4,<4.0a0 - readline >=8.2,<9.0a0 - tk >=8.6.13,<8.7.0a0 - - xz >=5.2.6,<6.0a0 + - tzdata constrains: - - python_abi 3.8.* *_cp38 + - python_abi 3.10.* *_cp310 license: Python-2.0 purls: [] - size: 11338027 - timestamp: 1727718893331 + size: 11674631 + timestamp: 1761173465015 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.12-h18782d2_1_cpython.conda build_number: 1 sha256: 626da9bb78459ce541407327d1e22ee673fd74e9103f1a0e0f4e3967ad0a23a7 @@ -4325,27 +4907,29 @@ packages: size: 13590581 timestamp: 1761177195716 python_site_packages_path: lib/python3.14/site-packages -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.8.20-h7d35d02_2_cpython.conda +- conda: https://conda.anaconda.org/conda-forge/win-64/python-3.10.19-hc20f281_2_cpython.conda build_number: 2 - sha256: cf8692e732697d47f0290ef83caa4b3115c7b277a3fb155b7de0f09fa1b5e27c - md5: 29ed2994beffea2a256a7e14f9468df8 + sha256: 58c3066571c9c8ba62254dfa1cee696d053f9f78cd3a92c8032af58232610c32 + md5: cd78c55405743e88fda2464be3c902b3 depends: - - __osx >=11.0 - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.7.1,<3.0a0 - libffi >=3.4,<4.0a0 - - libsqlite >=3.46.1,<4.0a0 + - liblzma >=5.8.1,<6.0a0 + - libsqlite >=3.50.4,<4.0a0 - libzlib >=1.3.1,<2.0a0 - - ncurses >=6.5,<7.0a0 - - openssl >=3.3.2,<4.0a0 - - readline >=8.2,<9.0a0 + - openssl >=3.5.4,<4.0a0 - tk >=8.6.13,<8.7.0a0 - - xz >=5.2.6,<6.0a0 + - tzdata + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 constrains: - - python_abi 3.8.* *_cp38 + - python_abi 3.10.* *_cp310 license: Python-2.0 purls: [] - size: 11774160 - timestamp: 1727718758277 + size: 16106778 + timestamp: 1761172101787 - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.12.12-h0159041_1_cpython.conda build_number: 1 sha256: 9b163b0426c92eee1881d5c838e230a750a3fa372092db494772886ab91c2548 @@ -4392,27 +4976,6 @@ packages: size: 16706286 timestamp: 1761175439068 python_site_packages_path: Lib/site-packages -- conda: https://conda.anaconda.org/conda-forge/win-64/python-3.8.20-hfaddaf0_2_cpython.conda - build_number: 2 - sha256: 4cf5c93b625cc353b7bb20eb2f2840a2c24c76578ae425c017812d1b95c5225d - md5: 4e181f484d292cb273fdf456e8dc7b4a - depends: - - bzip2 >=1.0.8,<2.0a0 - - libffi >=3.4,<4.0a0 - - libsqlite >=3.46.1,<4.0a0 - - libzlib >=1.3.1,<2.0a0 - - openssl >=3.3.2,<4.0a0 - - tk >=8.6.13,<8.7.0a0 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - - xz >=5.2.6,<6.0a0 - constrains: - - python_abi 3.8.* *_cp38 - license: Python-2.0 - purls: [] - size: 16152994 - timestamp: 1727719830490 - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda sha256: d6a17ece93bbd5139e02d2bd7dbfa80bee1a4261dced63f65f679121686bf664 md5: 5b8d21249ff20967101ffa321cab24e8 @@ -4422,8 +4985,21 @@ packages: - python license: Apache-2.0 license_family: APACHE + purls: + - pkg:pypi/python-dateutil?source=hash-mapping size: 233310 timestamp: 1751104122689 +- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.10-8_cp310.conda + build_number: 8 + sha256: 7ad76fa396e4bde336872350124c0819032a9e8a0a40590744ff9527b54351c1 + md5: 05e00f3b21e88bb3d658ac700b2ce58c + constrains: + - python 3.10.* *_cpython + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 6999 + timestamp: 1752805924192 - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda build_number: 8 sha256: 80677180dd3c22deb7426ca89d6203f1c7f1f256f2d5a94dc210f6e758229809 @@ -4444,17 +5020,6 @@ packages: license_family: BSD size: 6989 timestamp: 1752805904792 -- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.8-8_cp38.conda - build_number: 8 - sha256: 83c22066a672ce0b16e693c84aa6d5efb68e02eff037a55e047d7095d0fdb5ca - md5: 4f7b6e3de4f15cc44e0f93b39f07205d - constrains: - - python 3.8.* *_cpython - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 6960 - timestamp: 1752805923703 - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda sha256: 8d2a8bf110cc1fc3df6904091dead158ba3e614d8402a83e51ed3a8aa93cdeb0 md5: bc8e3267d44011051f2eb14d22fb0960 @@ -4462,8 +5027,28 @@ packages: - python >=3.9 license: MIT license_family: MIT + purls: + - pkg:pypi/pytz?source=hash-mapping size: 189015 timestamp: 1742920947249 +- conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-311-py310h282bd7d_1.conda + sha256: 2ce920e200699cc2a114106665451c05efcaf5cf0ca46685d9a7a5914616f7b5 + md5: 0289b272f8a22ad8fc29d6747383b503 + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.10.* *_cp310 + license: PSF-2.0 + license_family: PSF + purls: + - pkg:pypi/pywin32?source=hash-mapping + size: 6293229 + timestamp: 1756487147910 - conda: https://conda.anaconda.org/conda-forge/noarch/pyyaml-6.0.3-pyh7db6752_0.conda sha256: 828af2fd7bb66afc9ab1c564c2046be391aaf66c0215f05afaf6d7a9a270fe2a md5: b12f41c0d7fb5ab81709fcc86579688f @@ -4480,12 +5065,79 @@ packages: sha256: 69ab63bd45587406ae911811fc4d4c1bf972d643fa57a009de7c01ac978c4edd md5: e8e53c4150a1bba3b160eacf9d53a51b depends: - - python >=3.9 - - pyyaml - license: MIT - license_family: MIT - size: 11137 - timestamp: 1747237061448 + - python >=3.9 + - pyyaml + license: MIT + license_family: MIT + size: 11137 + timestamp: 1747237061448 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-27.1.0-py310h4f33d48_0.conda + sha256: 0c059e38246a3e148a019e18148098a4016b04e63a716942279e92301d3d16ae + md5: d175993378311ef7c74f17971a380655 + depends: + - python + - libgcc >=14 + - libstdcxx >=14 + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - python_abi 3.10.* *_cp310 + - zeromq >=4.3.5,<4.4.0a0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pyzmq?source=hash-mapping + size: 326821 + timestamp: 1757387023202 +- conda: https://conda.anaconda.org/conda-forge/osx-64/pyzmq-27.1.0-py310hbbd5e6a_0.conda + sha256: ef398437b1b0c9be2a273980f4b16d3ebe3ff4a77fe99c14d35f9dec6af1a7e3 + md5: e34212a205e2f2777218ec2bba52bdf0 + depends: + - python + - libcxx >=19 + - __osx >=10.13 + - zeromq >=4.3.5,<4.4.0a0 + - python_abi 3.10.* *_cp310 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pyzmq?source=hash-mapping + size: 305312 + timestamp: 1757387127397 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyzmq-27.1.0-py310hc4a7dca_0.conda + sha256: 2628f3fe310e5efc77ded2bf45805764abcf6c85be44efc40ae414e2d0948908 + md5: 5c3f215249761ab767c715e9e9ec2728 + depends: + - python + - libcxx >=19 + - python 3.10.* *_cpython + - __osx >=11.0 + - python_abi 3.10.* *_cp310 + - zeromq >=4.3.5,<4.4.0a0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pyzmq?source=hash-mapping + size: 301885 + timestamp: 1757387129559 +- conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-27.1.0-py310h535538e_0.conda + sha256: f906e317a3a88ff02fccc6d23507c50b7d34fdb6c65a87d680a7dbb9f2cb3aba + md5: e892d2b08f97504517be3e9393cacf3b + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - zeromq >=4.3.5,<4.3.6.0a0 + - python_abi 3.10.* *_cp310 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pyzmq?source=hash-mapping + size: 306889 + timestamp: 1757387021143 - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda sha256: 2d6d0c026902561ed77cd646b5021aef2d4db22e57a5b0178dfc669231e06d2c md5: 283b96675859b20a825f8fa30f311446 @@ -4517,23 +5169,6 @@ packages: purls: [] size: 252359 timestamp: 1740379663071 -- conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_0.conda - sha256: 5845ffe82a6fa4d437a2eae1e32a1ad308d7ad349f61e337c0a890fe04c513cc - md5: 5ede4753180c7a550a443c430dc8ab52 - depends: - - certifi >=2017.4.17 - - charset-normalizer >=2,<4 - - idna >=2.5,<4 - - python >=3.8 - - urllib3 >=1.21.1,<3 - constrains: - - chardet >=3.0.2,<6 - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/requests?source=hash-mapping - size: 58810 - timestamp: 1717057174842 - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.5-pyhd8ed1ab_0.conda sha256: 8dc54e94721e9ab545d7234aa5192b74102263d3e704e6d0c8aa7008f2da2a7b md5: db0c6b99149880c8ba515cf4abe93ee4 @@ -4547,22 +5182,25 @@ packages: - chardet >=3.0.2,<6 license: Apache-2.0 license_family: APACHE + purls: + - pkg:pypi/requests?source=hash-mapping size: 59263 timestamp: 1755614348400 -- conda: https://conda.anaconda.org/conda-forge/noarch/rich-13.9.4-pyhd8ed1ab_0.conda - sha256: c009488fc07fd5557434c9c1ad32ab1dd50241d6a766e4b2b4125cd6498585a8 - md5: bcf8cc8924b5d20ead3d122130b8320b +- conda: https://conda.anaconda.org/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda + sha256: edfb44d0b6468a8dfced728534c755101f06f1a9870a7ad329ec51389f16b086 + md5: a247579d8a59931091b16a1e932bbed6 depends: - markdown-it-py >=2.2.0 - pygments >=2.13.0,<3.0.0 - - python >=3.8 + - python >=3.10 - typing_extensions >=4.0.0,<5.0.0 + - python license: MIT license_family: MIT purls: - - pkg:pypi/rich?source=hash-mapping - size: 185481 - timestamp: 1730592349978 + - pkg:pypi/rich?source=compressed-mapping + size: 200840 + timestamp: 1760026188268 - conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.3.7-py312h9118e91_0.conda sha256: 457e71eb4a877715353510ec1fc28742bb21f874551a1ca1ef9f91456e18c202 md5: 76dc72c065cc15f69b96656b3431a5a4 @@ -4700,17 +5338,17 @@ packages: license_family: MIT size: 38055572 timestamp: 1754660019384 -- conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_0.conda - sha256: 3c49a0a101c41b7cf6ac05a1872d7a1f91f1b6d02eecb4a36b605a19517862bb - md5: d08db09a552699ee9e7eec56b4eb3899 +- conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_1.conda + sha256: 0557c090913aa63cdbe821dbdfa038a321b488e22bc80196c4b3b1aace4914ef + md5: 7c3c2a0f3ebdea2bbc35538d162b43bf depends: - - python >=3.7 + - python >=3.9 license: MIT license_family: MIT purls: - pkg:pypi/shellingham?source=hash-mapping - size: 14568 - timestamp: 1698144516278 + size: 14462 + timestamp: 1733301007770 - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda sha256: 458227f759d5e3fcec5d9b7acce54e10c9e1f4f4b7ec978f3bfd54ce4ee9853d md5: 3339e3b65d58accf4ca4fb8748ab16b3 @@ -4719,8 +5357,24 @@ packages: - python license: MIT license_family: MIT + purls: + - pkg:pypi/six?source=hash-mapping size: 18455 timestamp: 1753199211006 +- conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + sha256: 570da295d421661af487f1595045760526964f41471021056e993e73089e9c41 + md5: b1b505328da7a6b246787df4b5a49fbc + depends: + - asttokens + - executing + - pure_eval + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/stack-data?source=hash-mapping + size: 26988 + timestamp: 1733569565672 - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_8.conda sha256: 0053c17ffbd9f8af1a7f864995d70121c292e317804120be4667f37c92805426 md5: 1bad93f0aa428d618875ef3a588a889e @@ -4740,6 +5394,8 @@ packages: - webencodings >=0.4 license: BSD-3-Clause license_family: BSD + purls: + - pkg:pypi/tinycss2?source=hash-mapping size: 28285 timestamp: 1729802975370 - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda @@ -4788,76 +5444,135 @@ packages: purls: [] size: 3466348 timestamp: 1748388121356 -- conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.2-pyhd8ed1ab_0.conda - sha256: 5e742ba856168b606ac3c814d247657b1c33b8042371f1a08000bdc5075bc0cc - md5: e977934e00b355ff55ed154904044727 +- conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda + sha256: cb77c660b646c00a48ef942a9e1721ee46e90230c7c570cdeb5a893b5cce9bff + md5: d2732eb636c264dc9aa4cbee404b1a53 depends: - - python >=3.7 + - python >=3.10 + - python license: MIT license_family: MIT purls: - - pkg:pypi/tomli?source=hash-mapping - size: 18203 - timestamp: 1727974767524 -- conda: https://conda.anaconda.org/conda-forge/noarch/typer-0.12.5-pyhd8ed1ab_0.conda - sha256: da9ff9e27c5fa8268c2d5898335485a897d9496eef3b5b446cd9387a89d168de - md5: be70216cc1a5fe502c849676baabf498 + - pkg:pypi/tomli?source=compressed-mapping + size: 20973 + timestamp: 1760014679845 +- conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.2-py310h7c4b9e2_1.conda + sha256: 8dc52bac73848a0334c65491f8de31c5c298464888cfa35d1c41b8d3051131f0 + md5: c5f63ba41df24b9025c9196353541ed5 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.10,<3.11.0a0 + - python_abi 3.10.* *_cp310 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/tornado?source=hash-mapping + size: 661361 + timestamp: 1756854980081 +- conda: https://conda.anaconda.org/conda-forge/osx-64/tornado-6.5.2-py310h1b7cace_1.conda + sha256: 56e357b85e36789cef05dbc990fd361c4abb3701b3d5107cfc074155de811748 + md5: a495c530a0725c970536290e6a5d7ae1 + depends: + - __osx >=10.13 + - python >=3.10,<3.11.0a0 + - python_abi 3.10.* *_cp310 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/tornado?source=hash-mapping + size: 659928 + timestamp: 1756855069568 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.2-py310h7bdd564_1.conda + sha256: d5df47ef4a06ba615ed489d043e412c0fadd7d51f6ea10824a3015f005c17abd + md5: d1e218531d3d4f5aee7c4c6179194d0c + depends: + - __osx >=11.0 + - python >=3.10,<3.11.0a0 + - python >=3.10,<3.11.0a0 *_cpython + - python_abi 3.10.* *_cp310 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/tornado?source=hash-mapping + size: 661628 + timestamp: 1756855266674 +- conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.2-py310h29418f3_1.conda + sha256: fdb4d8a01f361dad584b3f7e2c798759de545b8a01b513b084e7f22e3e0774bf + md5: 880cb8e0f344117c527902f48fcd6463 + depends: + - python >=3.10,<3.11.0a0 + - python_abi 3.10.* *_cp310 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/tornado?source=hash-mapping + size: 663921 + timestamp: 1756855305219 +- conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + sha256: f39a5620c6e8e9e98357507262a7869de2ae8cc07da8b7f84e517c9fd6c2b959 + md5: 019a7385be9af33791c989871317e1ed depends: - - python >=3.7 - - typer-slim-standard 0.12.5 hd8ed1ab_0 + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/traitlets?source=hash-mapping + size: 110051 + timestamp: 1733367480074 +- conda: https://conda.anaconda.org/conda-forge/noarch/typer-0.20.0-pyhdb1f59b_0.conda + sha256: e4708f3f7f72e92511b1f6defca8cac520cef1af3cda92c3b7901731f7ddcb75 + md5: 27ec7c3f99366fa64228c3ee4ab49cbc + depends: + - typer-slim-standard ==0.20.0 h65a100f_0 + - python >=3.10 + - python license: MIT license_family: MIT purls: - pkg:pypi/typer?source=hash-mapping - size: 53350 - timestamp: 1724613663049 -- conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-0.12.5-pyhd8ed1ab_0.conda - sha256: 7be1876627495047f3f07c52c93ddc2ae2017b93affe58110a5474e5ebcb2662 - md5: a46aa56c0ca7cc2bd38baffc2686f0a6 + size: 79367 + timestamp: 1760982314002 +- conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-0.20.0-pyhcf101f3_0.conda + sha256: 08904a433b7ab6b2e0267576043a8397bb3ce7296d71aef34ae7d2506b2c192a + md5: d8ad446a00bbd434d6d03cdcc9b46524 depends: + - python >=3.10 - click >=8.0.0 - - python >=3.7 - typing_extensions >=3.7.4.3 + - python constrains: + - typer 0.20.0.* - rich >=10.11.0 - - typer >=0.12.5,<0.12.6.0a0 - shellingham >=1.3.0 license: MIT license_family: MIT purls: - pkg:pypi/typer-slim?source=hash-mapping - size: 45641 - timestamp: 1724613646022 -- conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-standard-0.12.5-hd8ed1ab_0.conda - sha256: bb298b116159ec1085f6b29eaeb982006651a0997eda08de8b70cfb6177297f3 - md5: 2dc1ee4046de0692077e9aa9ba351d36 + size: 47419 + timestamp: 1760982313997 +- conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-standard-0.20.0-h65a100f_0.conda + sha256: a4726dec9ec806757f5f0fee65f54f790d3f4854a869bd4cd2c2805c54b52d37 + md5: cfd4be2a44e441b12b58a7d04c9434e9 depends: + - typer-slim ==0.20.0 pyhcf101f3_0 - rich - shellingham - - typer-slim 0.12.5 pyhd8ed1ab_0 license: MIT license_family: MIT purls: [] - size: 46817 - timestamp: 1724613648907 -- pypi: https://files.pythonhosted.org/packages/31/c1/d73ff5900c6b462879039ac92f89424ad1eb544b1f6bd77f12f9c3013e20/types_networkx-3.4.2.20241227-py3-none-any.whl + size: 5294 + timestamp: 1760982314002 +- pypi: https://files.pythonhosted.org/packages/9a/41/cdd9498383b30290cce9de6dbed75fa75d6dc06fe4b47d6da6de4b156aa0/types_networkx-3.5.0.20251001-py3-none-any.whl name: types-networkx - version: 3.4.2.20241227 - sha256: adb0e3f0a16c1481a2cfa97772a0b925b220dcf857f0def1c5ab4c4f349e309d + version: 3.5.0.20251001 + sha256: 4bb9dd0378a52ca57d68f8215f8662e736d800a8ff892b457646eccd828a0230 requires_dist: - numpy>=1.20 - requires_python: '>=3.8' -- conda: https://conda.anaconda.org/conda-forge/noarch/types-requests-2.32.0.20241016-pyhd8ed1ab_0.conda - sha256: b2c58c36589a7670e4131f96fbe779cb943c0acb01192ae0f0e3954d1257cbd4 - md5: 5569933ebb375e7b561bdf64062c1658 - depends: - - python >=3.8 - - urllib3 >=2 - license: Apache-2.0 AND MIT - purls: - - pkg:pypi/types-requests?source=hash-mapping - size: 26313 - timestamp: 1729102626972 + requires_python: '>=3.10' - conda: https://conda.anaconda.org/conda-forge/noarch/types-requests-2.32.0.20250602-pyhd8ed1ab_0.conda sha256: cd4caffb0ff1822b44a42c8ee2bcad8a17123264bd40b851898b3e447c8eeed2 md5: 3f64a5b092b804b9458706ec10f679b9 @@ -4865,6 +5580,8 @@ packages: - python >=3.9 - urllib3 >=2 license: Apache-2.0 AND MIT + purls: + - pkg:pypi/types-requests?source=hash-mapping size: 27002 timestamp: 1748891345319 - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda @@ -4874,19 +5591,9 @@ packages: - typing_extensions ==4.15.0 pyhcf101f3_0 license: PSF-2.0 license_family: PSF + purls: [] size: 91383 timestamp: 1756220668932 -- conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_0.conda - sha256: 0fce54f8ec3e59f5ef3bb7641863be4e1bf1279623e5af3d3fa726e8f7628ddb - md5: ebe6952715e1d5eb567eeebf25250fa7 - depends: - - python >=3.8 - license: PSF-2.0 - license_family: PSF - purls: - - pkg:pypi/typing-extensions?source=hash-mapping - size: 39888 - timestamp: 1717802653893 - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda sha256: 032271135bca55aeb156cee361c81350c6f3fb203f57d024d7e5a1fc9ef18731 md5: 0caa1af407ecff61170c9437a808404d @@ -4895,12 +5602,15 @@ packages: - python license: PSF-2.0 license_family: PSF + purls: + - pkg:pypi/typing-extensions?source=hash-mapping size: 51692 timestamp: 1756220668932 - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda sha256: 5aaa366385d716557e365f0a4e9c3fca43ba196872abbbe3d56bb610d131e192 md5: 4222072737ccff51314b5ece9c7d6f5a license: LicenseRef-Public-Domain + purls: [] size: 122968 timestamp: 1742727099393 - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda @@ -4913,21 +5623,6 @@ packages: purls: [] size: 694692 timestamp: 1756385147981 -- conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.3-pyhd8ed1ab_0.conda - sha256: b6bb34ce41cd93956ad6eeee275ed52390fb3788d6c75e753172ea7ac60b66e5 - md5: 6b55867f385dd762ed99ea687af32a69 - depends: - - brotli-python >=1.0.9 - - h2 >=4,<5 - - pysocks >=1.5.6,<2.0,!=1.5.7 - - python >=3.8 - - zstandard >=0.18.0 - license: MIT - license_family: MIT - purls: - - pkg:pypi/urllib3?source=hash-mapping - size: 98076 - timestamp: 1726496531769 - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda sha256: 4fb9789154bd666ca74e428d973df81087a697dbb987775bc3198d2215f240f8 md5: 436c165519e140cb08d246a4472a9d6a @@ -4939,6 +5634,8 @@ packages: - zstandard >=0.18.0 license: MIT license_family: MIT + purls: + - pkg:pypi/urllib3?source=hash-mapping size: 101735 timestamp: 1750271478254 - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h2b53caa_32.conda @@ -5025,6 +5722,17 @@ packages: license_family: APACHE size: 176905 timestamp: 1756135615605 +- conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.14-pyhd8ed1ab_0.conda + sha256: e311b64e46c6739e2a35ab8582c20fa30eb608da130625ed379f4467219d4813 + md5: 7e1e5ff31239f9cd5855714df8a3783d + depends: + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/wcwidth?source=hash-mapping + size: 33670 + timestamp: 1758622418893 - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda sha256: 19ff205e138bb056a46f9e3839935a2e60bd1cf01c8241a5e172a422fed4f9c6 md5: 2841eb5bfc75ce15e9a0054b98dcd64d @@ -5032,19 +5740,10 @@ packages: - python >=3.9 license: BSD-3-Clause license_family: BSD + purls: + - pkg:pypi/webencodings?source=hash-mapping size: 15496 timestamp: 1733236131358 -- conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_7.conda - sha256: c5297692ab34aade5e21107abaf623d6f93847662e25f655320038d2bfa1a812 - md5: c998c13b2f998af57c3b88c7a47979e0 - depends: - - __win - - python >=3.6 - license: LicenseRef-Public-Domain - purls: - - pkg:pypi/win-inet-pton?source=hash-mapping - size: 9602 - timestamp: 1727796413384 - conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda sha256: 93807369ab91f230cf9e6e2a237eaa812492fe00face5b38068735858fba954f md5: 46e441ba871f524e2b067929da3051c2 @@ -5052,6 +5751,8 @@ packages: - __win - python >=3.9 license: LicenseRef-Public-Domain + purls: + - pkg:pypi/win-inet-pton?source=hash-mapping size: 9555 timestamp: 1733130678956 - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.1.2-hb9d3cd8_0.conda @@ -5187,148 +5888,6 @@ packages: license_family: MIT size: 33005 timestamp: 1734229037766 -- conda: https://conda.anaconda.org/conda-forge/linux-64/xz-5.8.1-hbcc6ac9_2.conda - sha256: 802725371682ea06053971db5b4fb7fbbcaee9cb1804ec688f55e51d74660617 - md5: 68eae977d7d1196d32b636a026dc015d - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - liblzma 5.8.1 hb9d3cd8_2 - - liblzma-devel 5.8.1 hb9d3cd8_2 - - xz-gpl-tools 5.8.1 hbcc6ac9_2 - - xz-tools 5.8.1 hb9d3cd8_2 - license: 0BSD AND LGPL-2.1-or-later AND GPL-2.0-or-later - purls: [] - size: 23987 - timestamp: 1749230104359 -- conda: https://conda.anaconda.org/conda-forge/osx-64/xz-5.8.1-h357f2ed_2.conda - sha256: 89248de6c9417522b6fec011dc26b81c25af731a31ba91e668f72f1b9aab05d7 - md5: 7eee908c7df8478c1f35b28efa2e42b1 - depends: - - __osx >=10.13 - - liblzma 5.8.1 hd471939_2 - - liblzma-devel 5.8.1 hd471939_2 - - xz-gpl-tools 5.8.1 h357f2ed_2 - - xz-tools 5.8.1 hd471939_2 - license: 0BSD AND LGPL-2.1-or-later AND GPL-2.0-or-later - purls: [] - size: 24033 - timestamp: 1749230223096 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-5.8.1-h9a6d368_2.conda - sha256: afb747cf017b67cc31d54c6e6c4bd1b1e179fe487a3d23a856232ed7fd0b099b - md5: 39435c82e5a007ef64cbb153ecc40cfd - depends: - - __osx >=11.0 - - liblzma 5.8.1 h39f12f2_2 - - liblzma-devel 5.8.1 h39f12f2_2 - - xz-gpl-tools 5.8.1 h9a6d368_2 - - xz-tools 5.8.1 h39f12f2_2 - license: 0BSD AND LGPL-2.1-or-later AND GPL-2.0-or-later - purls: [] - size: 23995 - timestamp: 1749230346887 -- conda: https://conda.anaconda.org/conda-forge/win-64/xz-5.8.1-h208afaa_2.conda - sha256: 22289a81da4698bb8d13ac032a88a4a1f49505b2303885e1add3d8bd1a7b56e6 - md5: fb3fa84ea37de9f12cc8ba730cec0bdc - depends: - - liblzma 5.8.1 h2466b09_2 - - liblzma-devel 5.8.1 h2466b09_2 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - - xz-tools 5.8.1 h2466b09_2 - license: 0BSD AND LGPL-2.1-or-later AND GPL-2.0-or-later - purls: [] - size: 24430 - timestamp: 1749230691276 -- conda: https://conda.anaconda.org/conda-forge/linux-64/xz-gpl-tools-5.8.1-hbcc6ac9_2.conda - sha256: 840838dca829ec53f1160f3fca6dbfc43f2388b85f15d3e867e69109b168b87b - md5: bf627c16aa26231720af037a2709ab09 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - liblzma 5.8.1 hb9d3cd8_2 - constrains: - - xz 5.8.1.* - license: 0BSD AND LGPL-2.1-or-later AND GPL-2.0-or-later - purls: [] - size: 33911 - timestamp: 1749230090353 -- conda: https://conda.anaconda.org/conda-forge/osx-64/xz-gpl-tools-5.8.1-h357f2ed_2.conda - sha256: 5cdadfff31de7f50d1b2f919dd80697c0a08d90f8d6fb89f00c93751ec135c3c - md5: d4044359fad6af47224e9ef483118378 - depends: - - __osx >=10.13 - - liblzma 5.8.1 hd471939_2 - constrains: - - xz 5.8.1.* - license: 0BSD AND LGPL-2.1-or-later AND GPL-2.0-or-later - purls: [] - size: 33890 - timestamp: 1749230206830 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-gpl-tools-5.8.1-h9a6d368_2.conda - sha256: a0790cfb48d240e7b655b0d797a00040219cf39e3ee38e2104e548515df4f9c2 - md5: 09b1442c1d49ac7c5f758c44695e77d1 - depends: - - __osx >=11.0 - - liblzma 5.8.1 h39f12f2_2 - constrains: - - xz 5.8.1.* - license: 0BSD AND LGPL-2.1-or-later AND GPL-2.0-or-later - purls: [] - size: 34103 - timestamp: 1749230329933 -- conda: https://conda.anaconda.org/conda-forge/linux-64/xz-tools-5.8.1-hb9d3cd8_2.conda - sha256: 58034f3fca491075c14e61568ad8b25de00cb3ae479de3e69be6d7ee5d3ace28 - md5: 1bad2995c8f1c8075c6c331bf96e46fb - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - liblzma 5.8.1 hb9d3cd8_2 - constrains: - - xz 5.8.1.* - license: 0BSD AND LGPL-2.1-or-later - purls: [] - size: 96433 - timestamp: 1749230076687 -- conda: https://conda.anaconda.org/conda-forge/osx-64/xz-tools-5.8.1-hd471939_2.conda - sha256: 3b1d8958f8dceaa4442100d5326b2ec9bcc2e8d7ee55345bf7101dc362fb9868 - md5: 349148960ad74aece88028f2b5c62c51 - depends: - - __osx >=10.13 - - liblzma 5.8.1 hd471939_2 - constrains: - - xz 5.8.1.* - license: 0BSD AND LGPL-2.1-or-later - purls: [] - size: 85777 - timestamp: 1749230191007 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-tools-5.8.1-h39f12f2_2.conda - sha256: 9d1232705e3d175f600dc8e344af9182d0341cdaa73d25330591a28532951063 - md5: 37996935aa33138fca43e4b4563b6a28 - depends: - - __osx >=11.0 - - liblzma 5.8.1 h39f12f2_2 - constrains: - - xz 5.8.1.* - license: 0BSD AND LGPL-2.1-or-later - purls: [] - size: 86425 - timestamp: 1749230316106 -- conda: https://conda.anaconda.org/conda-forge/win-64/xz-tools-5.8.1-h2466b09_2.conda - sha256: 38712f0e62f61741ab69d7551fa863099f5be769bdf9fdbc28542134874b4e88 - md5: e1b62ec0457e6ba10287a49854108fdb - depends: - - liblzma 5.8.1 h2466b09_2 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - constrains: - - xz 5.8.1.* - license: 0BSD AND LGPL-2.1-or-later - purls: [] - size: 67419 - timestamp: 1749230666460 - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda sha256: 6d9ea2f731e284e9316d95fa61869fe7bbba33df7929f82693c121022810f4ad md5: a77f85f77be52ff59391544bfe73390a @@ -5337,6 +5896,7 @@ packages: - __glibc >=2.17,<3.0.a0 license: MIT license_family: MIT + purls: [] size: 85189 timestamp: 1753484064210 - conda: https://conda.anaconda.org/conda-forge/osx-64/yaml-0.2.5-h4132b18_3.conda @@ -5346,6 +5906,7 @@ packages: - __osx >=10.13 license: MIT license_family: MIT + purls: [] size: 79419 timestamp: 1753484072608 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda @@ -5355,6 +5916,7 @@ packages: - __osx >=11.0 license: MIT license_family: MIT + purls: [] size: 83386 timestamp: 1753484079473 - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda @@ -5369,8 +5931,67 @@ packages: - ucrt >=10.0.20348.0 license: MIT license_family: MIT + purls: [] size: 63944 timestamp: 1753484092156 +- conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h387f397_9.conda + sha256: 47cfe31255b91b4a6fa0e9dbaf26baa60ac97e033402dbc8b90ba5fee5ffe184 + md5: 8035e5b54c08429354d5d64027041cad + depends: + - libstdcxx >=14 + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libsodium >=1.0.20,<1.0.21.0a0 + - krb5 >=1.21.3,<1.22.0a0 + license: MPL-2.0 + license_family: MOZILLA + purls: [] + size: 310648 + timestamp: 1757370847287 +- conda: https://conda.anaconda.org/conda-forge/osx-64/zeromq-4.3.5-h6c33b1e_9.conda + sha256: 30aa5a2e9c7b8dbf6659a2ccd8b74a9994cdf6f87591fcc592970daa6e7d3f3c + md5: d940d809c42fbf85b05814c3290660f5 + depends: + - __osx >=10.13 + - libcxx >=19 + - libsodium >=1.0.20,<1.0.21.0a0 + - krb5 >=1.21.3,<1.22.0a0 + license: MPL-2.0 + license_family: MOZILLA + purls: [] + size: 259628 + timestamp: 1757371000392 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h888dc83_9.conda + sha256: b6f9c130646e5971f6cad708e1eee278f5c7eea3ca97ec2fdd36e7abb764a7b8 + md5: 26f39dfe38a2a65437c29d69906a0f68 + depends: + - __osx >=11.0 + - libcxx >=19 + - libsodium >=1.0.20,<1.0.21.0a0 + - krb5 >=1.21.3,<1.22.0a0 + license: MPL-2.0 + license_family: MOZILLA + purls: [] + size: 244772 + timestamp: 1757371008525 +- conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h5bddc39_9.conda + sha256: 690cf749692c8ea556646d1a47b5824ad41b2f6dfd949e4cdb6c44a352fcb1aa + md5: a6c8f8ee856f7c3c1576e14b86cd8038 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - libsodium >=1.0.20,<1.0.21.0a0 + - krb5 >=1.21.3,<1.22.0a0 + license: MPL-2.0 + license_family: MOZILLA + purls: [] + size: 265212 + timestamp: 1757370864284 - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhd8ed1ab_0.conda sha256: 7560d21e1b021fd40b65bfb72f67945a3fcb83d78ad7ccf37b8b3165ec3b68ad md5: df5e78d904988eb55042c0c97446079f @@ -5378,6 +5999,8 @@ packages: - python >=3.9 license: MIT license_family: MIT + purls: + - pkg:pypi/zipp?source=hash-mapping size: 22963 timestamp: 1749421737203 - conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-ng-2.2.5-hde8ca8f_0.conda @@ -5422,20 +6045,23 @@ packages: license_family: Other size: 111210 timestamp: 1754587472195 -- conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.19.0-py38h0a891b7_0.tar.bz2 - sha256: e8fcc17cd856c9df0bd323f3ac00e0e0bcab9eead5269985d0b276634df52372 - md5: eebee9137f42ca4087754ff2d893758e +- conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.25.0-py310h139afa4_0.conda + sha256: de55fe71fa07bdd77eb6ea8819072d8558f315e3b022b4047f2f941d0854405d + md5: 6b243b9f9477ad0b0a90552ebddb27e7 depends: - - cffi >=1.8 - - libgcc-ng >=12 - - python >=3.8,<3.9.0a0 - - python_abi 3.8.* *_cp38 + - python + - cffi >=1.11 + - zstd >=1.5.7,<1.5.8.0a0 + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - zstd >=1.5.7,<1.6.0a0 + - python_abi 3.10.* *_cp310 license: BSD-3-Clause license_family: BSD purls: - pkg:pypi/zstandard?source=hash-mapping - size: 687551 - timestamp: 1667296255169 + size: 455402 + timestamp: 1757930101765 - conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.25.0-py312h5253ce2_0.conda sha256: 1a3beda8068b55639edb92da8e0dc2d487e2a11aba627f709aab1d3cd5dd271c md5: 05d73100768745631ab3de9dc1e08da2 @@ -5466,22 +6092,22 @@ packages: license_family: BSD size: 127864 timestamp: 1757930108791 -- conda: https://conda.anaconda.org/conda-forge/osx-64/zstandard-0.23.0-py38hdb7df32_0.conda - sha256: 9bd6178c20b468985784e63e5a5752ba09c23922e9113694bb0c308f035a85aa - md5: be8ae480a2c487987fc4aeda477328dc +- conda: https://conda.anaconda.org/conda-forge/osx-64/zstandard-0.25.0-py310h3aa7efa_0.conda + sha256: 5d8ebc310a0e0be8555ff154d410df4de85e6220e02444e4020e01cf6911ebfd + md5: 146ac62fe70655bc2f33f4c4917d193d depends: - - __osx >=10.13 + - python - cffi >=1.11 - - python >=3.8,<3.9.0a0 - - python_abi 3.8.* *_cp38 - - zstd >=1.5.6,<1.5.7.0a0 - - zstd >=1.5.6,<1.6.0a0 + - zstd >=1.5.7,<1.5.8.0a0 + - __osx >=10.13 + - python_abi 3.10.* *_cp310 + - zstd >=1.5.7,<1.6.0a0 license: BSD-3-Clause license_family: BSD purls: - pkg:pypi/zstandard?source=hash-mapping - size: 401982 - timestamp: 1721044258096 + size: 452388 + timestamp: 1757930173106 - conda: https://conda.anaconda.org/conda-forge/osx-64/zstandard-0.25.0-py312h01f6755_0.conda sha256: 90134ee636809d06ad250c19c370cbefe6ee28b9c6c2403ee8d8817ef9e5e804 md5: 794e234c2641a865810473af674536ce @@ -5510,23 +6136,23 @@ packages: license_family: BSD size: 123692 timestamp: 1757930114277 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstandard-0.23.0-py38h43bb1b3_0.conda - sha256: 14c3139f32c22b61ee4dbf9f4f1e44b445a54505b3e09dd0e41146f40865afe2 - md5: be90d81878902f7c0e7bb2846fbe126c +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstandard-0.25.0-py310hf151d32_0.conda + sha256: cb944446ad8d8fbbcd8b865731384e81034c1c2d058fe40601dc7763f6c3f67b + md5: 3d4806e7e69d2410244ef196cda2e180 depends: - - __osx >=11.0 + - python - cffi >=1.11 - - python >=3.8,<3.9.0a0 - - python >=3.8,<3.9.0a0 *_cpython - - python_abi 3.8.* *_cp38 - - zstd >=1.5.6,<1.5.7.0a0 - - zstd >=1.5.6,<1.6.0a0 + - zstd >=1.5.7,<1.5.8.0a0 + - __osx >=11.0 + - python 3.10.* *_cpython + - python_abi 3.10.* *_cp310 + - zstd >=1.5.7,<1.6.0a0 license: BSD-3-Clause license_family: BSD purls: - pkg:pypi/zstandard?source=hash-mapping - size: 322221 - timestamp: 1721044445032 + size: 377504 + timestamp: 1757930165987 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstandard-0.25.0-py312h37e1c23_0.conda sha256: 7b50d48e4f2d17d8a322d5896c1b0db49def163b105a078aaca4922ef7290696 md5: c05d2d4b438ef09c55b291e062eddf79 @@ -5557,24 +6183,27 @@ packages: license_family: BSD size: 125883 timestamp: 1757930173407 -- conda: https://conda.anaconda.org/conda-forge/win-64/zstandard-0.23.0-py38hf92978b_0.conda - sha256: 1a7dd43fd4d3e667521b2fb3477cd20ef6586c5696c003ef5c6bff01dca37a71 - md5: 98bd13d4c311b4c8402446c93b142f9b +- conda: https://conda.anaconda.org/conda-forge/win-64/zstandard-0.25.0-py310h1637853_0.conda + sha256: 1bc80de45c577d2a80afb056a52b873f795ce3ed3d131d44a7320dd82835b8f0 + md5: b45df16a296e7127a14dd9362103b80b depends: + - python - cffi >=1.11 - - python >=3.8,<3.9.0a0 - - python_abi 3.8.* *_cp38 + - zstd >=1.5.7,<1.5.8.0a0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - - zstd >=1.5.6,<1.5.7.0a0 - - zstd >=1.5.6,<1.6.0a0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.10.* *_cp310 + - zstd >=1.5.7,<1.6.0a0 license: BSD-3-Clause license_family: BSD purls: - pkg:pypi/zstandard?source=hash-mapping - size: 311395 - timestamp: 1721044412087 + size: 364179 + timestamp: 1757930140810 - conda: https://conda.anaconda.org/conda-forge/win-64/zstandard-0.25.0-py312he5662c2_0.conda sha256: 23675fe9b8574fe93d3912d13a9855be9c7800bd34f8e944dd3d5b9b7265838d md5: b14e2ff42f539a7eae7eaf03bd89ab82 @@ -5626,17 +6255,6 @@ packages: purls: [] size: 567578 timestamp: 1742433379869 -- conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.6-h915ae27_0.conda - sha256: efa04a98cb149643fa54c4dad5a0179e36a5fbc88427ea0eec88ceed87fd0f96 - md5: 4cb2cd56f039b129bb0e491c1164167e - depends: - - __osx >=10.9 - - libzlib >=1.2.13,<2.0.0a0 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 498900 - timestamp: 1714723303098 - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h8210216_2.conda sha256: c171c43d0c47eed45085112cb00c8c7d4f0caa5a32d47f2daca727e45fb98dca md5: cd60a4a5a8d6a476b30d8aa4bb49251a @@ -5645,19 +6263,9 @@ packages: - libzlib >=1.3.1,<2.0a0 license: BSD-3-Clause license_family: BSD + purls: [] size: 485754 timestamp: 1742433356230 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.6-hb46c0d2_0.conda - sha256: 2d4fd1ff7ee79cd954ca8e81abf11d9d49954dd1fef80f27289e2402ae9c2e09 - md5: d96942c06c3e84bfcc5efb038724a7fd - depends: - - __osx >=11.0 - - libzlib >=1.2.13,<2.0.0a0 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 405089 - timestamp: 1714723101397 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-h6491c7d_2.conda sha256: 0d02046f57f7a1a3feae3e9d1aa2113788311f3cf37a3244c71e61a93177ba67 md5: e6f69c7bcccdefa417f056fa593b40f0 @@ -5666,21 +6274,9 @@ packages: - libzlib >=1.3.1,<2.0a0 license: BSD-3-Clause license_family: BSD + purls: [] size: 399979 timestamp: 1742433432699 -- conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.6-h0ea2cb4_0.conda - sha256: 768e30dc513568491818fb068ee867c57c514b553915536da09e5d10b4ebf3c3 - md5: 9a17230f95733c04dc40a2b1e5491d74 - depends: - - libzlib >=1.2.13,<2.0.0a0 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 349143 - timestamp: 1714723445995 - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-hbeecb71_2.conda sha256: bc64864377d809b904e877a98d0584f43836c9f2ef27d3d2a1421fa6eae7ca04 md5: 21f56217d6125fb30c3c3f10c786d751 @@ -5691,5 +6287,6 @@ packages: - vc14_runtime >=14.29.30139 license: BSD-3-Clause license_family: BSD + purls: [] size: 354697 timestamp: 1742433568506 diff --git a/py-rattler-build/pixi.toml b/py-rattler-build/pixi.toml index 0d405a288..804063e85 100644 --- a/py-rattler-build/pixi.toml +++ b/py-rattler-build/pixi.toml @@ -7,7 +7,7 @@ platforms = ["win-64", "linux-64", "osx-64", "osx-arm64"] license = "BSD-3-Clause" preview = ["pixi-build"] # We are using the oldest supported python version as the build variant here -build-variants = { python = ["3.8"] } +build-variants = { python = ["3.10"] } [package.build] backend = { name = "pixi-build-python", version = "*" } @@ -42,26 +42,29 @@ lint-rust = "cargo clippy --all-targets --workspace -- -D warnings" [feature.test.dependencies] py-rattler-build = { path = "." } +ipykernel = "*" lua = "*" # Python 3.8 is the minimum supported version, so we use that for testing -python = "3.8.*" +python = "3.10.*" mypy = "~=1.5.1" -pytest = "~=7.4.0" -pytest-asyncio = "0.21.1.*" -pytest-xprocess = ">=0.23.0,<0.24" - +pytest = ">=8.3" +pytest-asyncio = "1.2.*" +pytest-xprocess = "1.0.*" +inline-snapshot = ">=0.31.0" # used in examples typer = "*" +rich = ">=14.2.0,<15" [feature.test.pypi-dependencies] types-networkx = "*" [feature.test.tasks] check-cargo-lock = "cargo check --locked" -test = { cmd = "pytest --doctest-modules", depends-on = ["type-check"] } +test = { cmd = "pytest ./tests --doctest-modules", depends-on = ["type-check"] } +# test = { cmd = "pytest ./tests -v -s", depends-on = ["type-check"] } type-check = { cmd = "mypy" } [feature.docs.dependencies] diff --git a/py-rattler-build/pyproject.toml b/py-rattler-build/pyproject.toml index 04549a110..029608d16 100644 --- a/py-rattler-build/pyproject.toml +++ b/py-rattler-build/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "maturin" [project] name = "py-rattler-build" version = "0.48.0" -requires-python = ">=3.8" +requires-python = ">=3.10" description = "The fastest way to build conda packages programatically" readme = "README.md" authors = [{ name = "Wolf Vollprecht", email = "w.vollprecht@gmail.com" }] @@ -30,11 +30,11 @@ features = ["pyo3/extension-module"] [tool.ruff] line-length = 120 -target-version = "py38" +target-version = "py310" [tool.mypy] -python_version = "3.8" -files = ["rattler_build", "tests", "examples"] +python_version = "3.10" +files = ["rattler_build", "tests"] strict = true enable_error_code = ["redundant-expr", "truthy-bool", "ignore-without-code"] disable_error_code = ["empty-body"] diff --git a/py-rattler-build/rattler_build/__init__.py b/py-rattler-build/rattler_build/__init__.py index 1c788a726..3d020ad9e 100644 --- a/py-rattler-build/rattler_build/__init__.py +++ b/py-rattler-build/rattler_build/__init__.py @@ -19,10 +19,22 @@ Source, TestType, TestTypeEnum, - SelectorConfig, ) from . import recipe_generation +from . import stage0 +from . import stage1 +from . import render +from . import tool_config +from . import build_types +from . import platform_types +from . import progress +from .variant_config import VariantConfig +from .jinja_config import JinjaConfig +from .tool_config import ToolConfiguration +from .build_types import Directories, PackagingSettings +from .platform_types import Platform, PlatformWithVirtualPackages +from .render import RenderConfig from pathlib import Path @@ -48,7 +60,21 @@ "Source", "TestType", "TestTypeEnum", - "SelectorConfig", + "JinjaConfig", + "stage0", + "stage1", + "render", + "tool_config", + "build_types", + "platform_types", + "progress", + "VariantConfig", + "ToolConfiguration", + "Directories", + "PackagingSettings", + "Platform", + "PlatformWithVirtualPackages", + "RenderConfig", ] diff --git a/py-rattler-build/rattler_build/build_types.py b/py-rattler-build/rattler_build/build_types.py new file mode 100644 index 000000000..ca7d9b30c --- /dev/null +++ b/py-rattler-build/rattler_build/build_types.py @@ -0,0 +1,14 @@ +""" +Build-related types for rattler-build. + +This module provides Pythonic APIs for build directories, packaging settings, and other +build-related configuration. +""" + +from . import rattler_build as _rb + +# Re-export the Rust types +Directories = _rb.build_types.Directories +PackagingSettings = _rb.build_types.PackagingSettings + +__all__ = ["Directories", "PackagingSettings"] diff --git a/py-rattler-build/rattler_build/jinja_config.py b/py-rattler-build/rattler_build/jinja_config.py new file mode 100644 index 000000000..1e4787247 --- /dev/null +++ b/py-rattler-build/rattler_build/jinja_config.py @@ -0,0 +1,94 @@ +from typing import Any, Dict, Optional +from .rattler_build import PyJinjaConfig + + +class JinjaConfig: + """Python wrapper for PyJinjaConfig to provide a cleaner interface.""" + + _config: PyJinjaConfig + + def __init__( + self, + target_platform: Optional[str] = None, + host_platform: Optional[str] = None, + build_platform: Optional[str] = None, + experimental: Optional[bool] = None, + allow_undefined: Optional[bool] = None, + variant: Optional[Dict[str, Any]] = None, + ): + self._config = PyJinjaConfig( + target_platform=target_platform, + host_platform=host_platform, + build_platform=build_platform, + experimental=experimental, + allow_undefined=allow_undefined, + variant=variant, + ) + + @property + def target_platform(self) -> Optional[str]: + """Get the target platform.""" + return self._config.target_platform + + @target_platform.setter + def target_platform(self, value: Optional[str]) -> None: + """Set the target platform.""" + self._config.target_platform = value + + @property + def host_platform(self) -> Optional[str]: + """Get the host platform.""" + return self._config.host_platform + + @host_platform.setter + def host_platform(self, value: Optional[str]) -> None: + """Set the host platform.""" + self._config.host_platform = value + + @property + def build_platform(self) -> Optional[str]: + """Get the build platform.""" + return self._config.build_platform + + @build_platform.setter + def build_platform(self, value: Optional[str]) -> None: + """Set the build platform.""" + self._config.build_platform = value + + @property + def experimental(self) -> Optional[bool]: + """Get whether experimental features are enabled.""" + return self._config.experimental + + @experimental.setter + def experimental(self, value: Optional[bool]) -> None: + """Set whether experimental features are enabled.""" + self._config.experimental = value + + @property + def allow_undefined(self) -> Optional[bool]: + """Get whether undefined variables are allowed.""" + return self._config.allow_undefined + + @allow_undefined.setter + def allow_undefined(self, value: Optional[bool]) -> None: + """Set whether undefined variables are allowed.""" + self._config.allow_undefined = value + + @property + def variant(self) -> Dict[str, Any]: + """Get the variant configuration.""" + return self._config.variant + + @variant.setter + def variant(self, value: Dict[str, Any]) -> None: + """Set the variant configuration.""" + self._config.variant = value + + @property + def config(self) -> PyJinjaConfig: + """Get the underlying PyJinjaConfig (for backward compatibility).""" + return self._config + + def __repr__(self) -> str: + return f"JinjaConfig(target_platform={self.target_platform!r}, variant={self.variant!r})" diff --git a/py-rattler-build/rattler_build/platform_types.py b/py-rattler-build/rattler_build/platform_types.py new file mode 100644 index 000000000..1d6feea4f --- /dev/null +++ b/py-rattler-build/rattler_build/platform_types.py @@ -0,0 +1,14 @@ +""" +Platform-related types for rattler-build. + +This module provides Pythonic APIs for platform detection, virtual packages, and +platform-specific configuration. +""" + +from . import rattler_build as _rb + +# Re-export the Rust types +Platform = _rb.platform_types.Platform +PlatformWithVirtualPackages = _rb.platform_types.PlatformWithVirtualPackages + +__all__ = ["Platform", "PlatformWithVirtualPackages"] diff --git a/py-rattler-build/rattler_build/progress.py b/py-rattler-build/rattler_build/progress.py new file mode 100644 index 000000000..2d6680cd0 --- /dev/null +++ b/py-rattler-build/rattler_build/progress.py @@ -0,0 +1,461 @@ +""" +Progress reporting and callbacks for rattler-build. + +This module provides base classes and implementations for progress reporting +during recipe rendering and building. You can use the built-in implementations +(RichProgressCallback, TqdmProgressCallback) or create your own by subclassing +ProgressCallback. +""" + +from typing import Protocol, Optional, runtime_checkable + +# Try to import from Rust module, but provide Python fallbacks if not available yet +try: + from rattler_build.rattler_build.progress import ( + DownloadStartEvent, + DownloadProgressEvent, + DownloadCompleteEvent, + BuildStepEvent, + LogEvent, + ) +except (ImportError, AttributeError): + # Fallback Python implementations for when Rust bindings aren't ready + class DownloadStartEvent: + """Event fired when a download starts.""" + + def __init__(self, url: str, total_bytes: Optional[int] = None): + self.url = url + self.total_bytes = total_bytes + + def __repr__(self): + return f"DownloadStartEvent(url='{self.url}', total_bytes={self.total_bytes})" + + class DownloadProgressEvent: + """Event fired during download progress.""" + + def __init__(self, url: str, bytes_downloaded: int, total_bytes: Optional[int] = None): + self.url = url + self.bytes_downloaded = bytes_downloaded + self.total_bytes = total_bytes + + def __repr__(self): + return f"DownloadProgressEvent(url='{self.url}', bytes_downloaded={self.bytes_downloaded}, total_bytes={self.total_bytes})" + + class DownloadCompleteEvent: + """Event fired when a download completes.""" + + def __init__(self, url: str): + self.url = url + + def __repr__(self): + return f"DownloadCompleteEvent(url='{self.url}')" + + class BuildStepEvent: + """Event fired when a build step begins.""" + + def __init__(self, step_name: str, message: str): + self.step_name = step_name + self.message = message + + def __repr__(self): + return f"BuildStepEvent(step_name='{self.step_name}', message='{self.message}')" + + class LogEvent: + """Event fired for log messages.""" + + def __init__(self, level: str, message: str, span: Optional[str] = None): + self.level = level + self.message = message + self.span = span + + def __repr__(self): + return f"LogEvent(level='{self.level}', message='{self.message}', span={self.span})" + + +__all__ = [ + "ProgressCallback", + "DownloadStartEvent", + "DownloadProgressEvent", + "DownloadCompleteEvent", + "BuildStepEvent", + "LogEvent", + "SimpleProgressCallback", + "RichProgressCallback", +] + + +@runtime_checkable +class ProgressCallback(Protocol): + """Protocol for progress callbacks. + + Implement this protocol to receive progress updates during builds. + All methods are optional - only implement the ones you need. + + Example: + ```python + class MyCallback(ProgressCallback): + def on_download_progress(self, event: DownloadProgressEvent): + percent = event.bytes_downloaded / event.total_bytes * 100 + print(f"Downloaded {percent:.1f}%") + + def on_build_step(self, event: BuildStepEvent): + print(f"[{event.step_name}] {event.message}") + ``` + """ + + def on_download_start(self, event: DownloadStartEvent) -> None: + """Called when a download starts. + + Args: + event: Event containing download URL and expected total bytes + """ + ... + + def on_download_progress(self, event: DownloadProgressEvent) -> None: + """Called periodically during download to report progress. + + Args: + event: Event containing bytes downloaded and total bytes + """ + ... + + def on_download_complete(self, event: DownloadCompleteEvent) -> None: + """Called when a download completes successfully. + + Args: + event: Event containing the download URL + """ + ... + + def on_build_step(self, event: BuildStepEvent) -> None: + """Called when a new build step begins. + + Args: + event: Event containing step name and message + """ + ... + + def on_log(self, event: LogEvent) -> None: + """Called for log messages. + + Args: + event: Event containing log level, message, and optional span + """ + ... + + +class SimpleProgressCallback: + """Simple console-based progress callback. + + Prints progress updates to the console with simple formatting. + + Example: + ```python + from rattler_build import Recipe, VariantConfig + from rattler_build.progress import SimpleProgressCallback + + recipe = Recipe.from_file("recipe.yaml") + rendered = recipe.render(VariantConfig()) + + callback = SimpleProgressCallback() + # Use callback in build (to be implemented) + ``` + """ + + def on_download_start(self, event: DownloadStartEvent) -> None: + """Print download start message.""" + if event.total_bytes: + print(f"šŸ“„ Downloading {event.url} ({event.total_bytes / 1024 / 1024:.1f} MB)") + else: + print(f"šŸ“„ Downloading {event.url}") + + def on_download_progress(self, event: DownloadProgressEvent) -> None: + """Print download progress (only at 25% intervals to avoid spam).""" + if event.total_bytes: + percent = (event.bytes_downloaded / event.total_bytes) * 100 + if int(percent) % 25 == 0 and percent > 0: + print(f" {percent:.0f}% complete") + + def on_download_complete(self, event: DownloadCompleteEvent) -> None: + """Print download complete message.""" + print(f"āœ… Downloaded {event.url}") + + def on_build_step(self, event: BuildStepEvent) -> None: + """Print build step message.""" + print(f"šŸ”Ø [{event.step_name}] {event.message}") + + def on_log(self, event: LogEvent) -> None: + """Print log message with appropriate prefix.""" + prefix = { + "error": "āŒ", + "warn": "āš ļø ", + "info": "ā„¹ļø ", + }.get(event.level, " ") + span_str = f" [{event.span}]" if event.span else "" + print(f"{prefix}{span_str} {event.message}") + + +class RichProgressCallback: + """Rich-based progress callback with beautiful terminal output. + + Automatically creates progress bars for long-running operations by parsing + log messages. Shows spinners for operations and bars for downloads. + + Requires the 'rich' library to be installed: + pip install rich + + Example: + ```python + from rattler_build import Recipe, VariantConfig + from rattler_build.progress import RichProgressCallback + + recipe = Recipe.from_file("recipe.yaml") + rendered = recipe.render(VariantConfig()) + + with RichProgressCallback() as callback: + # Use callback in build (to be implemented) + pass + ``` + """ + + def __init__(self, show_logs: bool = True, show_details: bool = False): + """Initialize the Rich progress callback. + + Args: + show_logs: Whether to display all log messages (default: True - recommended for debugging) + show_details: Whether to show detailed logs like index operations (default: False) + """ + try: + from rich.progress import ( + Progress, + SpinnerColumn, + BarColumn, + TextColumn, + TimeElapsedColumn, + ) + from rich.console import Console + except ImportError: + raise ImportError("Rich library is required for RichProgressCallback. " "Install it with: pip install rich") + + self.show_logs = show_logs + self.show_details = show_details + self.console = Console() + + # Create progress for operations + self.progress = Progress( + SpinnerColumn(), + TextColumn("[bold blue]{task.description}"), + BarColumn(complete_style="green", finished_style="bold green"), + TextColumn("[progress.percentage]{task.percentage:>3.0f}%"), + TimeElapsedColumn(), + console=self.console, + ) + + self.tasks = {} # Download tasks + self.operation_tasks = {} # Operation tasks (resolving, building, etc.) + self.current_operation = None + + def __enter__(self): + """Context manager entry.""" + self.progress.start() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Context manager exit.""" + self.progress.stop() + + def on_download_start(self, event: DownloadStartEvent) -> None: + """Create a progress bar for the download.""" + task_id = self.progress.add_task( + f"Downloading {self._shorten_url(event.url)}", + total=event.total_bytes, + ) + self.tasks[event.url] = task_id + + def on_download_progress(self, event: DownloadProgressEvent) -> None: + """Update the download progress bar.""" + task_id = self.tasks.get(event.url) + if task_id is not None: + self.progress.update(task_id, completed=event.bytes_downloaded) + + def on_download_complete(self, event: DownloadCompleteEvent) -> None: + """Mark the download as complete.""" + task_id = self.tasks.get(event.url) + if task_id is not None: + self.progress.update(task_id, completed=True) + del self.tasks[event.url] + + def on_build_step(self, event: BuildStepEvent) -> None: + """Update or create a build step task.""" + if self.step_task is not None: + self.progress.remove_task(self.step_task) + + self.step_task = self.progress.add_task( + f"[cyan]{event.step_name}[/cyan]: {event.message}", + total=None, # Indeterminate progress + ) + + def on_log(self, event: LogEvent) -> None: + """Parse log messages and create/update progress bars.""" + + msg = event.message + span = event.span or "" + + # Skip noisy index operations unless show_details is True + if not self.show_details and ( + "index_subdir" in span or "Adding 0 packages" in msg or "Writing repodata" in msg + ): + return + + # Detect operation starts and create progress indicators + if "Starting build of" in msg: + self._complete_operation() + self.current_operation = self.progress.add_task("šŸ”Ø Building package", total=100) + self.progress.update(self.current_operation, advance=10) + + elif "Fetching source code" in span: + self._complete_operation() + self.current_operation = self.progress.add_task("šŸ“„ Fetching sources", total=100) + if "No sources" in msg: + self.progress.update(self.current_operation, completed=100) + else: + self.progress.update(self.current_operation, advance=50) + + elif "Resolving environments" in span: + if self.current_operation is None or "Fetching" not in str( + self.progress.tasks[self.current_operation].description + ): + self._complete_operation() + self.current_operation = self.progress.add_task("šŸ” Resolving dependencies", total=100) + # Advance progress as we see different stages + if "Platform:" in msg: + self.progress.update(self.current_operation, advance=20) + elif "Specs:" in msg: + self.progress.update(self.current_operation, advance=20) + + elif "get_or_create_subdir" in span and "sharded repodata" in msg: + if self.current_operation: + self.progress.update(self.current_operation, advance=5) + + elif "Running build for" in span: + # Only create the task once for the entire build script phase + if self.current_operation is None or "āš™ļø Running build script" not in str( + self.progress.tasks[self.current_operation].description + ): + self._complete_operation() + self.current_operation = self.progress.add_task("āš™ļø Running build script", total=100) + + # Update progress based on environment updates + if "Successfully updated the build environment" in msg: + self.progress.update(self.current_operation, advance=50) + elif "Successfully updated the host environment" in msg: + self.progress.update(self.current_operation, completed=100) + + elif "Packaging new files" in span: + # Only create the packaging task once, not for every log message + if self.current_operation is None or "šŸ“¦ Packaging" not in str( + self.progress.tasks[self.current_operation].description + ): + self._complete_operation() + self.current_operation = self.progress.add_task("šŸ“¦ Packaging", total=100) + + # Update progress based on packaging steps + if "Copying done" in msg: + self.progress.update(self.current_operation, advance=30) + elif "Post-processing done" in msg: + self.progress.update(self.current_operation, advance=30) + elif "Writing test files" in msg: + self.progress.update(self.current_operation, advance=10) + elif "Writing metadata" in msg: + self.progress.update(self.current_operation, advance=15) + elif "Copying license" in msg: + self.progress.update(self.current_operation, advance=10) + elif "Copying recipe" in msg: + self.progress.update(self.current_operation, advance=5) + + # Show important messages or warnings/errors + if event.level in ("error", "warn") or self.show_logs: + style_map = { + "error": "bold red", + "warn": "bold yellow", + "info": "cyan", + } + style = style_map.get(event.level, "") + + # Format with span if available + if span and event.level == "info": + formatted_msg = f"[dim]│[/dim] [{style}]{span}[/{style}] {msg}" + elif event.level in ("error", "warn"): + prefix = "āŒ" if event.level == "error" else "āš ļø" + formatted_msg = f"[dim]│[/dim] {prefix} [{style}]{msg}[/{style}]" + else: + formatted_msg = f"[dim]│[/dim] {msg}" + + if event.level in ("error", "warn") or (self.show_logs and event.level == "info"): + self.console.print(formatted_msg) + + def _complete_operation(self): + """Complete the current operation task.""" + if self.current_operation is not None: + self.progress.update(self.current_operation, completed=100) + self.current_operation = None + + @staticmethod + def _shorten_url(url: str, max_len: int = 50) -> str: + """Shorten a URL for display.""" + if len(url) <= max_len: + return url + return url[: max_len - 3] + "..." + + +# Create a simple default callback for convenience +default_callback = SimpleProgressCallback() + + +def create_callback(style: str = "simple", **kwargs) -> ProgressCallback: + """Create a progress callback of the specified style. + + Args: + style: Style of callback - "simple", "rich", or "none" + **kwargs: Additional arguments passed to the callback constructor + + Returns: + A progress callback instance + + Example: + ```python + # Simple console output + callback = create_callback("simple") + + # Rich terminal output + callback = create_callback("rich", show_logs=True) + + # No output + callback = create_callback("none") + ``` + """ + if style == "simple": + return SimpleProgressCallback() + elif style == "rich": + return RichProgressCallback(**kwargs) + elif style == "none": + # Empty callback that does nothing + class NoOpCallback: + def on_download_start(self, event): + pass + + def on_download_progress(self, event): + pass + + def on_download_complete(self, event): + pass + + def on_build_step(self, event): + pass + + def on_log(self, event): + pass + + return NoOpCallback() + else: + raise ValueError(f"Unknown callback style: {style}") diff --git a/py-rattler-build/rattler_build/recipe.py b/py-rattler-build/rattler_build/recipe.py index 8052bf109..62be36d27 100644 --- a/py-rattler-build/rattler_build/recipe.py +++ b/py-rattler-build/rattler_build/recipe.py @@ -3,7 +3,7 @@ from enum import Enum from pathlib import Path from typing import Any, Dict, List, Optional, Union -from .rattler_build import parse_recipe_py, PySelectorConfig +from . import stage0 class TestTypeEnum(Enum): @@ -320,98 +320,6 @@ def __repr__(self) -> str: return f"TestType(type='{self.test_type}')" -class SelectorConfig: - """Python wrapper for PySelectorConfig to provide a cleaner interface.""" - - _config: PySelectorConfig - - def __init__( - self, - target_platform: Optional[str] = None, - host_platform: Optional[str] = None, - build_platform: Optional[str] = None, - experimental: Optional[bool] = None, - allow_undefined: Optional[bool] = None, - variant: Optional[Dict[str, Any]] = None, - ): - self._config = PySelectorConfig( - target_platform=target_platform, - host_platform=host_platform, - build_platform=build_platform, - experimental=experimental, - allow_undefined=allow_undefined, - variant=variant, - ) - - @property - def target_platform(self) -> Optional[str]: - """Get the target platform.""" - return self._config.target_platform - - @target_platform.setter - def target_platform(self, value: Optional[str]) -> None: - """Set the target platform.""" - self._config.target_platform = value - - @property - def host_platform(self) -> Optional[str]: - """Get the host platform.""" - return self._config.host_platform - - @host_platform.setter - def host_platform(self, value: Optional[str]) -> None: - """Set the host platform.""" - self._config.host_platform = value - - @property - def build_platform(self) -> Optional[str]: - """Get the build platform.""" - return self._config.build_platform - - @build_platform.setter - def build_platform(self, value: Optional[str]) -> None: - """Set the build platform.""" - self._config.build_platform = value - - @property - def experimental(self) -> Optional[bool]: - """Get whether experimental features are enabled.""" - return self._config.experimental - - @experimental.setter - def experimental(self, value: Optional[bool]) -> None: - """Set whether experimental features are enabled.""" - self._config.experimental = value - - @property - def allow_undefined(self) -> Optional[bool]: - """Get whether undefined variables are allowed.""" - return self._config.allow_undefined - - @allow_undefined.setter - def allow_undefined(self, value: Optional[bool]) -> None: - """Set whether undefined variables are allowed.""" - self._config.allow_undefined = value - - @property - def variant(self) -> Dict[str, Any]: - """Get the variant configuration.""" - return self._config.variant - - @variant.setter - def variant(self, value: Dict[str, Any]) -> None: - """Set the variant configuration.""" - self._config.variant = value - - def __repr__(self) -> str: - return f"SelectorConfig(target_platform={self.target_platform!r}, variant={self.variant!r})" - - @property - def config(self) -> PySelectorConfig: - """Get the underlying PySelectorConfig object.""" - return self._config - - class Recipe: """A parsed conda recipe with object-oriented access to all fields.""" @@ -441,16 +349,14 @@ def from_yaml( experimental: Enable experimental features. Defaults to False. allow_undefined: Allow undefined variables in Jinja templates. Defaults to False. variant: Variant configuration as a dictionary. Defaults to empty. + + NOTE: This method now uses the stage0 API. For more control, use stage0.Recipe.from_yaml() directly. """ - selector_config = SelectorConfig( - target_platform=target_platform, - host_platform=host_platform, - build_platform=build_platform, - experimental=experimental, - allow_undefined=allow_undefined, - variant=variant, - ) - data = parse_recipe_py(yaml_content, selector_config.config) + # Parse using the new stage0 API + stage0_recipe = stage0.Recipe.from_yaml(yaml_content) + + # Convert to dictionary for the legacy API + data = stage0_recipe.to_dict() return cls(data) @classmethod diff --git a/py-rattler-build/rattler_build/render.py b/py-rattler-build/rattler_build/render.py new file mode 100644 index 000000000..617aa5ee5 --- /dev/null +++ b/py-rattler-build/rattler_build/render.py @@ -0,0 +1,560 @@ +""" +Recipe rendering functionality for converting Stage0 to Stage1 recipes with variants. + +This module provides the ability to render Stage0 recipes (parsed but unevaluated) +into Stage1 recipes (fully evaluated and ready to build) using variant configurations. +""" + +from pathlib import Path +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union + +from rattler_build.stage0 import MultiOutputRecipe, SingleOutputRecipe + +# Import for type hints only - avoid circular import +if TYPE_CHECKING: + from rattler_build.tool_config import ToolConfiguration + +# Type for context values - can be strings, numbers, bools, or lists +ContextValue = Union[str, int, float, bool, List[Union[str, int, float, bool]]] + +# Try to import TypeAlias for better type hint support +try: + from typing import TypeAlias +except ImportError: + from typing_extensions import TypeAlias + +if TYPE_CHECKING: + from rattler_build.variant_config import VariantConfig + + # For type checking, use Any placeholders + _RenderConfig = Any + _RenderedVariant = Any + _HashInfo = Any + _PinSubpackageInfo = Any + _render_recipe = Any +else: + # At runtime, import from the Rust module + from . import rattler_build as _rb + + _render = _rb.render + _RenderConfig = _render.RenderConfig + _RenderedVariant = _render.RenderedVariant + _HashInfo = _render.HashInfo + _PinSubpackageInfo = _render.PinSubpackageInfo + _render_recipe = _render.render_recipe + + +class HashInfo: + """ + Hash information for a rendered variant. + + This class wraps the Rust HashInfo type and provides convenient access + to hash information computed during recipe rendering. + + Attributes: + hash: The hash string (first 7 letters of the sha1sum) + prefix: The hash prefix (e.g., 'py38' or 'np111') + + Example: + >>> hash_info = variant.hash_info() + >>> if hash_info: + ... print(f"Hash: {hash_info.hash}") + ... print(f"Prefix: {hash_info.prefix}") + """ + + def __init__(self, inner: _HashInfo): + """Create a HashInfo from the Rust object.""" + self._inner = inner + + @property + def hash(self) -> str: + """Get the hash string (first 7 letters of sha1sum).""" + return self._inner.hash + + @property + def prefix(self) -> str: + """Get the hash prefix (e.g., 'py38' or 'np111').""" + return self._inner.prefix + + def __repr__(self) -> str: + return repr(self._inner) + + def __str__(self) -> str: + return f"HashInfo(hash={self.hash!r}, prefix={self.prefix!r})" + + +class PinSubpackageInfo: + """ + Information about a pin_subpackage dependency. + + This class wraps the Rust PinSubpackageInfo type and provides information + about packages pinned via the pin_subpackage() Jinja function. + + Attributes: + name: The name of the pinned subpackage + version: The version of the pinned subpackage + build_string: The build string of the pinned subpackage (if known) + exact: Whether this is an exact pin + + Example: + >>> pins = variant.pin_subpackages() + >>> for name, info in pins.items(): + ... print(f"{name}: {info.version} (exact={info.exact})") + """ + + def __init__(self, inner: _PinSubpackageInfo): + """Create a PinSubpackageInfo from the Rust object.""" + self._inner = inner + + @property + def name(self) -> str: + """Get the package name.""" + return self._inner.name + + @property + def version(self) -> str: + """Get the package version.""" + return self._inner.version + + @property + def build_string(self) -> Optional[str]: + """Get the build string if available.""" + return self._inner.build_string + + @property + def exact(self) -> bool: + """Check if this is an exact pin.""" + return self._inner.exact + + def __repr__(self) -> str: + return repr(self._inner) + + def __str__(self) -> str: + return ( + f"PinSubpackageInfo(name={self.name!r}, version={self.version!r}, " + f"build_string={self.build_string!r}, exact={self.exact})" + ) + + +class RenderConfig: + """Configuration for rendering recipes with variants. + + This class configures how recipes are rendered, including platform settings, + experimental features, and additional Jinja context variables. + + Args: + target_platform: Target platform (e.g., "linux-64", "osx-arm64") + build_platform: Build platform (where the build runs) + host_platform: Host platform (for cross-compilation) + experimental: Enable experimental features + recipe_path: Path to the recipe file (for relative path resolution) + + Example: + >>> config = RenderConfig( + ... target_platform="linux-64", + ... experimental=True + ... ) + >>> config.set_context("custom_var", "value") + """ + + def __init__( + self, + target_platform: Optional[str] = None, + build_platform: Optional[str] = None, + host_platform: Optional[str] = None, + experimental: bool = False, + recipe_path: Optional[str] = None, + ): + """Create a new render configuration.""" + self._config = _RenderConfig( + target_platform=target_platform, + build_platform=build_platform, + host_platform=host_platform, + experimental=experimental, + recipe_path=recipe_path, + ) + + def set_context(self, key: str, value: ContextValue) -> None: + """Add an extra context variable for Jinja rendering. + + Args: + key: Variable name + value: Variable value (can be string, bool, int, float, or list) + """ + self._config.set_context(key, value) + + def get_context(self, key: str) -> Optional[ContextValue]: + """Get an extra context variable. + + Args: + key: Variable name + + Returns: + The variable value, or None if not found + """ + return self._config.get_context(key) + + def get_all_context(self) -> Dict[str, ContextValue]: + """Get all extra context variables as a dictionary.""" + return self._config.get_all_context() + + @property + def target_platform(self) -> str: + """Get the target platform.""" + return self._config.target_platform() + + @target_platform.setter + def target_platform(self, value: str) -> None: + """Set the target platform.""" + self._config.set_target_platform(value) + + @property + def build_platform(self) -> str: + """Get the build platform.""" + return self._config.build_platform() + + @build_platform.setter + def build_platform(self, value: str) -> None: + """Set the build platform.""" + self._config.set_build_platform(value) + + @property + def host_platform(self) -> str: + """Get the host platform.""" + return self._config.host_platform() + + @host_platform.setter + def host_platform(self, value: str) -> None: + """Set the host platform.""" + self._config.set_host_platform(value) + + @property + def experimental(self) -> bool: + """Get whether experimental features are enabled.""" + return self._config.experimental() + + @experimental.setter + def experimental(self, value: bool) -> None: + """Set whether experimental features are enabled.""" + self._config.set_experimental(value) + + @property + def recipe_path(self) -> Optional[str]: + """Get the recipe path.""" + return self._config.recipe_path() + + @recipe_path.setter + def recipe_path(self, value: Optional[str]) -> None: + """Set the recipe path.""" + self._config.set_recipe_path(value) + + def __repr__(self) -> str: + return repr(self._config) + + +class RenderedVariant: + """Result of rendering a recipe with a specific variant combination. + + Each RenderedVariant represents one specific variant of a recipe after + all Jinja templates have been evaluated and variant values applied. + + Attributes: + variant: The variant combination used (variable name -> value) + recipe: The rendered Stage1 recipe + hash_info: Build string hash information + pin_subpackages: Pin subpackage dependencies + + Example: + >>> for variant in rendered_variants: + ... print(f"Package: {variant.recipe().package().name()}") + ... print(f"Variant: {variant.variant()}") + ... print(f"Build string: {variant.recipe().build().string()}") + """ + + def __init__(self, inner: _RenderedVariant): + """Create a RenderedVariant from the Rust object.""" + self._inner = inner + + def variant(self) -> Dict[str, str]: + """Get the variant combination used for this render. + + Returns: + Dictionary mapping variable names to their values + """ + return self._inner.variant() + + def recipe(self) -> Any: # Returns Stage1Recipe + """Get the rendered Stage1 recipe. + + Returns: + The fully evaluated Stage1 recipe ready for building + """ + return self._inner.recipe() + + def hash_info(self) -> Optional[HashInfo]: + """Get hash info if available. + + Returns: + HashInfo object with 'hash' and 'prefix' attributes, or None + + Example: + >>> rendered = render_recipe(recipe, variant_config)[0] + >>> hash_info = rendered.hash_info() + >>> if hash_info: + ... print(f"Hash: {hash_info.hash}") + ... print(f"Prefix: {hash_info.prefix}") + """ + inner = self._inner.hash_info() + return HashInfo(inner) if inner else None + + def pin_subpackages(self) -> Dict[str, PinSubpackageInfo]: + """Get pin_subpackage information. + + Returns: + Dictionary mapping package names to PinSubpackageInfo objects + + Example: + >>> rendered = render_recipe(recipe, variant_config)[0] + >>> for name, info in rendered.pin_subpackages().items(): + ... print(f"{name}: version={info.version}, exact={info.exact}") + """ + inner_dict = self._inner.pin_subpackages() + return {name: PinSubpackageInfo(info) for name, info in inner_dict.items()} + + def run_build( + self, + tool_config: Optional["ToolConfiguration"] = None, + output_dir: Optional[Union[str, Path]] = None, + channel: Optional[List[str]] = None, + progress_callback: Optional[Any] = None, + recipe_path: Optional[Union[str, Path]] = None, + **kwargs: Any, + ) -> None: + """Build this rendered variant. + + This method builds a single rendered variant directly without needing + to go back through the Stage0 recipe. + + Args: + tool_config: Optional ToolConfiguration to use for the build. + output_dir: Directory to store the built package. Defaults to current directory. + channel: List of channels to use for resolving dependencies. + progress_callback: Optional progress callback for build events (e.g., RichProgressCallback or SimpleProgressCallback). + recipe_path: Path to the recipe file (for copying license files, etc.). Defaults to None. + **kwargs: Additional arguments passed to build (e.g., keep_build, test, etc.) + + Example: + >>> from rattler_build.stage0 import Recipe + >>> from rattler_build.variant_config import VariantConfig + >>> from rattler_build.render import render_recipe + >>> + >>> recipe = Recipe.from_yaml(yaml_string) + >>> rendered = render_recipe(recipe, VariantConfig()) + >>> # Build just the first variant + >>> rendered[0].run_build(output_dir="./output") + """ + from . import rattler_build as _rb + + # Extract the inner ToolConfiguration if provided + tool_config_inner = tool_config._inner if tool_config else None + + # Build this single variant + _rb.build_from_rendered_variants_py( + rendered_variants=[self._inner], + tool_config=tool_config_inner, + output_dir=Path(output_dir) if output_dir else None, + channel=channel, + progress_callback=progress_callback, + recipe_path=Path(recipe_path) if recipe_path else None, + **kwargs, + ) + + def __repr__(self) -> str: + return repr(self._inner) + + +RecipeInput: TypeAlias = Union[str, SingleOutputRecipe, MultiOutputRecipe, Path] + + +def render_recipe( + recipe: Union[RecipeInput, List[RecipeInput]], + variant_config: Union["VariantConfig", Path, str], + render_config: Optional[RenderConfig] = None, +) -> List[RenderedVariant]: + """Render a Stage0 recipe with a variant configuration into Stage1 recipes. + + This function takes a parsed Stage0 recipe and evaluates all Jinja templates + with different variant combinations to produce ready-to-build Stage1 recipes. + + Args: + recipe: The Stage0 recipe to render (from stage0.Recipe.from_yaml()) + variant_config: The variant configuration (from variant_config.VariantConfig) + render_config: Optional render configuration (defaults to current platform) + + Returns: + List of RenderedVariant objects, one for each variant combination + + Example: + >>> from rattler_build.stage0 import Recipe + >>> from rattler_build.variant_config import VariantConfig + >>> from rattler_build.render import render_recipe, RenderConfig + >>> + >>> # Parse stage0 recipe + >>> recipe = Recipe.from_yaml(''' + ... package: + ... name: my-package + ... version: 1.0.0 + ... requirements: + ... host: + ... - python ${{ python }} + ... ''') + >>> + >>> # Create variant config + >>> variant_config = VariantConfig.from_yaml(''' + ... python: + ... - "3.9" + ... - "3.10" + ... - "3.11" + ... ''') + >>> + >>> # Render with all variants + >>> rendered = render_recipe(recipe, variant_config) + >>> print(f"Generated {len(rendered)} variants") + Generated 3 variants + """ + from rattler_build.stage0 import Recipe + from rattler_build.variant_config import VariantConfig as VC + + # Handle render_config parameter + config_inner = render_config._config if render_config else None + + # Handle recipe parameter - convert str/Path to Recipe objects + recipes_to_render: List[Union[SingleOutputRecipe, MultiOutputRecipe]] = [] + + if isinstance(recipe, list): + # Handle list of recipes + for r in recipe: + if isinstance(r, (str, Path)): + parsed = Recipe.from_file(r) + recipes_to_render.append(parsed) + elif isinstance(r, (SingleOutputRecipe, MultiOutputRecipe)): + recipes_to_render.append(r) + else: + raise TypeError(f"Unsupported recipe type in list: {type(r)}") + elif isinstance(recipe, (str, Path)): + # Parse single recipe from file/string + if isinstance(recipe, Path): + # Definitely a file path + parsed = Recipe.from_file(recipe) + elif recipe.endswith(".yaml") or recipe.endswith(".yml") or "/" in recipe or "\\" in recipe: + # String that looks like a file path + parsed = Recipe.from_file(recipe) + else: + # Treat as YAML string + parsed = Recipe.from_yaml(recipe) + recipes_to_render.append(parsed) + elif isinstance(recipe, (SingleOutputRecipe, MultiOutputRecipe)): + recipes_to_render.append(recipe) + else: + raise TypeError(f"Unsupported recipe type: {type(recipe)}") + + # Handle variant_config parameter - convert str/Path to VariantConfig + if isinstance(variant_config, (str, Path)): + if isinstance(variant_config, Path): + variant_config = VC.from_file(variant_config) + else: + # Check if it's a file path or YAML string + if ( + variant_config.endswith(".yaml") + or variant_config.endswith(".yml") + or "/" in variant_config + or "\\" in variant_config + ): + variant_config = VC.from_file(variant_config) + else: + variant_config = VC.from_yaml(variant_config) + elif not isinstance(variant_config, VC): + raise TypeError(f"Unsupported variant_config type: {type(variant_config)}") + + # Now unwrap to get inner Rust objects + variant_config_inner = variant_config._inner + + # Render all recipes and collect results + all_rendered: List[RenderedVariant] = [] + for recipe_obj in recipes_to_render: + recipe_inner = recipe_obj._wrapper + rendered = _render_recipe(recipe_inner, variant_config_inner, config_inner) + all_rendered.extend([RenderedVariant(r) for r in rendered]) + + return all_rendered + + +def build_rendered_variants( + rendered_variants: List[RenderedVariant], + tool_config: Optional["ToolConfiguration"] = None, + output_dir: Optional[Union[str, Path]] = None, + channel: Optional[List[str]] = None, + progress_callback: Optional[Any] = None, + recipe_path: Optional[Union[str, Path]] = None, + **kwargs: Any, +) -> None: + """Build multiple rendered variants. + + This is a convenience function for building multiple rendered variants + in one call, useful when you want to build all variants from a recipe. + + Args: + rendered_variants: List of RenderedVariant objects to build + tool_config: Optional ToolConfiguration to use for the build. + output_dir: Directory to store the built packages. Defaults to current directory. + channel: List of channels to use for resolving dependencies. + progress_callback: Optional progress callback for build events (e.g., RichProgressCallback or SimpleProgressCallback). + recipe_path: Path to the recipe file (for copying license files, etc.). Defaults to None. + **kwargs: Additional arguments passed to build (e.g., keep_build, test, etc.) + + Example: + >>> from rattler_build.stage0 import Recipe + >>> from rattler_build.variant_config import VariantConfig + >>> from rattler_build.render import render_recipe, build_rendered_variants + >>> + >>> # Parse and render recipe + >>> recipe = Recipe.from_yaml(yaml_string) + >>> variant_config = VariantConfig.from_yaml(''' + ... python: + ... - "3.9" + ... - "3.10" + ... - "3.11" + ... ''') + >>> rendered = render_recipe(recipe, variant_config) + >>> + >>> # Build all variants at once + >>> build_rendered_variants(rendered, output_dir="./output") + >>> + >>> # Or build a subset + >>> build_rendered_variants(rendered[:2], output_dir="./output") + """ + from . import rattler_build as _rb + + # Extract the inner ToolConfiguration if provided + tool_config_inner = tool_config._inner if tool_config else None + + # Build all variants + _rb.build_from_rendered_variants_py( + rendered_variants=[v._inner for v in rendered_variants], + tool_config=tool_config_inner, + output_dir=Path(output_dir) if output_dir else None, + channel=channel, + progress_callback=progress_callback, + recipe_path=Path(recipe_path) if recipe_path else None, + **kwargs, + ) + + +__all__ = [ + "RenderConfig", + "RenderedVariant", + "HashInfo", + "PinSubpackageInfo", + "render_recipe", + "build_rendered_variants", +] diff --git a/py-rattler-build/rattler_build/stage0.py b/py-rattler-build/rattler_build/stage0.py new file mode 100644 index 000000000..18235bfcb --- /dev/null +++ b/py-rattler-build/rattler_build/stage0.py @@ -0,0 +1,587 @@ +""" +Stage0 - Parsed recipe types before evaluation. + +This module provides Python bindings for rattler-build's stage0 types, +which represent the parsed YAML recipe before Jinja template evaluation +and conditional resolution. +""" + +from pathlib import Path +from typing import Any, Dict, List, Optional, Union, TYPE_CHECKING + +if TYPE_CHECKING: + from rattler_build.tool_config import ToolConfiguration + + # For type checking, use Any as placeholder since we don't have stubs + _Stage0Recipe = Any + _SingleOutputRecipe = Any + _MultiOutputRecipe = Any + _Stage0Package = Any + _Stage0PackageMetadata = Any + _Stage0RecipeMetadata = Any + _Stage0Build = Any + _Stage0Requirements = Any + _Stage0About = Any + _Stage0PackageOutput = Any + _Stage0StagingOutput = Any +else: + # At runtime, import the Rust submodule + from . import rattler_build as _rb + + # Get the stage0 submodule + _stage0 = _rb.stage0 + + # Import classes from the stage0 submodule + _Stage0Recipe = _stage0.Stage0Recipe + _SingleOutputRecipe = _stage0.SingleOutputRecipe + _MultiOutputRecipe = _stage0.MultiOutputRecipe + _Stage0Package = _stage0.Stage0Package + _Stage0PackageMetadata = _stage0.Stage0PackageMetadata + _Stage0RecipeMetadata = _stage0.Stage0RecipeMetadata + _Stage0Build = _stage0.Stage0Build + _Stage0Requirements = _stage0.Stage0Requirements + _Stage0About = _stage0.Stage0About + _Stage0PackageOutput = _stage0.Stage0PackageOutput + _Stage0StagingOutput = _stage0.Stage0StagingOutput + +__all__ = [ + "Recipe", + "SingleOutputRecipe", + "MultiOutputRecipe", + "Package", + "RecipeMetadata", + "Build", + "Requirements", + "About", + "PackageOutput", + "StagingOutput", +] + + +class Recipe: + """ + A parsed conda recipe (stage0). + + This can be either a single-output or multi-output recipe. + + Example: + >>> recipe = Recipe.from_yaml(yaml_string) + >>> if recipe.is_single_output(): + ... single = recipe.as_single_output() + ... print(single.package.name) + """ + + def __init__(self, inner: _Stage0Recipe): + self._inner = inner + + @classmethod + def from_yaml(cls, yaml: str) -> Union["SingleOutputRecipe", "MultiOutputRecipe"]: + """ + Parse a recipe from YAML string. + + Returns the appropriate type: SingleOutputRecipe or MultiOutputRecipe. + """ + wrapper = _Stage0Recipe.from_yaml(yaml) + if wrapper.is_single_output(): + single_inner = wrapper.as_single_output() + return SingleOutputRecipe(single_inner, wrapper) + else: + multi_inner = wrapper.as_multi_output() + return MultiOutputRecipe(multi_inner, wrapper) + + @classmethod + def from_file(cls, path: Union[str, Path]) -> Union["SingleOutputRecipe", "MultiOutputRecipe"]: + """ + Parse a recipe from a YAML file. + + Returns the appropriate type: SingleOutputRecipe or MultiOutputRecipe. + """ + with open(path, "r", encoding="utf-8") as f: + return cls.from_yaml(f.read()) + + @classmethod + def from_dict(cls, recipe_dict: Dict[str, Any]) -> Union["SingleOutputRecipe", "MultiOutputRecipe"]: + """ + Create a recipe from a Python dictionary. + + This method validates the dictionary structure and provides detailed error + messages if the structure is invalid or types don't match. + + Args: + recipe_dict: Dictionary containing recipe data (must match recipe schema) + + Returns: + SingleOutputRecipe or MultiOutputRecipe depending on the recipe type + + Raises: + PyRecipeParseError: If the dictionary structure is invalid or types don't match + + Example: + >>> recipe_dict = { + ... "package": { + ... "name": "my-package", + ... "version": "1.0.0" + ... }, + ... "build": { + ... "number": 0 + ... } + ... } + >>> recipe = Recipe.from_dict(recipe_dict) + """ + wrapper = _Stage0Recipe.from_dict(recipe_dict) + if wrapper.is_single_output(): + single_inner = wrapper.as_single_output() + return SingleOutputRecipe(single_inner, wrapper) + else: + multi_inner = wrapper.as_multi_output() + return MultiOutputRecipe(multi_inner, wrapper) + + def is_single_output(self) -> bool: + """Check if this is a single output recipe.""" + return self._inner.is_single_output() + + def is_multi_output(self) -> bool: + """Check if this is a multi output recipe.""" + return self._inner.is_multi_output() + + def as_single_output(self) -> Optional["SingleOutputRecipe"]: + """Get as a single output recipe (None if multi-output).""" + inner = self._inner.as_single_output() + return SingleOutputRecipe(inner) if inner else None + + def as_multi_output(self) -> Optional["MultiOutputRecipe"]: + """Get as a multi output recipe (None if single-output).""" + inner = self._inner.as_multi_output() + return MultiOutputRecipe(inner) if inner else None + + def to_dict(self) -> Dict[str, Any]: + """Convert to Python dictionary.""" + return self._inner.to_dict() + + def __repr__(self) -> str: + return repr(self._inner) + + +class SingleOutputRecipe: + """A single-output recipe at stage0 (parsed, not yet evaluated).""" + + def __init__(self, inner: _SingleOutputRecipe, wrapper: Any = None): + self._inner = inner + # Keep reference to the original Rust Stage0Recipe wrapper for render() + self._wrapper = wrapper + + @property + def schema_version(self) -> int: + """Get the schema version.""" + return self._inner.schema_version + + @property + def context(self) -> Dict[str, Any]: + """Get the context variables as a dictionary.""" + return self._inner.context + + @property + def package(self) -> "Package": + """Get the package metadata.""" + return Package(self._inner.package) + + @property + def build(self) -> "Build": + """Get the build configuration.""" + return Build(self._inner.build) + + @property + def requirements(self) -> "Requirements": + """Get the requirements.""" + return Requirements(self._inner.requirements) + + @property + def about(self) -> "About": + """Get the about metadata.""" + return About(self._inner.about) + + def render(self, variant_config: Any = None, render_config: Any = None) -> List[Any]: + """ + Render this recipe with variant configuration. + + This is a convenience method that calls render.render_recipe() internally. + Always returns a list of RenderedVariant objects. + + Args: + variant_config: Optional VariantConfig to use. If None, creates an empty config. + render_config: Optional RenderConfig to use. If None, uses default config. + + Returns: + List of RenderedVariant objects (one for each variant combination) + + Example: + >>> recipe = Recipe.from_yaml(yaml_string) + >>> variants = recipe.render(variant_config) + >>> for variant in variants: + ... print(variant.recipe().package.name) + """ + # Import here to avoid circular dependency + from . import render as render_module + from . import variant_config as vc_module + + # Create empty variant config if not provided + if variant_config is None: + variant_config = vc_module.VariantConfig() + + # Pass self (the Python wrapper) to render_recipe, not the raw Rust object + return render_module.render_recipe(self, variant_config, render_config) + + def run_build( + self, + variant_config: Any = None, + tool_config: Optional["ToolConfiguration"] = None, + output_dir: Union[str, Path, None] = None, + channel: Optional[List[str]] = None, + **kwargs: Any, + ) -> None: + """ + Build this recipe. + + This method renders the recipe with variants and then builds the rendered outputs + directly without writing temporary files. + + Args: + variant_config: Optional VariantConfig to use for building variants. + tool_config: Optional ToolConfiguration to use for the build. If provided, individual + parameters like keep_build, test, etc. will be ignored. + output_dir: Directory to store the built packages. Defaults to current directory. + channel: List of channels to use for resolving dependencies. + **kwargs: Additional arguments passed to build (e.g., keep_build, test, etc.) + These are ignored if tool_config is provided. + + Example: + >>> recipe = Recipe.from_yaml(yaml_string) + >>> recipe.run_build(output_dir="./output") + + >>> # Or with custom tool configuration + >>> from rattler_build import ToolConfiguration + >>> config = ToolConfiguration(keep_build=True, test_strategy="native") + >>> recipe.run_build(tool_config=config, output_dir="./output") + """ + from . import rattler_build as _rb + + # Render the recipe to get Stage1 variants + rendered_variants = self.render(variant_config) + + # Extract the inner ToolConfiguration if provided + tool_config_inner = tool_config._inner if tool_config else None + + # Build from the rendered variants + _rb.build_from_rendered_variants_py( + rendered_variants=[v._inner for v in rendered_variants], + tool_config=tool_config_inner, + output_dir=Path(output_dir) if output_dir else None, + channel=channel, + **kwargs, + ) + + def to_dict(self) -> Dict[str, Any]: + """Convert to Python dictionary.""" + return self._inner.to_dict() + + def __repr__(self) -> str: + return repr(self._inner) + + +class MultiOutputRecipe: + """A multi-output recipe at stage0 (parsed, not yet evaluated).""" + + def __init__(self, inner: _MultiOutputRecipe, wrapper: Any = None): + self._inner = inner + # Keep reference to the original Rust Stage0Recipe wrapper for render() + self._wrapper = wrapper + + @property + def schema_version(self) -> int: + """Get the schema version.""" + return self._inner.schema_version + + @property + def context(self) -> Dict[str, Any]: + """Get the context variables as a dictionary.""" + return self._inner.context + + @property + def recipe(self) -> "RecipeMetadata": + """Get the top-level recipe metadata.""" + return RecipeMetadata(self._inner.recipe) + + @property + def build(self) -> "Build": + """Get the top-level build configuration.""" + return Build(self._inner.build) + + @property + def about(self) -> "About": + """Get the top-level about metadata.""" + return About(self._inner.about) + + @property + def outputs(self) -> List[Union["PackageOutput", "StagingOutput"]]: + """Get all outputs (package and staging).""" + result: List[Union["PackageOutput", "StagingOutput"]] = [] + for output in self._inner.outputs: + if isinstance(output, _Stage0PackageOutput): # type: ignore[misc] + result.append(PackageOutput(output)) + elif isinstance(output, _Stage0StagingOutput): # type: ignore[misc] + result.append(StagingOutput(output)) + return result + + def render(self, variant_config: Any = None, render_config: Any = None) -> List[Any]: + """ + Render this recipe with variant configuration. + + This is a convenience method that calls render.render_recipe() internally. + Always returns a list of RenderedVariant objects. + + Args: + variant_config: Optional VariantConfig to use. If None, creates an empty config. + render_config: Optional RenderConfig to use. If None, uses default config. + + Returns: + List of RenderedVariant objects (one for each variant combination and output) + + Example: + >>> recipe = Recipe.from_yaml(yaml_string) + >>> variants = recipe.render(variant_config) + >>> for variant in variants: + ... print(variant.recipe().package.name) + """ + # Import here to avoid circular dependency + from . import render as render_module + from . import variant_config as vc_module + + # Create empty variant config if not provided + if variant_config is None: + variant_config = vc_module.VariantConfig() + + # Pass self (the Python wrapper) to render_recipe, not the raw Rust object + return render_module.render_recipe(self, variant_config, render_config) + + def run_build( + self, + variant_config: Any = None, + tool_config: Optional["ToolConfiguration"] = None, + output_dir: Union[str, Path, None] = None, + channel: Optional[List[str]] = None, + **kwargs: Any, + ) -> None: + """ + Build this multi-output recipe. + + This method renders the recipe with variants and then builds the rendered outputs + directly without writing temporary files. + + Args: + variant_config: Optional VariantConfig to use for building variants. + tool_config: Optional ToolConfiguration to use for the build. If provided, individual + parameters like keep_build, test, etc. will be ignored. + output_dir: Directory to store the built packages. Defaults to current directory. + channel: List of channels to use for resolving dependencies. + **kwargs: Additional arguments passed to build (e.g., keep_build, test, etc.) + These are ignored if tool_config is provided. + + Example: + >>> recipe = Recipe.from_yaml(yaml_string) + >>> recipe.run_build(output_dir="./output") + + >>> # Or with custom tool configuration + >>> from rattler_build import ToolConfiguration + >>> config = ToolConfiguration(keep_build=True, test_strategy="native") + >>> recipe.run_build(tool_config=config, output_dir="./output") + """ + from . import rattler_build as _rb + + # Render the recipe to get Stage1 variants + rendered_variants = self.render(variant_config) + + # Extract the inner ToolConfiguration if provided + tool_config_inner = tool_config._inner if tool_config else None + + # Build from the rendered variants + _rb.build_from_rendered_variants_py( + rendered_variants=[v._inner for v in rendered_variants], + tool_config=tool_config_inner, + output_dir=Path(output_dir) if output_dir else None, + channel=channel, + **kwargs, + ) + + def to_dict(self) -> Dict[str, Any]: + """Convert to Python dictionary.""" + return self._inner.to_dict() + + def __repr__(self) -> str: + return repr(self._inner) + + +class Package: + """Package metadata at stage0.""" + + def __init__(self, inner: _Stage0Package): + self._inner = inner + + @property + def name(self) -> Any: + """Get the package name (may be a template string like '${{ name }}').""" + return self._inner.name + + @property + def version(self) -> Any: + """Get the package version (may be a template string like '${{ version }}').""" + return self._inner.version + + def to_dict(self) -> Dict[str, Any]: + """Convert to Python dictionary.""" + return self._inner.to_dict() + + +class RecipeMetadata: + """Recipe metadata for multi-output recipes.""" + + def __init__(self, inner: _Stage0RecipeMetadata): + self._inner = inner + + def to_dict(self) -> Dict[str, Any]: + """Convert to Python dictionary.""" + return self._inner.to_dict() + + +class Build: + """Build configuration at stage0.""" + + def __init__(self, inner: _Stage0Build): + self._inner = inner + + @property + def number(self) -> Any: + """Get the build number (may be a template).""" + return self._inner.number + + @property + def string(self) -> Optional[Any]: + """Get the build string (may be a template or None for auto-generated).""" + return self._inner.string + + @property + def script(self) -> Any: + """Get the build script configuration.""" + return self._inner.script + + @property + def noarch(self) -> Optional[Any]: + """Get the noarch type (may be a template or None).""" + return self._inner.noarch + + def to_dict(self) -> Dict[str, Any]: + """Convert to Python dictionary.""" + return self._inner.to_dict() + + +class Requirements: + """Requirements at stage0.""" + + def __init__(self, inner: _Stage0Requirements): + self._inner = inner + + @property + def build(self) -> List[Any]: + """Get build-time requirements (list of matchspecs or templates).""" + return self._inner.build + + @property + def host(self) -> List[Any]: + """Get host-time requirements (list of matchspecs or templates).""" + return self._inner.host + + @property + def run(self) -> List[Any]: + """Get run-time requirements (list of matchspecs or templates).""" + return self._inner.run + + @property + def run_constraints(self) -> List[Any]: + """Get run-time constraints (list of matchspecs or templates).""" + return self._inner.run_constraints + + def to_dict(self) -> Dict[str, Any]: + """Convert to Python dictionary.""" + return self._inner.to_dict() + + +class About: + """About metadata at stage0.""" + + def __init__(self, inner: _Stage0About): + self._inner = inner + + @property + def homepage(self) -> Optional[Any]: + """Get the homepage URL (may be a template or None).""" + return self._inner.homepage + + @property + def license(self) -> Optional[Any]: + """Get the license (may be a template or None).""" + return self._inner.license + + @property + def license_family(self) -> Optional[Any]: + """Get the license family (deprecated, may be a template or None).""" + return self._inner.license_family + + @property + def summary(self) -> Optional[Any]: + """Get the summary (may be a template or None).""" + return self._inner.summary + + @property + def description(self) -> Optional[Any]: + """Get the description (may be a template or None).""" + return self._inner.description + + @property + def documentation(self) -> Optional[Any]: + """Get the documentation URL (may be a template or None).""" + return self._inner.documentation + + @property + def repository(self) -> Optional[Any]: + """Get the repository URL (may be a template or None).""" + return self._inner.repository + + def to_dict(self) -> Dict[str, Any]: + """Convert to Python dictionary.""" + return self._inner.to_dict() + + +class PackageOutput: + """A package output in a multi-output recipe.""" + + def __init__(self, inner: _Stage0PackageOutput): + self._inner = inner + + @property + def package(self) -> Package: + """Get the package metadata for this output.""" + return Package(self._inner.package) + + def to_dict(self) -> Dict[str, Any]: + """Convert to Python dictionary.""" + return self._inner.to_dict() + + +class StagingOutput: + """A staging output in a multi-output recipe.""" + + def __init__(self, inner: _Stage0StagingOutput): + self._inner = inner + + def to_dict(self) -> Dict[str, Any]: + """Convert to Python dictionary.""" + return self._inner.to_dict() diff --git a/py-rattler-build/rattler_build/stage1.py b/py-rattler-build/rattler_build/stage1.py new file mode 100644 index 000000000..9dd6c7aeb --- /dev/null +++ b/py-rattler-build/rattler_build/stage1.py @@ -0,0 +1,288 @@ +""" +Stage1 - Evaluated recipe types ready for building. + +This module provides Python bindings for rattler-build's stage1 types, +which represent the fully evaluated recipe with all Jinja templates resolved +and conditionals evaluated. +""" + +from typing import Any, Dict, List, Optional, TYPE_CHECKING + +if TYPE_CHECKING: + # For type checking, use Any as placeholder since we don't have stubs + _Stage1Recipe = Any + _Stage1Package = Any + _Stage1Build = Any + _Stage1Requirements = Any + _Stage1About = Any + _Stage1Source = Any + _Stage1StagingCache = Any +else: + # At runtime, import the Rust submodule + from . import rattler_build as _rb + + # Get the stage1 submodule + _stage1 = _rb.stage1 + + # Import classes from the stage1 submodule + _Stage1Recipe = _stage1.Stage1Recipe + _Stage1Package = _stage1.Stage1Package + _Stage1Build = _stage1.Stage1Build + _Stage1Requirements = _stage1.Stage1Requirements + _Stage1About = _stage1.Stage1About + _Stage1Source = _stage1.Stage1Source + _Stage1StagingCache = _stage1.Stage1StagingCache + +__all__ = [ + "Recipe", + "Package", + "Build", + "Requirements", + "About", + "Source", + "StagingCache", +] + + +class Recipe: + """ + A fully evaluated conda recipe (stage1), ready for building. + + This represents the recipe after all Jinja templates have been evaluated + and all conditionals resolved. + + Example: + >>> # After parsing and evaluating a stage0 recipe + >>> stage1_recipe = evaluate(stage0_recipe, context) + >>> print(stage1_recipe.package.name) + >>> print(stage1_recipe.package.version) + """ + + def __init__(self, inner: _Stage1Recipe): + self._inner = inner + + @property + def package(self) -> "Package": + """Get the package metadata.""" + return Package(self._inner.package) + + @property + def build(self) -> "Build": + """Get the build configuration.""" + return Build(self._inner.build) + + @property + def requirements(self) -> "Requirements": + """Get the requirements.""" + return Requirements(self._inner.requirements) + + @property + def about(self) -> "About": + """Get the about metadata.""" + return About(self._inner.about) + + @property + def context(self) -> Dict[str, Any]: + """Get the evaluation context.""" + return self._inner.context + + @property + def used_variant(self) -> Dict[str, Any]: + """Get the variant values used in this build.""" + return self._inner.used_variant + + @property + def sources(self) -> List["Source"]: + """Get all sources for this recipe.""" + return [Source(s) for s in self._inner.sources] + + @property + def staging_caches(self) -> List["StagingCache"]: + """Get all staging caches.""" + return [StagingCache(s) for s in self._inner.staging_caches] + + @property + def inherits_from(self) -> Optional[Dict[str, Any]]: + """Get inheritance information if this output inherits from a cache.""" + return self._inner.inherits_from + + def to_dict(self) -> Dict[str, Any]: + """Convert to Python dictionary.""" + return self._inner.to_dict() + + def __repr__(self) -> str: + return repr(self._inner) + + +class Package: + """Package metadata at stage1 (fully evaluated).""" + + def __init__(self, inner: _Stage1Package): + self._inner = inner + + @property + def name(self) -> str: + """Get the package name.""" + return self._inner.name + + @property + def version(self) -> str: + """Get the package version.""" + return self._inner.version + + def to_dict(self) -> Dict[str, Any]: + """Convert to Python dictionary.""" + return self._inner.to_dict() + + def __repr__(self) -> str: + return repr(self._inner) + + def __str__(self) -> str: + return f"{self.name}-{self.version}" + + +class Build: + """Build configuration at stage1 (fully evaluated).""" + + def __init__(self, inner: _Stage1Build): + self._inner = inner + + @property + def number(self) -> int: + """Get the build number.""" + return self._inner.number + + @property + def string(self) -> Any: + """Get the build string.""" + return self._inner.string + + @property + def script(self) -> Any: + """Get the build script.""" + return self._inner.script + + @property + def noarch(self) -> Optional[Any]: + """Get the noarch configuration if any.""" + return self._inner.noarch + + def to_dict(self) -> Dict[str, Any]: + """Convert to Python dictionary.""" + return self._inner.to_dict() + + def __repr__(self) -> str: + return repr(self._inner) + + +class Requirements: + """Requirements at stage1 (fully evaluated).""" + + def __init__(self, inner: _Stage1Requirements): + self._inner = inner + + @property + def build(self) -> List[Any]: + """Get build requirements.""" + return self._inner.build + + @property + def host(self) -> List[Any]: + """Get host requirements.""" + return self._inner.host + + @property + def run(self) -> List[Any]: + """Get run requirements.""" + return self._inner.run + + def to_dict(self) -> Dict[str, Any]: + """Convert to Python dictionary.""" + return self._inner.to_dict() + + def __repr__(self) -> str: + return repr(self._inner) + + +class About: + """About metadata at stage1 (fully evaluated).""" + + def __init__(self, inner: _Stage1About): + self._inner = inner + + @property + def homepage(self) -> Optional[str]: + """Get the homepage URL.""" + return self._inner.homepage + + @property + def repository(self) -> Optional[str]: + """Get the repository URL.""" + return self._inner.repository + + @property + def documentation(self) -> Optional[str]: + """Get the documentation URL.""" + return self._inner.documentation + + @property + def license(self) -> Optional[str]: + """Get the license string.""" + return self._inner.license + + @property + def summary(self) -> Optional[str]: + """Get the summary.""" + return self._inner.summary + + @property + def description(self) -> Optional[str]: + """Get the description.""" + return self._inner.description + + def to_dict(self) -> Dict[str, Any]: + """Convert to Python dictionary.""" + return self._inner.to_dict() + + def __repr__(self) -> str: + return repr(self._inner) + + +class Source: + """Source information at stage1 (fully evaluated).""" + + def __init__(self, inner: _Stage1Source): + self._inner = inner + + def to_dict(self) -> Dict[str, Any]: + """Convert to Python dictionary.""" + return self._inner.to_dict() + + +class StagingCache: + """Staging cache information at stage1 (fully evaluated).""" + + def __init__(self, inner: _Stage1StagingCache): + self._inner = inner + + @property + def name(self) -> str: + """Get the cache name.""" + return self._inner.name + + @property + def build(self) -> Build: + """Get the build configuration for this cache.""" + return Build(self._inner.build) + + @property + def requirements(self) -> Requirements: + """Get the requirements for this cache.""" + return Requirements(self._inner.requirements) + + def to_dict(self) -> Dict[str, Any]: + """Convert to Python dictionary.""" + return self._inner.to_dict() + + def __repr__(self) -> str: + return repr(self._inner) diff --git a/py-rattler-build/rattler_build/tool_config.py b/py-rattler-build/rattler_build/tool_config.py new file mode 100644 index 000000000..2ae79dede --- /dev/null +++ b/py-rattler-build/rattler_build/tool_config.py @@ -0,0 +1,216 @@ +""" +Tool configuration for rattler-build. + +This module provides a Pythonic API for configuring the build tool. +""" + +from typing import List, Optional, TYPE_CHECKING + +if TYPE_CHECKING: + # Type stub for the Rust ToolConfiguration + class _ToolConfiguration: + def __init__( + self, + keep_build: bool = ..., + compression_threads: Optional[int] = ..., + io_concurrency_limit: Optional[int] = ..., + test_strategy: Optional[str] = ..., + skip_existing: Optional[str] = ..., + continue_on_failure: bool = ..., + noarch_build_platform: Optional[str] = ..., + channel_priority: Optional[str] = ..., + allow_insecure_host: Optional[List[str]] = ..., + error_prefix_in_binary: bool = ..., + allow_symlinks_on_windows: bool = ..., + use_zstd: bool = ..., + use_bz2: bool = ..., + use_sharded: bool = ..., + use_jlap: bool = ..., + ) -> None: ... + @property + def keep_build(self) -> bool: ... + def set_keep_build(self, value: bool) -> None: ... + @property + def test_strategy(self) -> str: ... + @property + def skip_existing(self) -> str: ... + @property + def continue_on_failure(self) -> bool: ... + @property + def channel_priority(self) -> str: ... + @property + def use_zstd(self) -> bool: ... + @property + def use_bz2(self) -> bool: ... + @property + def use_sharded(self) -> bool: ... + @property + def use_jlap(self) -> bool: ... + @property + def compression_threads(self) -> Optional[int]: ... + @property + def io_concurrency_limit(self) -> Optional[int]: ... + @property + def allow_insecure_host(self) -> Optional[List[str]]: ... + @property + def error_prefix_in_binary(self) -> bool: ... + @property + def allow_symlinks_on_windows(self) -> bool: ... +else: + from . import rattler_build as _rb + + _ToolConfiguration = _rb.tool_config.ToolConfiguration + + +class ToolConfiguration: + """Configuration for the rattler-build tool. + + This class wraps the Rust ToolConfiguration and provides a Pythonic interface + for configuring build behavior. + + Args: + keep_build: Whether to keep the build directory after the build is done + compression_threads: Number of threads to use for compression (default: None - auto) + io_concurrency_limit: Maximum number of concurrent I/O operations (default: None) + test_strategy: Test strategy to use ("skip", "native", or "tests") (default: "skip") + skip_existing: Whether to skip packages that already exist ("none", "local", or "all") (default: "none") + continue_on_failure: Whether to continue building other recipes even if one fails (default: False) + noarch_build_platform: Platform to use for noarch builds (default: None) + channel_priority: Channel priority for solving ("strict" or "disabled") (default: "strict") + allow_insecure_host: List of hosts for which SSL certificate verification should be skipped + error_prefix_in_binary: Whether to error if the host prefix is detected in binary files (default: False) + allow_symlinks_on_windows: Whether to allow symlinks in packages on Windows (default: False) + use_zstd: Whether to use zstd compression when downloading repodata (default: True) + use_bz2: Whether to use bzip2 compression when downloading repodata (default: True) + use_sharded: Whether to use sharded repodata when downloading (default: True) + use_jlap: Whether to use JLAP when downloading repodata (default: False) + + Example: + >>> config = ToolConfiguration( + ... keep_build=True, + ... test_strategy="native", + ... compression_threads=4 + ... ) + >>> print(config.keep_build) + True + >>> print(config.test_strategy) + Native + """ + + def __init__( + self, + keep_build: bool = False, + compression_threads: Optional[int] = None, + io_concurrency_limit: Optional[int] = None, + test_strategy: Optional[str] = None, + skip_existing: Optional[str] = None, + continue_on_failure: bool = False, + noarch_build_platform: Optional[str] = None, + channel_priority: Optional[str] = None, + allow_insecure_host: Optional[List[str]] = None, + error_prefix_in_binary: bool = False, + allow_symlinks_on_windows: bool = False, + use_zstd: bool = True, + use_bz2: bool = True, + use_sharded: bool = True, + use_jlap: bool = False, + ): + """Create a new tool configuration.""" + self._inner = _ToolConfiguration( + keep_build=keep_build, + compression_threads=compression_threads, + io_concurrency_limit=io_concurrency_limit, + test_strategy=test_strategy, + skip_existing=skip_existing, + continue_on_failure=continue_on_failure, + noarch_build_platform=noarch_build_platform, + channel_priority=channel_priority, + allow_insecure_host=allow_insecure_host, + error_prefix_in_binary=error_prefix_in_binary, + allow_symlinks_on_windows=allow_symlinks_on_windows, + use_zstd=use_zstd, + use_bz2=use_bz2, + use_sharded=use_sharded, + use_jlap=use_jlap, + ) + + @property + def keep_build(self) -> bool: + """Whether to keep the build directory after the build is done.""" + return self._inner.keep_build + + @keep_build.setter + def keep_build(self, value: bool) -> None: + """Set whether to keep the build directory.""" + self._inner.set_keep_build(value) + + @property + def test_strategy(self) -> str: + """The test strategy to use.""" + return self._inner.test_strategy + + @property + def skip_existing(self) -> str: + """Whether to skip existing packages.""" + return self._inner.skip_existing + + @property + def continue_on_failure(self) -> bool: + """Whether to continue building on failure.""" + return self._inner.continue_on_failure + + @property + def channel_priority(self) -> str: + """The channel priority to use in solving.""" + return self._inner.channel_priority + + @property + def use_zstd(self) -> bool: + """Whether to use zstd compression.""" + return self._inner.use_zstd + + @property + def use_bz2(self) -> bool: + """Whether to use bzip2 compression.""" + return self._inner.use_bz2 + + @property + def use_sharded(self) -> bool: + """Whether to use sharded repodata.""" + return self._inner.use_sharded + + @property + def use_jlap(self) -> bool: + """Whether to use JLAP.""" + return self._inner.use_jlap + + @property + def compression_threads(self) -> Optional[int]: + """Number of compression threads.""" + return self._inner.compression_threads + + @property + def io_concurrency_limit(self) -> Optional[int]: + """IO concurrency limit.""" + return self._inner.io_concurrency_limit + + @property + def allow_insecure_host(self) -> Optional[List[str]]: + """List of hosts for which SSL certificate verification should be skipped.""" + return self._inner.allow_insecure_host + + @property + def error_prefix_in_binary(self) -> bool: + """Whether to error if the host prefix is detected in binary files.""" + return self._inner.error_prefix_in_binary + + @property + def allow_symlinks_on_windows(self) -> bool: + """Whether to allow symlinks in packages on Windows.""" + return self._inner.allow_symlinks_on_windows + + def __repr__(self) -> str: + return repr(self._inner) + + +__all__ = ["ToolConfiguration"] diff --git a/py-rattler-build/rattler_build/variant_config.py b/py-rattler-build/rattler_build/variant_config.py new file mode 100644 index 000000000..d896893d1 --- /dev/null +++ b/py-rattler-build/rattler_build/variant_config.py @@ -0,0 +1,491 @@ +""" +VariantConfig - Manage variant configuration for recipe builds. + +This module provides Python bindings for rattler-build's VariantConfig, +which manages variant matrices for building packages with different configurations. +""" + +from pathlib import Path +from typing import Any, Dict, ItemsView, Iterator, List, Optional, Union, ValuesView +from .rattler_build import VariantConfig as _VariantConfig, PyJinjaConfig +from .jinja_config import JinjaConfig + +__all__ = ["VariantConfig"] + + +class VariantConfig: + """ + Configuration for build variants. + + Variants allow building the same recipe with different configurations, + such as different Python versions, compilers, or other parameters. + + This class provides a dict-like interface for managing variants. + + Example: + >>> # Create from dict + >>> config = VariantConfig({ + ... "python": ["3.8", "3.9", "3.10"], + ... "numpy": ["1.21", "1.22"] + ... }) + >>> len(config.combinations()) # 3 * 2 = 6 combinations + 6 + + >>> # Dict-like access + >>> config["python"] = ["3.9", "3.10"] + >>> print(config["python"]) + ['3.9', '3.10'] + + >>> # Traditional method calls + >>> config.set_values("compiler", ["gcc", "clang"]) + >>> print(config.get_values("compiler")) + ['gcc', 'clang'] + + >>> # Load from YAML file + >>> config = VariantConfig.from_file("variant_config.yaml") + >>> print(config.keys()) + """ + + def __init__(self, variants: Optional[Union[Dict[str, List[Any]], _VariantConfig]] = None): + """ + Create a new VariantConfig. + + Args: + variants: Either a dictionary mapping variant keys to value lists, + or an existing _VariantConfig instance. If None, creates empty config. + + Example: + >>> # Create from dict + >>> config = VariantConfig({"python": ["3.9", "3.10"]}) + + >>> # Create empty + >>> config = VariantConfig() + """ + if variants is None: + self._inner = _VariantConfig() + elif isinstance(variants, dict): + self._inner = _VariantConfig() + for key, values in variants.items(): + self._inner.set_values(key, values) + else: + self._inner = variants + + @classmethod + def from_file(cls, path: Union[str, Path]) -> "VariantConfig": + """ + Load VariantConfig from a YAML file (variants.yaml format). + + Args: + path: Path to the variant configuration YAML file + + Returns: + A new VariantConfig instance + + Example: + >>> config = VariantConfig.from_file("variants.yaml") + """ + return cls(_VariantConfig.from_file(Path(path))) + + @classmethod + def from_file_with_context( + cls, path: Union[str, Path], jinja_config: Union[PyJinjaConfig, JinjaConfig] + ) -> "VariantConfig": + """ + Load VariantConfig from a YAML file with a JinjaConfig context (variants.yaml format). + + This allows evaluation of conditionals and templates in the variant file. + The jinja_config provides platform information and other context needed for evaluation. + + Args: + path: Path to the variant configuration YAML file + jinja_config: JinjaConfig providing context for evaluation + + Returns: + A new VariantConfig instance + + Example: + >>> from rattler_build import JinjaConfig + >>> jinja_config = JinjaConfig(target_platform="linux-64") + >>> config = VariantConfig.from_file_with_context("variants.yaml", jinja_config) + """ + # Convert JinjaConfig to PyJinjaConfig if needed + py_config = jinja_config._config if isinstance(jinja_config, JinjaConfig) else jinja_config + return cls(_VariantConfig.from_file_with_context(Path(path), py_config)) + + @classmethod + def from_conda_build_config( + cls, path: Union[str, Path], jinja_config: Union[PyJinjaConfig, JinjaConfig] + ) -> "VariantConfig": + """ + Load VariantConfig from a conda_build_config.yaml file. + + This supports the legacy conda-build format with `# [selector]` syntax. + Selectors are evaluated using the provided JinjaConfig. + + Args: + path: Path to the conda_build_config.yaml file + jinja_config: JinjaConfig providing context for selector evaluation + + Returns: + A new VariantConfig instance + + Example: + >>> from rattler_build import JinjaConfig + >>> jinja_config = JinjaConfig(target_platform="linux-64") + >>> config = VariantConfig.from_conda_build_config("conda_build_config.yaml", jinja_config) + """ + # Convert JinjaConfig to PyJinjaConfig if needed + py_config = jinja_config._config if isinstance(jinja_config, JinjaConfig) else jinja_config + return cls(_VariantConfig.from_conda_build_config(Path(path), py_config)) + + @classmethod + def from_yaml(cls, yaml: str) -> "VariantConfig": + """ + Load VariantConfig from a YAML string (variants.yaml format). + + Args: + yaml: YAML string containing variant configuration + + Returns: + A new VariantConfig instance + + Example: + >>> yaml_str = ''' + ... python: + ... - "3.8" + ... - "3.9" + ... ''' + >>> config = VariantConfig.from_yaml(yaml_str) + """ + return cls(_VariantConfig.from_yaml(yaml)) + + @classmethod + def from_yaml_with_context(cls, yaml: str, jinja_config: Union[PyJinjaConfig, JinjaConfig]) -> "VariantConfig": + """ + Load VariantConfig from a YAML string with a JinjaConfig context (variants.yaml format). + + This allows evaluation of conditionals and templates in the variant YAML. + The jinja_config provides platform information and other context needed for evaluation. + + Args: + yaml: YAML string containing variant configuration + jinja_config: JinjaConfig providing context for evaluation + + Returns: + A new VariantConfig instance + + Example: + >>> from rattler_build import JinjaConfig + >>> yaml_str = ''' + ... c_compiler: + ... - if: unix + ... then: gcc + ... - if: win + ... then: msvc + ... ''' + >>> jinja_config = JinjaConfig(target_platform="linux-64") + >>> config = VariantConfig.from_yaml_with_context(yaml_str, jinja_config) + """ + # Convert JinjaConfig to PyJinjaConfig if needed + py_config = jinja_config._config if isinstance(jinja_config, JinjaConfig) else jinja_config + return cls(_VariantConfig.from_yaml_with_context(yaml, py_config)) + + def keys(self) -> List[str]: + """ + Get all variant keys. + + Returns: + List of variant key names + + Example: + >>> config = VariantConfig() + >>> config.set_values("python", ["3.8", "3.9"]) + >>> config.set_values("numpy", ["1.21"]) + >>> config.keys() + ['numpy', 'python'] + """ + return self._inner.keys() + + @property + def zip_keys(self) -> Optional[List[List[str]]]: + """ + Get zip_keys - groups of keys that should be zipped together. + + Zip keys ensure that certain variant keys are synchronized when creating + combinations. For example, if python and numpy are zipped, then + python=3.9 will always be paired with numpy=1.20, not with other numpy versions. + + Returns: + List of groups (each group is a list of keys), or None if no zip keys are defined + + Example: + >>> config = VariantConfig() + >>> config.set_values("python", ["3.9", "3.10"]) + >>> config.set_values("numpy", ["1.20", "1.21"]) + >>> config.zip_keys = [["python", "numpy"]] + >>> len(config.combinations()) # 2, not 4 + 2 + """ + return self._inner.zip_keys + + @zip_keys.setter + def zip_keys(self, value: Optional[List[List[str]]]) -> None: + """ + Set zip_keys - groups of keys that should be zipped together. + + Args: + value: List of groups (each group is a list of keys), or None to clear + + Example: + >>> config = VariantConfig() + >>> config.zip_keys = [["python", "numpy"], ["c_compiler", "cxx_compiler"]] + """ + self._inner.zip_keys = value + + def get_values(self, key: str) -> Optional[List[Any]]: + """ + Get values for a specific variant key. + + Args: + key: The variant key name + + Returns: + List of values for the key, or None if key doesn't exist + + Example: + >>> config = VariantConfig() + >>> config.set_values("python", ["3.8", "3.9", "3.10"]) + >>> config.get_values("python") + ['3.8', '3.9', '3.10'] + """ + return self._inner.get_values(key) + + def set_values(self, key: str, values: List[Any]) -> None: + """ + Set values for a variant key. + + Args: + key: The variant key name + values: List of values for this key + + Example: + >>> config = VariantConfig() + >>> config.set_values("python", ["3.8", "3.9", "3.10"]) + >>> config.set_values("numpy", ["1.21", "1.22"]) + """ + self._inner.set_values(key, values) + + def to_dict(self) -> Dict[str, List[Any]]: + """ + Get all variants as a dictionary. + + Returns: + Dictionary mapping variant keys to their value lists + + Example: + >>> config = VariantConfig() + >>> config.set_values("python", ["3.8", "3.9"]) + >>> config.to_dict() + {'python': ['3.8', '3.9']} + """ + return self._inner.to_dict() + + def merge(self, other: "VariantConfig") -> None: + """ + Merge another VariantConfig into this one. + + Args: + other: Another VariantConfig to merge + + Example: + >>> config1 = VariantConfig() + >>> config1.set_values("python", ["3.8", "3.9"]) + >>> config2 = VariantConfig() + >>> config2.set_values("numpy", ["1.21"]) + >>> config1.merge(config2) + >>> config1.keys() + ['numpy', 'python'] + """ + self._inner.merge(other._inner) + + def combinations(self) -> List[Dict[str, Any]]: + """ + Generate all combinations of variant values. + + Returns: + List of dictionaries, each representing one variant combination + + Example: + >>> config = VariantConfig() + >>> config.set_values("python", ["3.8", "3.9"]) + >>> config.set_values("numpy", ["1.21", "1.22"]) + >>> combos = config.combinations() + >>> len(combos) + 4 + >>> combos[0] + {'python': '3.8', 'numpy': '1.21'} + """ + return self._inner.combinations() + + def __len__(self) -> int: + """Get the number of variant keys.""" + return len(self._inner) + + def __getitem__(self, key: str) -> List[Any]: + """ + Get values for a variant key using dict-like access. + + Args: + key: The variant key name + + Returns: + List of values for the key + + Raises: + KeyError: If the key doesn't exist + + Example: + >>> config = VariantConfig({"python": ["3.9", "3.10"]}) + >>> config["python"] + ['3.9', '3.10'] + """ + values = self._inner.get_values(key) + if values is None: + raise KeyError(f"Variant key '{key}' not found") + return values + + def __setitem__(self, key: str, values: List[Any]) -> None: + """ + Set values for a variant key using dict-like access. + + Args: + key: The variant key name + values: List of values for this key + + Example: + >>> config = VariantConfig() + >>> config["python"] = ["3.9", "3.10"] + >>> config["numpy"] = ["1.21", "1.22"] + """ + self._inner.set_values(key, values) + + def __contains__(self, key: str) -> bool: + """ + Check if a variant key exists. + + Args: + key: The variant key name + + Returns: + True if the key exists, False otherwise + + Example: + >>> config = VariantConfig({"python": ["3.9"]}) + >>> "python" in config + True + >>> "ruby" in config + False + """ + return self._inner.get_values(key) is not None + + def __delitem__(self, key: str) -> None: + """ + Delete a variant key (not implemented - raises NotImplementedError). + + Args: + key: The variant key name + + Raises: + NotImplementedError: Deletion is not supported + """ + raise NotImplementedError("Deletion of variant keys is not supported") + + def __iter__(self) -> Iterator[str]: + """ + Iterate over variant keys. + + Returns: + Iterator over variant key names + + Example: + >>> config = VariantConfig({"python": ["3.9"], "numpy": ["1.21"]}) + >>> list(config) + ['numpy', 'python'] + """ + return iter(self.keys()) + + def items(self) -> ItemsView[str, List[str]]: + """ + Get all variant key-value pairs. + + Returns: + Iterator of (key, values) tuples + + Example: + >>> config = VariantConfig({"python": ["3.9", "3.10"]}) + >>> dict(config.items()) + {'python': ['3.9', '3.10']} + """ + return self.to_dict().items() + + def values(self) -> ValuesView[List[str]]: + """ + Get all variant value lists. + + Returns: + Iterator of value lists + + Example: + >>> config = VariantConfig({"python": ["3.9", "3.10"]}) + >>> list(config.values()) + [['3.9', '3.10']] + """ + return self.to_dict().values() + + def get(self, key: str, default: Optional[List[Any]] = None) -> Optional[List[Any]]: + """ + Get values for a variant key with a default. + + Args: + key: The variant key name + default: Default value if key doesn't exist + + Returns: + List of values for the key, or default if key doesn't exist + + Example: + >>> config = VariantConfig({"python": ["3.9"]}) + >>> config.get("python") + ['3.9'] + >>> config.get("ruby", ["2.7"]) + ['2.7'] + """ + values = self._inner.get_values(key) + return values if values is not None else default + + def update(self, other: Union["VariantConfig", Dict[str, List[Any]]]) -> None: + """ + Update this config with values from another config or dict. + + Args: + other: Another VariantConfig or dict to merge + + Example: + >>> config = VariantConfig({"python": ["3.9"]}) + >>> config.update({"numpy": ["1.21"]}) + >>> config.keys() + ['numpy', 'python'] + """ + if isinstance(other, VariantConfig): + self.merge(other) + elif isinstance(other, dict): + for key, values in other.items(): + self.set_values(key, values) + else: + raise TypeError(f"Expected VariantConfig or dict, got {type(other)}") + + def __repr__(self) -> str: + return repr(self._inner) + + def __str__(self) -> str: + return f"VariantConfig(keys={self.keys()})" diff --git a/py-rattler-build/src/build_types.rs b/py-rattler-build/src/build_types.rs new file mode 100644 index 000000000..0e05d8a45 --- /dev/null +++ b/py-rattler-build/src/build_types.rs @@ -0,0 +1,152 @@ +use pyo3::prelude::*; +use rattler_build::types::{Directories, PackagingSettings}; +use rattler_conda_types::package::ArchiveType; +use std::path::PathBuf; + +use crate::error::RattlerBuildError; + +/// Python wrapper for Directories +#[pyclass(name = "Directories")] +#[derive(Clone)] +pub struct PyDirectories { + pub(crate) inner: Directories, +} + +#[pymethods] +impl PyDirectories { + /// Get the recipe directory + #[getter] + fn recipe_dir(&self) -> PathBuf { + self.inner.recipe_dir.clone() + } + + /// Get the work directory + #[getter] + fn work_dir(&self) -> PathBuf { + self.inner.work_dir.clone() + } + + /// Get the host prefix directory + #[getter] + fn host_prefix(&self) -> PathBuf { + self.inner.host_prefix.clone() + } + + /// Get the build prefix directory + #[getter] + fn build_prefix(&self) -> PathBuf { + self.inner.build_prefix.clone() + } + + /// Get the output directory + #[getter] + fn output_dir(&self) -> PathBuf { + self.inner.output_dir.clone() + } + + fn __repr__(&self) -> String { + format!( + "Directories(recipe_dir='{}', work_dir='{}', host_prefix='{}', build_prefix='{}', output_dir='{}')", + self.inner.recipe_dir.display(), + self.inner.work_dir.display(), + self.inner.host_prefix.display(), + self.inner.build_prefix.display(), + self.inner.output_dir.display() + ) + } +} + +/// Python wrapper for PackagingSettings +#[pyclass(name = "PackagingSettings")] +#[derive(Clone)] +pub struct PyPackagingSettings { + pub(crate) inner: PackagingSettings, +} + +#[pymethods] +impl PyPackagingSettings { + /// Create a new packaging settings + #[new] + #[pyo3(signature = (archive_type="conda", compression_level=None))] + fn new(archive_type: &str, compression_level: Option) -> PyResult { + let archive_type = match archive_type.to_lowercase().as_str() { + "conda" => ArchiveType::Conda, + "tar-bz2" | "tar.bz2" | "tarbz2" => ArchiveType::TarBz2, + _ => { + return Err(RattlerBuildError::Other(format!( + "Invalid archive type: {}. Must be 'conda' or 'tar-bz2'", + archive_type + )) + .into()); + } + }; + + // Default compression levels + let compression_level = compression_level.unwrap_or(match archive_type { + ArchiveType::Conda => 22, // zstd default + ArchiveType::TarBz2 => 9, // bzip2 default + }); + + Ok(Self { + inner: PackagingSettings { + archive_type, + compression_level, + }, + }) + } + + /// Get the archive type as a string + #[getter] + fn archive_type(&self) -> String { + match self.inner.archive_type { + ArchiveType::Conda => "conda".to_string(), + ArchiveType::TarBz2 => "tar-bz2".to_string(), + } + } + + /// Get the compression level + #[getter] + fn compression_level(&self) -> i32 { + self.inner.compression_level + } + + /// Set the archive type + #[setter] + fn set_archive_type(&mut self, archive_type: &str) -> PyResult<()> { + self.inner.archive_type = match archive_type.to_lowercase().as_str() { + "conda" => ArchiveType::Conda, + "tar-bz2" | "tar.bz2" | "tarbz2" => ArchiveType::TarBz2, + _ => { + return Err(RattlerBuildError::Other(format!( + "Invalid archive type: {}. Must be 'conda' or 'tar-bz2'", + archive_type + )) + .into()); + } + }; + Ok(()) + } + + /// Set the compression level + #[setter] + fn set_compression_level(&mut self, level: i32) { + self.inner.compression_level = level; + } + + fn __repr__(&self) -> String { + format!( + "PackagingSettings(archive_type='{}', compression_level={})", + self.archive_type(), + self.inner.compression_level + ) + } +} + +/// Register the build_types module with Python +pub fn register_build_types_module(py: Python<'_>, parent: &Bound<'_, PyModule>) -> PyResult<()> { + let m = PyModule::new(py, "build_types")?; + m.add_class::()?; + m.add_class::()?; + parent.add_submodule(&m)?; + Ok(()) +} diff --git a/py-rattler-build/src/jinja_config.rs b/py-rattler-build/src/jinja_config.rs new file mode 100644 index 000000000..203a2afb5 --- /dev/null +++ b/py-rattler-build/src/jinja_config.rs @@ -0,0 +1,265 @@ +use serde_json::Value as JsonValue; +use std::{ + collections::{BTreeMap, HashMap}, + path::PathBuf, + str::FromStr, +}; + +use crate::error::RattlerBuildError; +use pyo3::prelude::*; + +use rattler_build_jinja::{JinjaConfig, NormalizedKey, UndefinedBehavior, Variable}; +use rattler_conda_types::Platform; + +/// Python wrapper for JinjaConfig +#[pyclass(name = "PyJinjaConfig")] +#[derive(Clone)] +pub struct PyJinjaConfig { + pub(crate) inner: JinjaConfig, +} + +#[pymethods] +impl PyJinjaConfig { + #[new] + #[pyo3(signature = (target_platform=None, host_platform=None, build_platform=None, variant=None, experimental=None, allow_undefined=None, recipe_path=None))] + #[allow(clippy::too_many_arguments)] + fn new( + py: Python<'_>, + target_platform: Option, + host_platform: Option, + build_platform: Option, + variant: Option>>, + experimental: Option, + allow_undefined: Option, + recipe_path: Option, + ) -> PyResult { + let target_platform = target_platform + .map(|p| Platform::from_str(&p)) + .transpose() + .map_err(RattlerBuildError::from)? + .unwrap_or_else(Platform::current); + + let host_platform = host_platform + .map(|p| Platform::from_str(&p)) + .transpose() + .map_err(RattlerBuildError::from)? + .unwrap_or_else(Platform::current); + + let build_platform = build_platform + .map(|p| Platform::from_str(&p)) + .transpose() + .map_err(RattlerBuildError::from)? + .unwrap_or_else(Platform::current); + + // Convert variant from Python dict to BTreeMap + let variant_map = if let Some(variant_dict) = variant { + let mut map = BTreeMap::new(); + for (key, value) in variant_dict { + let normalized_key = NormalizedKey::from(key); + // Convert Python object to JSON Value then to Variable + let json_val: serde_json::Value = + pythonize::depythonize(value.bind(py)).map_err(|e| { + RattlerBuildError::Variant(format!( + "Failed to convert variant value: {}", + e + )) + })?; + let variable = match &json_val { + JsonValue::String(s) => Variable::from_string(s), + JsonValue::Bool(b) => Variable::from(*b), + JsonValue::Number(n) => { + if let Some(i) = n.as_i64() { + Variable::from(i) + } else { + Variable::from_string(&n.to_string()) + } + } + + JsonValue::Array(arr) => { + let vars: Result, RattlerBuildError> = arr + .iter() + .map(|v| match v { + JsonValue::String(s) => Ok(Variable::from_string(s)), + JsonValue::Bool(b) => Ok(Variable::from(*b)), + JsonValue::Number(n) => Ok(if let Some(i) = n.as_i64() { + Variable::from(i) + } else { + Variable::from_string(&n.to_string()) + }), + _ => Err(RattlerBuildError::Variant( + "Complex array elements not supported".to_string(), + )), + }) + .collect(); + Variable::from(vars?) + } + _ => { + return Err(RattlerBuildError::Variant( + "Object and null variants not supported".to_string(), + ) + .into()); + } + }; + map.insert(normalized_key, variable); + } + Ok::, RattlerBuildError>(map)? + } else { + BTreeMap::new() + }; + + // Convert allow_undefined to undefined_behavior + let undefined_behavior = if allow_undefined.unwrap_or(false) { + UndefinedBehavior::Lenient + } else { + UndefinedBehavior::SemiStrict + }; + + let jinja_config = JinjaConfig { + target_platform, + host_platform, + build_platform, + variant: variant_map, + experimental: experimental.unwrap_or(false), + recipe_path, + undefined_behavior, + }; + + Ok(PyJinjaConfig { + inner: jinja_config, + }) + } + + #[getter] + fn target_platform(&self) -> String { + self.inner.target_platform.to_string() + } + + #[setter] + fn set_target_platform(&mut self, value: String) -> PyResult<()> { + let platform = Platform::from_str(&value).map_err(RattlerBuildError::from)?; + self.inner.target_platform = platform; + Ok(()) + } + + #[getter] + fn host_platform(&self) -> String { + self.inner.host_platform.to_string() + } + + #[setter] + fn set_host_platform(&mut self, value: String) -> PyResult<()> { + let platform = Platform::from_str(&value).map_err(RattlerBuildError::from)?; + self.inner.host_platform = platform; + Ok(()) + } + + #[getter] + fn build_platform(&self) -> String { + self.inner.build_platform.to_string() + } + + #[setter] + fn set_build_platform(&mut self, value: String) -> PyResult<()> { + let platform = Platform::from_str(&value).map_err(RattlerBuildError::from)?; + self.inner.build_platform = platform; + Ok(()) + } + + #[getter] + fn experimental(&self) -> bool { + self.inner.experimental + } + + #[setter] + fn set_experimental(&mut self, value: bool) { + self.inner.experimental = value; + } + + #[getter] + fn allow_undefined(&self) -> bool { + matches!(self.inner.undefined_behavior, UndefinedBehavior::Lenient) + } + + #[setter] + fn set_allow_undefined(&mut self, value: bool) { + self.inner.undefined_behavior = if value { + UndefinedBehavior::Lenient + } else { + UndefinedBehavior::SemiStrict + }; + } + + #[getter] + fn recipe_path(&self) -> Option { + self.inner.recipe_path.clone() + } + + #[setter] + fn set_recipe_path(&mut self, value: Option) { + self.inner.recipe_path = value; + } + + #[getter] + fn variant(&self, py: Python<'_>) -> PyResult> { + let mut dict = HashMap::new(); + for (key, value) in &self.inner.variant { + let json_value = serde_json::to_value(value).map_err(RattlerBuildError::from)?; + dict.insert(key.normalize(), json_value); + } + Ok(pythonize::pythonize(py, &dict) + .map(|obj| obj.into()) + .map_err(|e| { + RattlerBuildError::Variant(format!("Failed to convert variant to Python: {}", e)) + })?) + } + + #[setter] + fn set_variant(&mut self, py: Python<'_>, value: HashMap>) -> PyResult<()> { + let mut map = BTreeMap::new(); + for (key, py_value) in value { + let normalized_key = NormalizedKey::from(key); + let json_val: serde_json::Value = + pythonize::depythonize(py_value.bind(py)).map_err(|e| { + RattlerBuildError::Variant(format!("Failed to convert variant value: {}", e)) + })?; + let variable = match &json_val { + JsonValue::String(s) => Variable::from_string(s), + JsonValue::Bool(b) => Variable::from(*b), + JsonValue::Number(n) => { + if let Some(i) = n.as_i64() { + Variable::from(i) + } else { + Variable::from_string(&n.to_string()) + } + } + JsonValue::Array(arr) => { + let vars: Result, RattlerBuildError> = arr + .iter() + .map(|v| match v { + JsonValue::String(s) => Ok(Variable::from_string(s)), + JsonValue::Bool(b) => Ok(Variable::from(*b)), + JsonValue::Number(n) => Ok(if let Some(i) = n.as_i64() { + Variable::from(i) + } else { + Variable::from_string(&n.to_string()) + }), + _ => Err(RattlerBuildError::Variant( + "Complex array elements not supported".to_string(), + )), + }) + .collect(); + Variable::from(vars?) + } + _ => { + return Err(RattlerBuildError::Variant( + "Object and null variants not supported".to_string(), + ) + .into()); + } + }; + map.insert(normalized_key, variable); + } + self.inner.variant = map; + Ok(()) + } +} diff --git a/py-rattler-build/src/lib.rs b/py-rattler-build/src/lib.rs index 52b64e2f5..5206e0491 100644 --- a/py-rattler-build/src/lib.rs +++ b/py-rattler-build/src/lib.rs @@ -1,37 +1,33 @@ -use serde_json::Value as JsonValue; -use std::{ - collections::{BTreeMap, HashMap}, - future::Future, - path::PathBuf, - str::FromStr, -}; +use std::{collections::HashMap, future::Future, path::PathBuf, str::FromStr}; use ::rattler_build::{ - NormalizedKey, Variable, build_recipes, get_rattler_build_version, - hash::HashInfo, + build_recipes, get_rattler_build_version, metadata::Debug, opt::{BuildData, ChannelPriorityWrapper, CommonData, TestData}, - recipe::parser::Recipe, - recipe_generator::{ - CpanOpts, PyPIOpts, generate_cpan_recipe_string, generate_luarocks_recipe_string, - generate_pypi_recipe_string, generate_r_recipe_string, - }, run_test, - selectors::SelectorConfig, - tool_configuration::{self, ContinueOnFailure, SkipExisting, TestStrategy}, + tool_configuration::{ContinueOnFailure, SkipExisting, TestStrategy}, }; use clap::ValueEnum; use pyo3::prelude::*; use rattler_conda_types::{NamedChannelOrUrl, Platform}; use rattler_config::config::{ConfigBase, build::PackageFormatAndCompression}; -use rattler_upload::upload; -use rattler_upload::upload::opt::{ - AnacondaData, ArtifactoryData, CondaForgeData, PrefixData, QuetzData, -}; -use url::Url; +mod build_types; mod error; +mod jinja_config; +mod platform_types; +mod progress_callback; +mod recipe_generation; +mod render; +mod stage0; +mod stage1; +mod tool_config; +mod tracing_subscriber; +mod upload; +mod variant_config; + use error::RattlerBuildError; +use jinja_config::PyJinjaConfig; /// Execute async tasks in Python bindings with proper error handling fn run_async_task(future: F) -> PyResult @@ -44,318 +40,16 @@ where Ok(rt.block_on(async { future.await.map_err(RattlerBuildError::from) })?) } -/// Python wrapper for SelectorConfig -#[pyclass] -#[derive(Clone)] -pub struct PySelectorConfig { - pub(crate) inner: SelectorConfig, -} - -#[pymethods] -impl PySelectorConfig { - #[new] - #[pyo3(signature = (target_platform=None, host_platform=None, build_platform=None, variant=None, experimental=None, allow_undefined=None, recipe_path=None, hash=None))] - #[allow(clippy::too_many_arguments)] - fn new( - py: Python<'_>, - target_platform: Option, - host_platform: Option, - build_platform: Option, - variant: Option>>, - experimental: Option, - allow_undefined: Option, - recipe_path: Option, - hash: Option, - ) -> PyResult { - let target_platform = target_platform - .map(|p| Platform::from_str(&p)) - .transpose() - .map_err(RattlerBuildError::from)? - .unwrap_or_else(Platform::current); - - let host_platform = host_platform - .map(|p| Platform::from_str(&p)) - .transpose() - .map_err(RattlerBuildError::from)? - .unwrap_or_else(Platform::current); - - let build_platform = build_platform - .map(|p| Platform::from_str(&p)) - .transpose() - .map_err(RattlerBuildError::from)? - .unwrap_or_else(Platform::current); - - // Convert variant from Python dict to BTreeMap - let variant_map = if let Some(variant_dict) = variant { - let mut map = BTreeMap::new(); - for (key, value) in variant_dict { - let normalized_key = NormalizedKey::from(key); - // Convert Python object to JSON Value then to Variable - let json_val: serde_json::Value = - pythonize::depythonize(value.bind(py)).map_err(|e| { - RattlerBuildError::Variant(format!( - "Failed to convert variant value: {}", - e - )) - })?; - let variable = match &json_val { - JsonValue::String(s) => Variable::from_string(s), - JsonValue::Bool(b) => Variable::from(*b), - JsonValue::Number(n) => { - if let Some(i) = n.as_i64() { - Variable::from(i) - } else { - Variable::from_string(&n.to_string()) - } - } - JsonValue::Array(arr) => { - let vars: Result, RattlerBuildError> = arr - .iter() - .map(|v| match v { - JsonValue::String(s) => Ok(Variable::from_string(s)), - JsonValue::Bool(b) => Ok(Variable::from(*b)), - JsonValue::Number(n) => Ok(if let Some(i) = n.as_i64() { - Variable::from(i) - } else { - Variable::from_string(&n.to_string()) - }), - _ => Err(RattlerBuildError::Variant( - "Complex array elements not supported".to_string(), - )), - }) - .collect(); - Variable::from(vars?) - } - _ => { - return Err(RattlerBuildError::Variant( - "Object and null variants not supported".to_string(), - ) - .into()); - } - }; - map.insert(normalized_key, variable); - } - Ok::, RattlerBuildError>(map)? - } else { - BTreeMap::new() - }; - - let selector_config = SelectorConfig { - target_platform, - host_platform, - build_platform, - hash: hash.map(|h| HashInfo { - hash: h, - prefix: String::new(), - }), - variant: variant_map, - experimental: experimental.unwrap_or(false), - allow_undefined: allow_undefined.unwrap_or(false), - recipe_path, - }; - - Ok(PySelectorConfig { - inner: selector_config, - }) - } - - #[getter] - fn target_platform(&self) -> String { - self.inner.target_platform.to_string() - } - - #[setter] - fn set_target_platform(&mut self, value: String) -> PyResult<()> { - let platform = Platform::from_str(&value).map_err(RattlerBuildError::from)?; - self.inner.target_platform = platform; - Ok(()) - } - - #[getter] - fn host_platform(&self) -> String { - self.inner.host_platform.to_string() - } - - #[setter] - fn set_host_platform(&mut self, value: String) -> PyResult<()> { - let platform = Platform::from_str(&value).map_err(RattlerBuildError::from)?; - self.inner.host_platform = platform; - Ok(()) - } - - #[getter] - fn build_platform(&self) -> String { - self.inner.build_platform.to_string() - } - - #[setter] - fn set_build_platform(&mut self, value: String) -> PyResult<()> { - let platform = Platform::from_str(&value).map_err(RattlerBuildError::from)?; - self.inner.build_platform = platform; - Ok(()) - } - - #[getter] - fn experimental(&self) -> bool { - self.inner.experimental - } - - #[setter] - fn set_experimental(&mut self, value: bool) { - self.inner.experimental = value; - } - - #[getter] - fn allow_undefined(&self) -> bool { - self.inner.allow_undefined - } - - #[setter] - fn set_allow_undefined(&mut self, value: bool) { - self.inner.allow_undefined = value; - } - - #[getter] - fn recipe_path(&self) -> Option { - self.inner.recipe_path.clone() - } - - #[setter] - fn set_recipe_path(&mut self, value: Option) { - self.inner.recipe_path = value; - } - - #[getter] - fn hash(&self) -> Option { - self.inner.hash.as_ref().map(|h| h.hash.clone()) - } - - #[setter] - fn set_hash(&mut self, value: Option) { - self.inner.hash = value.map(|h| HashInfo { - hash: h, - prefix: String::new(), - }); - } - - #[getter] - fn variant(&self, py: Python<'_>) -> PyResult> { - let mut dict = HashMap::new(); - for (key, value) in &self.inner.variant { - let json_value = serde_json::to_value(value).map_err(RattlerBuildError::from)?; - dict.insert(key.normalize(), json_value); - } - Ok(pythonize::pythonize(py, &dict) - .map(|obj| obj.into()) - .map_err(|e| { - RattlerBuildError::Variant(format!("Failed to convert variant to Python: {}", e)) - })?) - } - - #[setter] - fn set_variant(&mut self, py: Python<'_>, value: HashMap>) -> PyResult<()> { - let mut map = BTreeMap::new(); - for (key, py_value) in value { - let normalized_key = NormalizedKey::from(key); - let json_val: serde_json::Value = - pythonize::depythonize(py_value.bind(py)).map_err(|e| { - RattlerBuildError::Variant(format!("Failed to convert variant value: {}", e)) - })?; - let variable = match &json_val { - JsonValue::String(s) => Variable::from_string(s), - JsonValue::Bool(b) => Variable::from(*b), - JsonValue::Number(n) => { - if let Some(i) = n.as_i64() { - Variable::from(i) - } else { - Variable::from_string(&n.to_string()) - } - } - JsonValue::Array(arr) => { - let vars: Result, RattlerBuildError> = arr - .iter() - .map(|v| match v { - JsonValue::String(s) => Ok(Variable::from_string(s)), - JsonValue::Bool(b) => Ok(Variable::from(*b)), - JsonValue::Number(n) => Ok(if let Some(i) = n.as_i64() { - Variable::from(i) - } else { - Variable::from_string(&n.to_string()) - }), - _ => Err(RattlerBuildError::Variant( - "Complex array elements not supported".to_string(), - )), - }) - .collect(); - Variable::from(vars?) - } - _ => { - return Err(RattlerBuildError::Variant( - "Object and null variants not supported".to_string(), - ) - .into()); - } - }; - map.insert(normalized_key, variable); - } - self.inner.variant = map; - Ok(()) - } -} - // Bind the get version function to the Python module #[pyfunction] fn get_rattler_build_version_py() -> PyResult { Ok(get_rattler_build_version().to_string()) } -/// Generate a PyPI recipe and return the YAML as a string. -#[pyfunction] -#[pyo3(signature = (package, version=None, use_mapping=true))] -fn generate_pypi_recipe_string_py( - package: String, - version: Option, - use_mapping: bool, -) -> PyResult { - let opts = PyPIOpts { - package, - version, - write: false, - use_mapping, - tree: false, - }; - - run_async_task(generate_pypi_recipe_string(&opts)) -} - -/// Generate a CRAN (R) recipe and return the YAML as a string. -#[pyfunction] -#[pyo3(signature = (package, universe=None))] -fn generate_r_recipe_string_py(package: String, universe: Option) -> PyResult { - run_async_task(generate_r_recipe_string(&package, universe.as_deref())) -} - -/// Generate a CPAN (Perl) recipe and return the YAML as a string. -#[pyfunction] -#[pyo3(signature = (package, version=None))] -fn generate_cpan_recipe_string_py(package: String, version: Option) -> PyResult { - let opts = CpanOpts { - package, - version, - write: false, - tree: false, - }; - - run_async_task(generate_cpan_recipe_string(&opts)) -} - -/// Generate a LuaRocks recipe and return the YAML as a string. -#[pyfunction] -#[pyo3(signature = (rock))] -fn generate_luarocks_recipe_string_py(rock: String) -> PyResult { - run_async_task(generate_luarocks_recipe_string(&rock)) -} - +// Legacy parse_recipe_py function - now use the stage0/stage1 Python bindings instead +// This function is commented out as the Recipe::from_yaml API has changed +// Use the new Stage0Recipe.from_yaml() in the stage0 module +/* /// Parse a recipe YAML string and return the parsed recipe as a Python dictionary. #[pyfunction] #[pyo3(signature = (yaml_content, selector_config))] @@ -363,29 +57,11 @@ fn parse_recipe_py( yaml_content: String, selector_config: &PySelectorConfig, ) -> PyResult> { - match Recipe::from_yaml(yaml_content.as_str(), selector_config.inner.clone()) { - Ok(recipe) => { - let json_value = serde_json::to_value(recipe).map_err(RattlerBuildError::from)?; - - Python::attach(|py| { - pythonize::pythonize(py, &json_value) - .map(|obj| obj.into()) - .map_err(|e| { - RattlerBuildError::RecipeParse(format!( - "Failed to convert to Python: {}", - e - )) - .into() - }) - }) - } - Err(errors) => Err(RattlerBuildError::RecipeParse(format!( - "Recipe parsing failed: {:?}", - errors - )) - .into()), - } + // This function needs to be updated to work with the new stage0/stage1 API + // For now, use the stage0.Recipe.from_yaml() method instead + unimplemented!("Use stage0.Recipe.from_yaml() instead") } +*/ #[pyfunction] #[pyo3(signature = (recipes, up_to, build_platform, target_platform, host_platform, channel, variant_config, variant_overrides=None, ignore_recipe_variants=false, render_only=false, with_solve=false, keep_build=false, no_build_id=false, package_format=None, compression_threads=None, io_concurrency_limit=None, no_include_recipe=false, test=None, output_dir=None, auth_file=None, channel_priority=None, skip_existing=None, noarch_build_platform=None, allow_insecure_host=None, continue_on_failure=false, debug=false, error_prefix_in_binary=false, allow_symlinks_on_windows=false, exclude_newer=None, use_bz2=true, use_zstd=true, use_jlap=false, use_sharded=true))] @@ -516,6 +192,278 @@ fn build_recipes_py( }) } +/// Build from already-rendered variants (Stage1 recipes) +/// +/// This function takes RenderedVariant objects (from recipe.render()) and builds them +/// directly without needing to write temporary files. +/// +/// If tool_config is provided, it will be used instead of the individual parameters. +#[pyfunction] +#[pyo3(signature = (rendered_variants, tool_config=None, output_dir=None, channel=None, progress_callback=None, recipe_path=None, keep_build=false, no_build_id=false, package_format=None, compression_threads=None, io_concurrency_limit=None, no_include_recipe=false, test=None, auth_file=None, channel_priority=None, skip_existing=None, allow_insecure_host=None, continue_on_failure=false, debug=false, _error_prefix_in_binary=false, _allow_symlinks_on_windows=false, exclude_newer=None, use_bz2=true, use_zstd=true, use_jlap=false, use_sharded=true))] +#[allow(clippy::too_many_arguments)] +fn build_from_rendered_variants_py( + rendered_variants: Vec, + tool_config: Option, + output_dir: Option, + channel: Option>, + progress_callback: Option>, + recipe_path: Option, + keep_build: bool, + no_build_id: bool, + package_format: Option, + compression_threads: Option, + io_concurrency_limit: Option, + no_include_recipe: bool, + test: Option, + auth_file: Option, + channel_priority: Option, + skip_existing: Option, + allow_insecure_host: Option>, + continue_on_failure: bool, + debug: bool, + _error_prefix_in_binary: bool, + _allow_symlinks_on_windows: bool, + exclude_newer: Option>, + use_bz2: bool, + use_zstd: bool, + use_jlap: bool, + use_sharded: bool, +) -> PyResult<()> { + use ::rattler_build::{ + console_utils::LoggingOutputHandler, + metadata::{BuildConfiguration, Output, PlatformWithVirtualPackages}, + run_build_from_args, + system_tools::SystemTools, + tool_configuration::Configuration, + types::{BuildSummary, Directories, PackageIdentifier, PackagingSettings}, + }; + use rattler_build_recipe::stage1::HashInfo; + use rattler_build_types::NormalizedKey; + use rattler_solve::SolveStrategy; + use std::{ + collections::BTreeMap, + sync::{Arc, Mutex}, + }; + + // Use provided tool_config or build one from parameters + let tool_config = if let Some(config) = tool_config { + config.inner + } else { + let channel_priority = channel_priority + .map(|c| ChannelPriorityWrapper::from_str(&c).map(|c| c.value)) + .transpose() + .map_err(|e| RattlerBuildError::ChannelPriority(e.to_string()))?; + + let config = ConfigBase::<()>::default(); + let channel_config = config.channel_config.clone(); + let _common = CommonData::new( + output_dir.clone(), + false, + auth_file.map(|a| a.into()), + config, + channel_priority, + allow_insecure_host.clone(), + use_bz2, + use_zstd, + use_jlap, + use_sharded, + ); + + let test_strategy = test.map(|t| TestStrategy::from_str(&t, false).unwrap()); + let skip_existing = skip_existing.map(|s| SkipExisting::from_str(&s, false).unwrap()); + + // Use a hidden multi-progress if Python callback is provided to suppress Rust progress bars + let log_handler = if progress_callback.is_some() { + use indicatif::MultiProgress; + // Create a hidden MultiProgress that doesn't render to terminal + let mp = MultiProgress::with_draw_target(indicatif::ProgressDrawTarget::hidden()); + LoggingOutputHandler::default().with_multi_progress(mp) + } else { + LoggingOutputHandler::default() + }; + + Configuration::builder() + .with_logging_output_handler(log_handler) + .with_channel_config(channel_config.clone()) + .with_compression_threads(compression_threads) + .with_io_concurrency_limit(io_concurrency_limit) + .with_keep_build(keep_build) + .with_test_strategy(test_strategy.unwrap_or(TestStrategy::Skip)) + .with_zstd_repodata_enabled(use_zstd) + .with_bz2_repodata_enabled(use_bz2) + .with_sharded_repodata_enabled(use_sharded) + .with_jlap_enabled(use_jlap) + .with_skip_existing(skip_existing.unwrap_or(SkipExisting::None)) + .with_channel_priority(channel_priority.unwrap_or_default()) + .with_continue_on_failure(ContinueOnFailure::from(continue_on_failure)) + .with_allow_insecure_host(allow_insecure_host) + .finish() + }; + + let package_format = package_format + .map(|p| PackageFormatAndCompression::from_str(&p)) + .transpose() + .map_err(|e| RattlerBuildError::PackageFormat(e.to_string()))?; + + let channels = match channel { + None => vec![NamedChannelOrUrl::Name("conda-forge".to_string())], + Some(channel) => channel + .iter() + .map(|c| { + NamedChannelOrUrl::from_str(c) + .map_err(|e| RattlerBuildError::ChannelPriority(e.to_string())) + .map_err(|e| e.into()) + }) + .collect::>()?, + }; + + // Convert rendered variants to Output objects + let output_dir = output_dir.unwrap_or_else(|| PathBuf::from(".")); + let timestamp = chrono::Utc::now(); + let virtual_package_override = rattler_virtual_packages::VirtualPackageOverrides::from_env(); + + let mut outputs = Vec::new(); + let mut subpackages = BTreeMap::new(); + + // First pass: collect all subpackage identifiers + for rendered_variant in &rendered_variants { + let recipe = &rendered_variant.inner.recipe; + subpackages.insert( + recipe.package.name.clone(), + PackageIdentifier { + name: recipe.package.name.clone(), + version: recipe.package.version.clone(), + build_string: recipe + .build + .string + .as_resolved() + .ok_or_else(|| { + RattlerBuildError::Other("Build string not resolved".to_string()) + })? + .to_string(), + }, + ); + } + + // If recipe_path is None, we should not include the recipe in the package + let effective_no_include_recipe = no_include_recipe || recipe_path.is_none(); + + // Create a safe fallback recipe path when None is provided + // We use a subdirectory in output_dir to avoid copying unrelated files + let safe_recipe_path = recipe_path + .clone() + .unwrap_or_else(|| output_dir.join("_no_recipe").join("recipe.yaml")); + + // Second pass: create Output objects + for rendered_variant in rendered_variants { + let recipe = rendered_variant.inner.recipe; + let variant = rendered_variant.inner.variant; + + // Extract platforms from variant or use current platform + let target_platform = variant + .get(&NormalizedKey("target_platform".to_string())) + .and_then(|v| v.to_string().parse::().ok()) + .unwrap_or_else(Platform::current); + + let build_platform = variant + .get(&NormalizedKey("build_platform".to_string())) + .and_then(|v| v.to_string().parse::().ok()) + .unwrap_or_else(Platform::current); + + let host_platform = variant + .get(&NormalizedKey("host_platform".to_string())) + .and_then(|v| v.to_string().parse::().ok()) + .unwrap_or_else(Platform::current); + + // Convert channels to base URLs + let channels_urls = channels + .iter() + .map(|c| c.clone().into_base_url(&tool_config.channel_config)) + .collect::, _>>() + .map_err(|e| RattlerBuildError::Other(format!("Channel error: {}", e)))?; + + // Create hash info - use default if not available + let hash_info = rendered_variant + .inner + .hash_info + .clone() + .unwrap_or_else(|| HashInfo { + hash: String::new(), + prefix: String::new(), + }); + + let build_name = recipe.package.name.as_normalized().to_string(); + + let output = Output { + recipe, + build_configuration: BuildConfiguration { + target_platform, + host_platform: PlatformWithVirtualPackages::detect_for_platform( + host_platform, + &virtual_package_override, + ) + .map_err(|e| { + RattlerBuildError::Other(format!("Platform detection error: {}", e)) + })?, + build_platform: PlatformWithVirtualPackages::detect_for_platform( + build_platform, + &virtual_package_override, + ) + .map_err(|e| { + RattlerBuildError::Other(format!("Platform detection error: {}", e)) + })?, + hash: hash_info, + variant, + directories: Directories::setup( + &build_name, + &safe_recipe_path, + &output_dir, + no_build_id, + ×tamp, + false, // merge_build_and_host_envs - we can infer from recipe if needed + ) + .map_err(|e| RattlerBuildError::Other(format!("Directory setup error: {}", e)))?, + channels: channels_urls.clone(), + channel_priority: tool_config.channel_priority, + solve_strategy: SolveStrategy::Highest, + timestamp, + subpackages: subpackages.clone(), + packaging_settings: PackagingSettings::from_args( + package_format + .as_ref() + .map(|p| p.archive_type) + .unwrap_or(rattler_conda_types::package::ArchiveType::Conda), + package_format + .as_ref() + .map(|p| p.compression_level) + .unwrap_or( + rattler_conda_types::compression_level::CompressionLevel::Default, + ), + ), + store_recipe: !effective_no_include_recipe, + force_colors: false, // Set to false for Python API + sandbox_config: None, + debug: ::rattler_build::metadata::Debug::new(debug), + exclude_newer, + }, + finalized_dependencies: None, + finalized_sources: None, + finalized_cache_dependencies: None, + finalized_cache_sources: None, + build_summary: Arc::new(Mutex::new(BuildSummary::default())), + system_tools: SystemTools::default(), + extra_meta: None, + }; + + outputs.push(output); + } + + // Run the build with optional tracing subscriber + tracing_subscriber::with_python_tracing(progress_callback, || { + run_async_task(async { run_build_from_args(outputs, tool_config).await }) + }) +} + #[allow(clippy::too_many_arguments)] #[pyfunction] #[pyo3(signature = (package_file, channel, compression_threads, auth_file, channel_priority, allow_insecure_host=None, debug=false, test_index=None, use_bz2=true, use_zstd=true, use_jlap=false, use_sharded=true))] @@ -579,153 +527,41 @@ fn test_package_py( }) } -#[pyfunction] -#[pyo3(signature = (package_files, url, channels, api_key, auth_file))] -fn upload_package_to_quetz_py( - package_files: Vec, - url: String, - channels: String, - api_key: Option, - auth_file: Option, -) -> PyResult<()> { - let store = tool_configuration::get_auth_store(auth_file).map_err(RattlerBuildError::Auth)?; - - let url = Url::parse(&url).map_err(RattlerBuildError::from)?; - let quetz_data = QuetzData::new(url, channels, api_key); - - run_async_task(async { - upload::upload_package_to_quetz(&store, &package_files, quetz_data).await?; - Ok(()) - }) -} - -#[pyfunction] -#[pyo3(signature = (package_files, url, channels, token, auth_file))] -fn upload_package_to_artifactory_py( - package_files: Vec, - url: String, - channels: String, - token: Option, - auth_file: Option, -) -> PyResult<()> { - let store = tool_configuration::get_auth_store(auth_file).map_err(RattlerBuildError::Auth)?; - let url = Url::parse(&url).map_err(RattlerBuildError::from)?; - let artifactory_data = ArtifactoryData::new(url, channels, token); - - run_async_task(async { - upload::upload_package_to_artifactory(&store, &package_files, artifactory_data).await?; - Ok(()) - }) -} - -#[pyfunction] -#[pyo3(signature = (package_files, url, channel, api_key, auth_file, skip_existing, attestation_file=None,))] -fn upload_package_to_prefix_py( - package_files: Vec, - url: String, - channel: String, - api_key: Option, - auth_file: Option, - skip_existing: bool, - attestation_file: Option, -) -> PyResult<()> { - let store = tool_configuration::get_auth_store(auth_file).map_err(RattlerBuildError::Auth)?; - - let url = Url::parse(&url).map_err(RattlerBuildError::from)?; - let prefix_data = PrefixData::new(url, channel, api_key, attestation_file, skip_existing); - - run_async_task(async { - upload::upload_package_to_prefix(&store, &package_files, prefix_data).await?; - Ok(()) - }) -} - -#[pyfunction] -#[pyo3(signature = (package_files, owner, channel, api_key, url, force, auth_file))] -fn upload_package_to_anaconda_py( - package_files: Vec, - owner: String, - channel: Option>, - api_key: Option, - url: Option, - force: bool, - auth_file: Option, -) -> PyResult<()> { - let store = tool_configuration::get_auth_store(auth_file).map_err(RattlerBuildError::Auth)?; - - let url = url - .map(|u| Url::parse(&u)) - .transpose() - .map_err(RattlerBuildError::from)?; - let anaconda_data = AnacondaData::new(owner, channel, api_key, url, force); - - run_async_task(async { - upload::upload_package_to_anaconda(&store, &package_files, anaconda_data).await?; - Ok(()) - }) -} - -#[pyfunction] -#[pyo3(signature = (package_files, staging_token, feedstock, feedstock_token, staging_channel, anaconda_url, validation_endpoint, provider, dry_run))] -#[allow(clippy::too_many_arguments)] -fn upload_packages_to_conda_forge_py( - package_files: Vec, - staging_token: String, - feedstock: String, - feedstock_token: String, - staging_channel: Option, - anaconda_url: Option, - validation_endpoint: Option, - provider: Option, - dry_run: bool, -) -> PyResult<()> { - let anaconda_url = anaconda_url - .map(|u| Url::parse(&u)) - .transpose() - .map_err(|e| RattlerBuildError::Other(format!("Error parsing anaconda_url: {e}")))?; - - let validation_endpoint = validation_endpoint - .map(|u| Url::parse(&u)) - .transpose() - .map_err(|e| { - RattlerBuildError::Other(format!("Error parsing validation_endpoint: {e}",)) - })?; - - let conda_forge_data = CondaForgeData::new( - staging_token, - feedstock, - feedstock_token, - staging_channel, - anaconda_url, - validation_endpoint, - provider, - dry_run, - ); - - run_async_task(async { - upload::conda_forge::upload_packages_to_conda_forge(&package_files, conda_forge_data) - .await?; - Ok(()) - }) -} - #[pymodule] fn rattler_build<'py>(_py: Python<'py>, m: Bound<'py, PyModule>) -> PyResult<()> { error::register_exceptions(_py, &m)?; m.add_function(wrap_pyfunction!(get_rattler_build_version_py, &m).unwrap())?; - m.add_function(wrap_pyfunction!(generate_pypi_recipe_string_py, &m).unwrap())?; - m.add_function(wrap_pyfunction!(generate_r_recipe_string_py, &m).unwrap())?; - m.add_function(wrap_pyfunction!(generate_cpan_recipe_string_py, &m).unwrap())?; - m.add_function(wrap_pyfunction!(generate_luarocks_recipe_string_py, &m).unwrap())?; - m.add_function(wrap_pyfunction!(parse_recipe_py, &m).unwrap())?; + m.add_function( + wrap_pyfunction!(recipe_generation::generate_pypi_recipe_string_py, &m).unwrap(), + )?; + m.add_function(wrap_pyfunction!(recipe_generation::generate_r_recipe_string_py, &m).unwrap())?; + m.add_function( + wrap_pyfunction!(recipe_generation::generate_cpan_recipe_string_py, &m).unwrap(), + )?; + m.add_function( + wrap_pyfunction!(recipe_generation::generate_luarocks_recipe_string_py, &m).unwrap(), + )?; + // parse_recipe_py is deprecated - use stage0.Recipe.from_yaml() instead + // m.add_function(wrap_pyfunction!(parse_recipe_py, &m).unwrap())?; m.add_function(wrap_pyfunction!(build_recipes_py, &m).unwrap())?; + m.add_function(wrap_pyfunction!(build_from_rendered_variants_py, &m).unwrap())?; m.add_function(wrap_pyfunction!(test_package_py, &m).unwrap())?; - m.add_function(wrap_pyfunction!(upload_package_to_quetz_py, &m).unwrap())?; - m.add_function(wrap_pyfunction!(upload_package_to_artifactory_py, &m).unwrap())?; - m.add_function(wrap_pyfunction!(upload_package_to_prefix_py, &m).unwrap())?; - m.add_function(wrap_pyfunction!(upload_package_to_anaconda_py, &m).unwrap())?; - m.add_function(wrap_pyfunction!(upload_packages_to_conda_forge_py, &m).unwrap())?; - m.add_class::()?; + m.add_function(wrap_pyfunction!(upload::upload_package_to_quetz_py, &m).unwrap())?; + m.add_function(wrap_pyfunction!(upload::upload_package_to_artifactory_py, &m).unwrap())?; + m.add_function(wrap_pyfunction!(upload::upload_package_to_prefix_py, &m).unwrap())?; + m.add_function(wrap_pyfunction!(upload::upload_package_to_anaconda_py, &m).unwrap())?; + m.add_function(wrap_pyfunction!(upload::upload_packages_to_conda_forge_py, &m).unwrap())?; + m.add_class::()?; + + // Register all submodules + stage0::register_stage0_module(_py, &m)?; + stage1::register_stage1_module(_py, &m)?; + variant_config::register_variant_config_module(_py, &m)?; + render::register_render_module(_py, &m)?; + tool_config::register_tool_config_module(_py, &m)?; + build_types::register_build_types_module(_py, &m)?; + platform_types::register_platform_types_module(_py, &m)?; + progress_callback::register_progress_types(_py, &m)?; Ok(()) } diff --git a/py-rattler-build/src/platform_types.rs b/py-rattler-build/src/platform_types.rs new file mode 100644 index 000000000..836c67e2f --- /dev/null +++ b/py-rattler-build/src/platform_types.rs @@ -0,0 +1,134 @@ +use pyo3::prelude::*; +use pyo3::types::PyList; +use rattler_build::types::PlatformWithVirtualPackages; +use rattler_conda_types::Platform; + +use crate::error::RattlerBuildError; + +/// Python wrapper for PlatformWithVirtualPackages +#[pyclass(name = "PlatformWithVirtualPackages")] +#[derive(Clone)] +pub struct PyPlatformWithVirtualPackages { + pub(crate) inner: PlatformWithVirtualPackages, +} + +#[pymethods] +impl PyPlatformWithVirtualPackages { + /// Create a new platform with virtual packages + #[new] + #[pyo3(signature = (platform=None))] + fn new(platform: Option) -> PyResult { + let platform = if let Some(p) = platform { + p.parse::() + .map_err(|e| RattlerBuildError::Other(format!("Invalid platform: {}", e)))? + } else { + Platform::current() + }; + + let inner = PlatformWithVirtualPackages::detect_for_platform( + platform, + &rattler_virtual_packages::VirtualPackageOverrides::from_env(), + ) + .map_err(|e| { + RattlerBuildError::Other(format!("Failed to detect virtual packages: {}", e)) + })?; + + Ok(Self { inner }) + } + + /// Get the platform as a string + #[getter] + fn platform(&self) -> String { + self.inner.platform.to_string() + } + + /// Get the virtual packages + #[getter] + fn virtual_packages(&self, py: Python<'_>) -> PyResult> { + let list = PyList::empty(py); + for vp in &self.inner.virtual_packages { + let dict = pyo3::types::PyDict::new(py); + dict.set_item("name", vp.name.as_source())?; + dict.set_item("version", vp.version.to_string())?; + // build_string is a String, not an Option + dict.set_item("build_string", &vp.build_string)?; + list.append(dict)?; + } + Ok(list.into()) + } + + fn __repr__(&self) -> String { + format!( + "PlatformWithVirtualPackages(platform='{}', virtual_packages_count={})", + self.inner.platform, + self.inner.virtual_packages.len() + ) + } +} + +/// Python wrapper for Platform enum +#[pyclass(name = "Platform")] +#[derive(Clone)] +pub struct PyPlatform { + pub(crate) inner: Platform, +} + +#[pymethods] +impl PyPlatform { + /// Create a platform from a string + #[new] + fn new(platform: &str) -> PyResult { + let inner = platform + .parse::() + .map_err(|e| RattlerBuildError::Other(format!("Invalid platform: {}", e)))?; + Ok(Self { inner }) + } + + /// Get the current platform + #[staticmethod] + fn current() -> Self { + Self { + inner: Platform::current(), + } + } + + /// Check if this is a NoArch platform + fn is_noarch(&self) -> bool { + self.inner == Platform::NoArch + } + + /// Get the platform as a string + fn __str__(&self) -> String { + self.inner.to_string() + } + + fn __repr__(&self) -> String { + format!("Platform('{}')", self.inner) + } + + /// Check equality + fn __eq__(&self, other: &Self) -> bool { + self.inner == other.inner + } + + /// Hash for use in sets/dicts + fn __hash__(&self) -> u64 { + use std::collections::hash_map::DefaultHasher; + use std::hash::{Hash, Hasher}; + let mut hasher = DefaultHasher::new(); + format!("{:?}", self.inner).hash(&mut hasher); + hasher.finish() + } +} + +/// Register the platform_types module with Python +pub fn register_platform_types_module( + py: Python<'_>, + parent: &Bound<'_, PyModule>, +) -> PyResult<()> { + let m = PyModule::new(py, "platform_types")?; + m.add_class::()?; + m.add_class::()?; + parent.add_submodule(&m)?; + Ok(()) +} diff --git a/py-rattler-build/src/progress_callback.rs b/py-rattler-build/src/progress_callback.rs new file mode 100644 index 000000000..18df9f111 --- /dev/null +++ b/py-rattler-build/src/progress_callback.rs @@ -0,0 +1,235 @@ +use pyo3::prelude::*; +use std::sync::Arc; + +/// Event types for progress reporting +#[pyclass] +#[derive(Clone)] +pub struct DownloadStartEvent { + #[pyo3(get)] + pub url: String, + #[pyo3(get)] + pub total_bytes: Option, +} + +#[pymethods] +impl DownloadStartEvent { + fn __repr__(&self) -> String { + format!( + "DownloadStartEvent(url='{}', total_bytes={:?})", + self.url, self.total_bytes + ) + } +} + +#[pyclass] +#[derive(Clone)] +pub struct DownloadProgressEvent { + #[pyo3(get)] + pub url: String, + #[pyo3(get)] + pub bytes_downloaded: u64, + #[pyo3(get)] + pub total_bytes: Option, +} + +#[pymethods] +impl DownloadProgressEvent { + fn __repr__(&self) -> String { + format!( + "DownloadProgressEvent(url='{}', bytes_downloaded={}, total_bytes={:?})", + self.url, self.bytes_downloaded, self.total_bytes + ) + } +} + +#[pyclass] +#[derive(Clone)] +pub struct DownloadCompleteEvent { + #[pyo3(get)] + pub url: String, +} + +#[pymethods] +impl DownloadCompleteEvent { + fn __repr__(&self) -> String { + format!("DownloadCompleteEvent(url='{}')", self.url) + } +} + +#[pyclass] +#[derive(Clone)] +pub struct BuildStepEvent { + #[pyo3(get)] + pub step_name: String, + #[pyo3(get)] + pub message: String, +} + +#[pymethods] +impl BuildStepEvent { + fn __repr__(&self) -> String { + format!( + "BuildStepEvent(step_name='{}', message='{}')", + self.step_name, self.message + ) + } +} + +#[pyclass] +#[derive(Clone)] +pub struct LogEvent { + #[pyo3(get)] + pub level: String, + #[pyo3(get)] + pub message: String, + #[pyo3(get)] + pub span: Option, +} + +#[pymethods] +impl LogEvent { + fn __repr__(&self) -> String { + format!( + "LogEvent(level='{}', message='{}', span={:?})", + self.level, self.message, self.span + ) + } +} + +/// Python progress callback bridge +/// +/// This struct wraps a Python callback object and provides thread-safe +/// methods to invoke it from Rust code during the build process. +#[derive(Clone)] +pub struct PyProgressCallback { + callback: Arc>, +} + +impl PyProgressCallback { + /// Create a new Python progress callback wrapper + pub fn new(callback: Py) -> Self { + Self { + callback: Arc::new(callback), + } + } + + /// Get the inner Arc for sharing across threads + pub fn inner(&self) -> Arc> { + Arc::clone(&self.callback) + } + + /// Get a clone of this callback wrapper + pub fn clone_callback(&self) -> Arc { + Arc::new(self.clone()) + } + + /// Call the on_download_start callback + pub fn on_download_start(&self, url: &str, total_bytes: Option) { + if let Err(e) = Python::attach(|py| { + let event = DownloadStartEvent { + url: url.to_string(), + total_bytes, + }; + let event_obj = Py::new(py, event)?; + + self.callback + .bind(py) + .call_method1("on_download_start", (event_obj,))?; + Ok::<(), PyErr>(()) + }) { + // Log error but don't fail the build + eprintln!("Error in Python progress callback on_download_start: {}", e); + } + } + + /// Call the on_download_progress callback + pub fn on_download_progress(&self, url: &str, bytes_downloaded: u64, total_bytes: Option) { + if let Err(e) = Python::attach(|py| { + let event = DownloadProgressEvent { + url: url.to_string(), + bytes_downloaded, + total_bytes, + }; + let event_obj = Py::new(py, event)?; + + self.callback + .bind(py) + .call_method1("on_download_progress", (event_obj,))?; + Ok::<(), PyErr>(()) + }) { + eprintln!( + "Error in Python progress callback on_download_progress: {}", + e + ); + } + } + + /// Call the on_download_complete callback + pub fn on_download_complete(&self, url: &str) { + if let Err(e) = Python::attach(|py| { + let event = DownloadCompleteEvent { + url: url.to_string(), + }; + let event_obj = Py::new(py, event)?; + + self.callback + .bind(py) + .call_method1("on_download_complete", (event_obj,))?; + Ok::<(), PyErr>(()) + }) { + eprintln!( + "Error in Python progress callback on_download_complete: {}", + e + ); + } + } + + /// Call the on_build_step callback + pub fn on_build_step(&self, step_name: &str, message: &str) { + if let Err(e) = Python::attach(|py| { + let event = BuildStepEvent { + step_name: step_name.to_string(), + message: message.to_string(), + }; + let event_obj = Py::new(py, event)?; + + self.callback + .bind(py) + .call_method1("on_build_step", (event_obj,))?; + Ok::<(), PyErr>(()) + }) { + eprintln!("Error in Python progress callback on_build_step: {}", e); + } + } + + /// Call the on_log callback + pub fn on_log(&self, level: &str, message: &str, span: Option<&str>) { + if let Err(e) = Python::attach(|py| { + let event = LogEvent { + level: level.to_string(), + message: message.to_string(), + span: span.map(|s| s.to_string()), + }; + let event_obj = Py::new(py, event)?; + + self.callback + .bind(py) + .call_method1("on_log", (event_obj,))?; + Ok::<(), PyErr>(()) + }) { + eprintln!("Error in Python progress callback on_log: {}", e); + } + } +} + +/// Register progress callback types with Python +pub fn register_progress_types(py: Python<'_>, parent: &Bound<'_, PyModule>) -> PyResult<()> { + let m = PyModule::new(py, "progress")?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + parent.add_submodule(&m)?; + Ok(()) +} diff --git a/py-rattler-build/src/recipe_generation.rs b/py-rattler-build/src/recipe_generation.rs new file mode 100644 index 000000000..437165976 --- /dev/null +++ b/py-rattler-build/src/recipe_generation.rs @@ -0,0 +1,57 @@ +use ::rattler_build::recipe_generator::{ + CpanOpts, PyPIOpts, generate_cpan_recipe_string, generate_luarocks_recipe_string, + generate_pypi_recipe_string, generate_r_recipe_string, +}; +use pyo3::prelude::*; + +use crate::run_async_task; + +/// Generate a PyPI recipe and return the YAML as a string. +#[pyfunction] +#[pyo3(signature = (package, version=None, use_mapping=true))] +pub fn generate_pypi_recipe_string_py( + package: String, + version: Option, + use_mapping: bool, +) -> PyResult { + let opts = PyPIOpts { + package, + version, + write: false, + use_mapping, + tree: false, + }; + + run_async_task(generate_pypi_recipe_string(&opts)) +} + +/// Generate a CRAN (R) recipe and return the YAML as a string. +#[pyfunction] +#[pyo3(signature = (package, universe=None))] +pub fn generate_r_recipe_string_py(package: String, universe: Option) -> PyResult { + run_async_task(generate_r_recipe_string(&package, universe.as_deref())) +} + +/// Generate a CPAN (Perl) recipe and return the YAML as a string. +#[pyfunction] +#[pyo3(signature = (package, version=None))] +pub fn generate_cpan_recipe_string_py( + package: String, + version: Option, +) -> PyResult { + let opts = CpanOpts { + package, + version, + write: false, + tree: false, + }; + + run_async_task(generate_cpan_recipe_string(&opts)) +} + +/// Generate a LuaRocks recipe and return the YAML as a string. +#[pyfunction] +#[pyo3(signature = (rock))] +pub fn generate_luarocks_recipe_string_py(rock: String) -> PyResult { + run_async_task(generate_luarocks_recipe_string(&rock)) +} diff --git a/py-rattler-build/src/render.rs b/py-rattler-build/src/render.rs new file mode 100644 index 000000000..ee4a1bfd0 --- /dev/null +++ b/py-rattler-build/src/render.rs @@ -0,0 +1,396 @@ +use std::path::PathBuf; + +use indexmap::IndexMap; +use pyo3::prelude::*; +use pyo3::types::PyDict; +use rattler_build_jinja::Variable; +use rattler_build_recipe::variant_render::{ + RenderConfig as RustRenderConfig, RenderedVariant as RustRenderedVariant, + render_recipe_with_variant_config, +}; +use rattler_conda_types::Platform; + +use crate::error::RattlerBuildError; +use crate::stage0::PyStage0Recipe; +use crate::stage1::PyStage1Recipe; +use crate::variant_config::PyVariantConfig; + +/// Configuration for rendering recipes with variants +#[pyclass(name = "RenderConfig")] +#[derive(Clone)] +pub struct PyRenderConfig { + pub(crate) inner: RustRenderConfig, +} + +#[pymethods] +impl PyRenderConfig { + /// Create a new render configuration with default settings + #[new] + #[pyo3(signature = (target_platform=None, build_platform=None, host_platform=None, experimental=false, recipe_path=None))] + fn new( + target_platform: Option, + build_platform: Option, + host_platform: Option, + experimental: bool, + recipe_path: Option, + ) -> PyResult { + let target_platform = target_platform + .map(|p| { + p.parse::().map_err(|e| { + RattlerBuildError::Other(format!("Invalid target_platform: {}", e)) + }) + }) + .transpose()? + .unwrap_or_else(Platform::current); + + let build_platform = build_platform + .map(|p| { + p.parse::() + .map_err(|e| RattlerBuildError::Other(format!("Invalid build_platform: {}", e))) + }) + .transpose()? + .unwrap_or_else(Platform::current); + + let host_platform = host_platform + .map(|p| { + p.parse::() + .map_err(|e| RattlerBuildError::Other(format!("Invalid host_platform: {}", e))) + }) + .transpose()? + .unwrap_or_else(Platform::current); + + Ok(Self { + inner: RustRenderConfig { + extra_context: IndexMap::new(), + experimental, + recipe_path, + target_platform, + build_platform, + host_platform, + }, + }) + } + + /// Add an extra context variable for Jinja rendering + /// + /// # Arguments + /// * `key` - The variable name + /// * `value` - The value (can be string, bool, int, float, or list) + fn set_context(&mut self, key: String, value: Bound<'_, PyAny>) -> PyResult<()> { + let variable = python_to_variable(value)?; + self.inner.extra_context.insert(key, variable); + Ok(()) + } + + /// Get an extra context variable + fn get_context(&self, py: Python<'_>, key: &str) -> PyResult>> { + if let Some(var) = self.inner.extra_context.get(key) { + Ok(Some(variable_to_python(py, var)?)) + } else { + Ok(None) + } + } + + /// Get all extra context variables as a dictionary + fn get_all_context(&self, py: Python<'_>) -> PyResult> { + let dict = PyDict::new(py); + for (key, value) in &self.inner.extra_context { + dict.set_item(key, variable_to_python(py, value)?)?; + } + Ok(dict.into()) + } + + /// Set the target platform + fn set_target_platform(&mut self, platform: String) -> PyResult<()> { + self.inner.target_platform = platform + .parse() + .map_err(|e| RattlerBuildError::Other(format!("Invalid platform: {}", e)))?; + Ok(()) + } + + /// Set the build platform + fn set_build_platform(&mut self, platform: String) -> PyResult<()> { + self.inner.build_platform = platform + .parse() + .map_err(|e| RattlerBuildError::Other(format!("Invalid platform: {}", e)))?; + Ok(()) + } + + /// Set the host platform + fn set_host_platform(&mut self, platform: String) -> PyResult<()> { + self.inner.host_platform = platform + .parse() + .map_err(|e| RattlerBuildError::Other(format!("Invalid platform: {}", e)))?; + Ok(()) + } + + /// Set whether experimental features are enabled + fn set_experimental(&mut self, experimental: bool) { + self.inner.experimental = experimental; + } + + /// Set the recipe path for relative path resolution + fn set_recipe_path(&mut self, recipe_path: Option) { + self.inner.recipe_path = recipe_path; + } + + /// Get the target platform as a string + fn target_platform(&self) -> String { + self.inner.target_platform.to_string() + } + + /// Get the build platform as a string + fn build_platform(&self) -> String { + self.inner.build_platform.to_string() + } + + /// Get the host platform as a string + fn host_platform(&self) -> String { + self.inner.host_platform.to_string() + } + + /// Get whether experimental features are enabled + fn experimental(&self) -> bool { + self.inner.experimental + } + + /// Get the recipe path + fn recipe_path(&self) -> Option { + self.inner.recipe_path.clone() + } + + fn __repr__(&self) -> String { + format!( + "RenderConfig(target_platform='{}', build_platform='{}', host_platform='{}', experimental={})", + self.inner.target_platform, + self.inner.build_platform, + self.inner.host_platform, + self.inner.experimental + ) + } +} + +/// Hash information for a rendered variant +#[pyclass(name = "HashInfo")] +#[derive(Clone, Debug)] +pub struct PyHashInfo { + #[pyo3(get)] + pub hash: String, + #[pyo3(get)] + pub prefix: String, +} + +#[pymethods] +impl PyHashInfo { + fn __repr__(&self) -> String { + format!("HashInfo(hash='{}', prefix='{}')", self.hash, self.prefix) + } +} + +/// Information about a pin_subpackage dependency +#[pyclass(name = "PinSubpackageInfo")] +#[derive(Clone, Debug)] +pub struct PyPinSubpackageInfo { + #[pyo3(get)] + pub name: String, + #[pyo3(get)] + pub version: String, + #[pyo3(get)] + pub build_string: Option, + #[pyo3(get)] + pub exact: bool, +} + +#[pymethods] +impl PyPinSubpackageInfo { + fn __repr__(&self) -> String { + format!( + "PinSubpackageInfo(name='{}', version='{}', build_string={:?}, exact={})", + self.name, self.version, self.build_string, self.exact + ) + } +} + +/// Result of rendering a recipe with a specific variant combination +#[pyclass(name = "RenderedVariant")] +#[derive(Clone)] +pub struct PyRenderedVariant { + pub(crate) inner: RustRenderedVariant, +} + +#[pymethods] +impl PyRenderedVariant { + /// Get the variant combination used (variable name -> value) + fn variant(&self, py: Python<'_>) -> PyResult> { + let dict = PyDict::new(py); + for (key, value) in &self.inner.variant { + dict.set_item(key.0.as_str(), variable_to_python(py, value)?)?; + } + Ok(dict.into()) + } + + /// Get the rendered stage1 recipe + fn recipe(&self) -> PyStage1Recipe { + PyStage1Recipe { + inner: self.inner.recipe.clone(), + } + } + + /// Get hash info if available + fn hash_info(&self) -> Option { + self.inner.hash_info.as_ref().map(|hash_info| PyHashInfo { + hash: hash_info.hash.clone(), + prefix: hash_info.prefix.clone(), + }) + } + + /// Get pin_subpackage information + fn pin_subpackages(&self, py: Python<'_>) -> PyResult> { + let dict = PyDict::new(py); + for (key, pin_info) in &self.inner.pin_subpackages { + let py_pin_info = PyPinSubpackageInfo { + name: pin_info.name.as_normalized().to_string(), + version: pin_info.version.to_string(), + build_string: pin_info.build_string.clone(), + exact: pin_info.exact, + }; + dict.set_item(key.0.as_str(), py_pin_info)?; + } + Ok(dict.into()) + } + + fn __repr__(&self) -> String { + format!( + "RenderedVariant(package='{}', version='{}', build_string='{}')", + self.inner.recipe.package.name.as_normalized(), + self.inner.recipe.package.version, + self.inner + .recipe + .build + .string + .as_resolved() + .map(|s| s.to_string()) + .unwrap_or_else(|| "None".to_string()) + ) + } +} + +/// Render a Stage0 recipe with a variant configuration into Stage1 recipes +/// +/// # Arguments +/// * `recipe` - The Stage0 recipe to render (Recipe, SingleOutputRecipe, or MultiOutputRecipe) +/// * `variant_config` - The variant configuration +/// * `render_config` - Optional render configuration (defaults to current platform) +/// +/// # Returns +/// A list of RenderedVariant objects, one for each variant combination +#[pyfunction] +#[pyo3(signature = (recipe, variant_config, render_config=None))] +pub fn render_recipe( + recipe: &Bound<'_, PyAny>, + variant_config: &PyVariantConfig, + render_config: Option, +) -> PyResult> { + let config = render_config.unwrap_or_else(|| PyRenderConfig { + inner: RustRenderConfig::default(), + }); + + // Try to extract the inner stage0 recipe + let stage0_recipe = if let Ok(r) = recipe.extract::>() { + r.inner.clone() + } else { + return Err(RattlerBuildError::Other("Expected a Stage0 Recipe".to_string()).into()); + }; + + // Call the Rust render function + let rendered = + render_recipe_with_variant_config(&stage0_recipe, &variant_config.inner, config.inner) + .map_err(|e| RattlerBuildError::Other(format!("Render error: {:?}", e)))?; + + // Convert to Python objects + Ok(rendered + .into_iter() + .map(|r| PyRenderedVariant { inner: r }) + .collect()) +} + +/// Helper function to convert Python values to Variable +fn python_to_variable(value: Bound<'_, PyAny>) -> PyResult { + if let Ok(b) = value.extract::() { + Ok(Variable::from(b)) + } else if let Ok(i) = value.extract::() { + Ok(Variable::from(i)) + } else if let Ok(s) = value.extract::() { + Ok(Variable::from(s)) + } else if let Ok(list) = value.downcast::() { + let items: PyResult> = + list.iter().map(|item| python_to_variable(item)).collect(); + Ok(Variable::from(items?)) + } else { + Ok(Variable::from(value.to_string())) + } +} + +/// Helper function to convert Variable to Python values +fn variable_to_python(py: Python<'_>, var: &Variable) -> PyResult> { + // Try to extract as bool first (must be before number check) + if let Some(b) = var.as_bool() { + let json_val = serde_json::Value::Bool(b); + return pythonize::pythonize(py, &json_val) + .map(|obj| obj.into()) + .map_err(|e| { + RattlerBuildError::Other(format!("Failed to convert bool: {}", e)).into() + }); + } + + // Try to extract as integer + if let Some(i) = var.as_i64() { + let json_val = serde_json::Value::Number(i.into()); + return pythonize::pythonize(py, &json_val) + .map(|obj| obj.into()) + .map_err(|e| RattlerBuildError::Other(format!("Failed to convert int: {}", e)).into()); + } + + // Try to extract as string + if let Some(s) = var.as_str() { + let json_val = serde_json::Value::String(s.to_string()); + return pythonize::pythonize(py, &json_val) + .map(|obj| obj.into()) + .map_err(|e| { + RattlerBuildError::Other(format!("Failed to convert string: {}", e)).into() + }); + } + + // Try to extract as list/sequence + if var.is_sequence() { + let mut vec = Vec::new(); + if let Ok(iter) = var.try_iter() { + for item in iter { + let item_var = Variable::from(item); + let py_item = variable_to_python(py, &item_var)?; + vec.push(py_item); + } + let list = pyo3::types::PyList::new(py, &vec)?; + return Ok(list.unbind().into()); + } + } + + // Fallback to string representation + let s = var.to_string(); + let json_val = serde_json::Value::String(s); + pythonize::pythonize(py, &json_val) + .map(|obj| obj.into()) + .map_err(|e| RattlerBuildError::Other(format!("Failed to convert value: {}", e)).into()) +} + +/// Register the render module with Python +pub fn register_render_module(py: Python<'_>, parent: &Bound<'_, PyModule>) -> PyResult<()> { + let m = PyModule::new(py, "render")?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(render_recipe, &m)?)?; + parent.add_submodule(&m)?; + Ok(()) +} diff --git a/py-rattler-build/src/stage0.rs b/py-rattler-build/src/stage0.rs new file mode 100644 index 000000000..659216917 --- /dev/null +++ b/py-rattler-build/src/stage0.rs @@ -0,0 +1,713 @@ +// Python bindings for rattler-build stage0 types (parsed recipe) + +use crate::error::RattlerBuildError; +use pyo3::types::PyDict; +use pyo3::{IntoPyObjectExt, prelude::*}; +use rattler_build_recipe::stage0; + +/// Stage0 Recipe - The parsed recipe before evaluation +/// +/// This is a union type that wraps either SingleOutputRecipe or MultiOutputRecipe. +/// Most users should use `from_yaml()` which returns the specific type directly. +#[pyclass(name = "Stage0Recipe")] +#[derive(Clone)] +pub struct PyStage0Recipe { + pub(crate) inner: stage0::Recipe, +} + +#[pymethods] +impl PyStage0Recipe { + /// Parse a recipe from YAML string + #[staticmethod] + fn from_yaml(yaml: &str) -> PyResult { + let recipe = stage0::parse_recipe_or_multi_from_source(yaml) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{:?}", e)))?; + Ok(PyStage0Recipe { inner: recipe }) + } + + /// Create a recipe from a Python dictionary + #[staticmethod] + fn from_dict(dict: &Bound<'_, PyAny>) -> PyResult { + // Convert Python dict to JSON value via pythonize + let json_value: serde_json::Value = pythonize::depythonize(dict).map_err(|e| { + RattlerBuildError::RecipeParse(format!("Failed to convert Python dict to JSON: {}", e)) + })?; + + // Convert to YAML string using the JSON value's serde repr + // This is a simple approach: serialize to JSON and parse as YAML + let json_string = serde_json::to_string(&json_value).map_err(|e| { + RattlerBuildError::RecipeParse(format!("Failed to serialize to JSON: {}", e)) + })?; + + // Parse as YAML (YAML is a superset of JSON, so this works) + let recipe = stage0::parse_recipe_or_multi_from_source(&json_string) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{:?}", e)))?; + + Ok(PyStage0Recipe { inner: recipe }) + } + + /// Check if this is a single output recipe + fn is_single_output(&self) -> bool { + matches!(self.inner, stage0::Recipe::SingleOutput(_)) + } + + /// Check if this is a multi output recipe + fn is_multi_output(&self) -> bool { + matches!(self.inner, stage0::Recipe::MultiOutput(_)) + } + + /// Get the recipe as a single output (returns None if multi-output) + fn as_single_output(&self) -> Option { + match &self.inner { + stage0::Recipe::SingleOutput(single) => Some(PySingleOutputRecipe { + inner: single.as_ref().clone(), + }), + _ => None, + } + } + + /// Get the recipe as a multi output (returns None if single-output) + fn as_multi_output(&self) -> Option { + match &self.inner { + stage0::Recipe::MultiOutput(multi) => Some(PyMultiOutputRecipe { + inner: multi.as_ref().clone(), + }), + _ => None, + } + } + + /// Get the package information + /// For single-output recipes, returns the package directly. + /// For multi-output recipes, returns None (use .recipe.package on the multi-output recipe instead). + #[getter] + fn package(&self) -> Option { + match &self.inner { + stage0::Recipe::SingleOutput(single) => Some(PyStage0Package { + inner: single.package.clone(), + }), + _ => None, + } + } + + /// Get the build configuration + #[getter] + fn build(&self) -> PyStage0Build { + match &self.inner { + stage0::Recipe::SingleOutput(single) => PyStage0Build { + inner: single.build.clone(), + }, + stage0::Recipe::MultiOutput(multi) => PyStage0Build { + inner: multi.build.clone(), + }, + } + } + + /// Get the requirements + /// For single-output recipes, returns the requirements directly. + /// For multi-output recipes, returns None (requirements are per-output). + #[getter] + fn requirements(&self) -> Option { + match &self.inner { + stage0::Recipe::SingleOutput(single) => Some(PyStage0Requirements { + inner: single.requirements.clone(), + }), + _ => None, + } + } + + /// Get the about metadata + #[getter] + fn about(&self) -> PyStage0About { + match &self.inner { + stage0::Recipe::SingleOutput(single) => PyStage0About { + inner: single.about.clone(), + }, + stage0::Recipe::MultiOutput(multi) => PyStage0About { + inner: multi.about.clone(), + }, + } + } + + /// Get the context dictionary + #[getter] + fn context(&self, py: Python<'_>) -> PyResult> { + let context_map = match &self.inner { + stage0::Recipe::SingleOutput(single) => &single.context, + stage0::Recipe::MultiOutput(multi) => &multi.context, + }; + + let dict = PyDict::new(py); + for (key, value) in context_map { + let json_value = serde_json::to_value(value).map_err(RattlerBuildError::from)?; + let py_value = pythonize::pythonize(py, &json_value) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)))?; + dict.set_item(key, py_value)?; + } + Ok(dict.into()) + } + + /// Convert to Python dictionary representation + fn to_dict(&self, py: Python<'_>) -> PyResult> { + let json_value = serde_json::to_value(&self.inner).map_err(RattlerBuildError::from)?; + pythonize::pythonize(py, &json_value) + .map(|obj| obj.into()) + .map_err(|e| { + RattlerBuildError::RecipeParse(format!("Failed to convert to Python: {}", e)).into() + }) + } + + fn __repr__(&self) -> String { + match &self.inner { + stage0::Recipe::SingleOutput(_) => "Stage0Recipe(type='single-output')".to_string(), + stage0::Recipe::MultiOutput(_) => "Stage0Recipe(type='multi-output')".to_string(), + } + } +} + +/// Stage0 Single Output Recipe +#[pyclass(name = "SingleOutputRecipe")] +#[derive(Clone)] +pub struct PySingleOutputRecipe { + pub(crate) inner: stage0::SingleOutputRecipe, +} + +#[pymethods] +impl PySingleOutputRecipe { + #[getter] + fn schema_version(&self) -> Option { + self.inner.schema_version + } + + #[getter] + fn context(&self, py: Python<'_>) -> PyResult> { + let dict = PyDict::new(py); + for (key, value) in &self.inner.context { + let json_value = serde_json::to_value(value).map_err(RattlerBuildError::from)?; + let py_value = pythonize::pythonize(py, &json_value) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)))?; + dict.set_item(key, py_value)?; + } + Ok(dict.into()) + } + + #[getter] + fn package(&self) -> PyStage0Package { + PyStage0Package { + inner: self.inner.package.clone(), + } + } + + #[getter] + fn build(&self) -> PyStage0Build { + PyStage0Build { + inner: self.inner.build.clone(), + } + } + + #[getter] + fn requirements(&self) -> PyStage0Requirements { + PyStage0Requirements { + inner: self.inner.requirements.clone(), + } + } + + #[getter] + fn about(&self) -> PyStage0About { + PyStage0About { + inner: self.inner.about.clone(), + } + } + + /// Convert to Python dictionary + fn to_dict(&self, py: Python<'_>) -> PyResult> { + let json_value = serde_json::to_value(&self.inner).map_err(RattlerBuildError::from)?; + pythonize::pythonize(py, &json_value) + .map(|obj| obj.into()) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)).into()) + } + + fn __repr__(&self) -> String { + format!( + "SingleOutputRecipe(name='{}', version='{}')", + serde_json::to_value(&self.inner.package.name) + .and_then(serde_json::from_value::) + .unwrap_or_default(), + serde_json::to_value(&self.inner.package.version) + .and_then(serde_json::from_value::) + .unwrap_or_default() + ) + } +} + +/// Stage0 Multi Output Recipe +#[pyclass(name = "MultiOutputRecipe")] +#[derive(Clone)] +pub struct PyMultiOutputRecipe { + pub(crate) inner: stage0::MultiOutputRecipe, +} + +#[pymethods] +impl PyMultiOutputRecipe { + #[getter] + fn schema_version(&self) -> Option { + self.inner.schema_version + } + + #[getter] + fn context(&self, py: Python<'_>) -> PyResult> { + let dict = PyDict::new(py); + for (key, value) in &self.inner.context { + let json_value = serde_json::to_value(value).map_err(RattlerBuildError::from)?; + let py_value = pythonize::pythonize(py, &json_value) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)))?; + dict.set_item(key, py_value)?; + } + Ok(dict.into()) + } + + #[getter] + fn recipe(&self) -> PyStage0RecipeMetadata { + PyStage0RecipeMetadata { + inner: self.inner.recipe.clone(), + } + } + + #[getter] + fn build(&self) -> PyStage0Build { + PyStage0Build { + inner: self.inner.build.clone(), + } + } + + #[getter] + fn about(&self) -> PyStage0About { + PyStage0About { + inner: self.inner.about.clone(), + } + } + + #[getter] + fn outputs(&self, py: Python<'_>) -> PyResult>> { + let mut result = Vec::new(); + for output in &self.inner.outputs { + match output { + stage0::Output::Package(pkg) => { + let py_output = PyStage0PackageOutput { + inner: pkg.as_ref().clone(), + }; + result.push(py_output.into_py_any(py)?); + } + stage0::Output::Staging(staging) => { + let py_staging = PyStage0StagingOutput { + inner: *staging.clone(), + }; + result.push(py_staging.into_py_any(py)?); + } + } + } + Ok(result) + } + + fn to_dict(&self, py: Python<'_>) -> PyResult> { + let json_value = serde_json::to_value(&self.inner).map_err(RattlerBuildError::from)?; + pythonize::pythonize(py, &json_value) + .map(|obj| obj.into()) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)).into()) + } + + fn __repr__(&self) -> String { + format!("MultiOutputRecipe(outputs={})", self.inner.outputs.len()) + } +} + +/// Stage0 Package (full package with name and version) +#[pyclass(name = "Stage0Package")] +#[derive(Clone)] +pub struct PyStage0Package { + pub(crate) inner: stage0::Package, +} + +#[pymethods] +impl PyStage0Package { + /// Get the package name (may be a template string like "${{ name }}") + #[getter] + fn name(&self, py: Python<'_>) -> PyResult> { + let json_value = serde_json::to_value(&self.inner.name).map_err(RattlerBuildError::from)?; + pythonize::pythonize(py, &json_value) + .map(|obj| obj.into()) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)).into()) + } + + /// Get the package version (may be a template string like "${{ version }}") + #[getter] + fn version(&self, py: Python<'_>) -> PyResult> { + let json_value = + serde_json::to_value(&self.inner.version).map_err(RattlerBuildError::from)?; + pythonize::pythonize(py, &json_value) + .map(|obj| obj.into()) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)).into()) + } + + fn to_dict(&self, py: Python<'_>) -> PyResult> { + let json_value = serde_json::to_value(&self.inner).map_err(RattlerBuildError::from)?; + pythonize::pythonize(py, &json_value) + .map(|obj| obj.into()) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)).into()) + } +} + +/// Stage0 Package metadata (package with optional version for multi-output recipes) +#[pyclass(name = "Stage0PackageMetadata")] +#[derive(Clone)] +pub struct PyStage0PackageMetadata { + pub(crate) inner: stage0::PackageMetadata, +} + +#[pymethods] +impl PyStage0PackageMetadata { + /// Get the package name (may be a template string like "${{ name }}") + #[getter] + fn name(&self, py: Python<'_>) -> PyResult> { + let json_value = serde_json::to_value(&self.inner.name).map_err(RattlerBuildError::from)?; + pythonize::pythonize(py, &json_value) + .map(|obj| obj.into()) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)).into()) + } + + /// Get the package version (may be a template string like "${{ version }}", or None if inherited) + #[getter] + fn version(&self, py: Python<'_>) -> PyResult>> { + match &self.inner.version { + Some(version) => { + let json_value = serde_json::to_value(version).map_err(RattlerBuildError::from)?; + let py_value = pythonize::pythonize(py, &json_value) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)))?; + Ok(Some(py_value.into())) + } + None => Ok(None), + } + } + + fn to_dict(&self, py: Python<'_>) -> PyResult> { + let json_value = serde_json::to_value(&self.inner).map_err(RattlerBuildError::from)?; + pythonize::pythonize(py, &json_value) + .map(|obj| obj.into()) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)).into()) + } +} + +/// Stage0 Recipe metadata (for multi-output recipes) +#[pyclass(name = "Stage0RecipeMetadata")] +#[derive(Clone)] +pub struct PyStage0RecipeMetadata { + pub(crate) inner: stage0::RecipeMetadata, +} + +#[pymethods] +impl PyStage0RecipeMetadata { + fn to_dict(&self, py: Python<'_>) -> PyResult> { + let json_value = serde_json::to_value(&self.inner).map_err(RattlerBuildError::from)?; + pythonize::pythonize(py, &json_value) + .map(|obj| obj.into()) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)).into()) + } +} + +/// Stage0 Build configuration +#[pyclass(name = "Stage0Build")] +#[derive(Clone)] +pub struct PyStage0Build { + pub(crate) inner: stage0::Build, +} + +#[pymethods] +impl PyStage0Build { + /// Get the build number (may be a template) + #[getter] + fn number(&self, py: Python<'_>) -> PyResult> { + let json_value = + serde_json::to_value(&self.inner.number).map_err(RattlerBuildError::from)?; + pythonize::pythonize(py, &json_value) + .map(|obj| obj.into()) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)).into()) + } + + /// Get the build string (may be a template or None for auto-generated) + #[getter] + fn string(&self, py: Python<'_>) -> PyResult>> { + match &self.inner.string { + Some(string) => { + let json_value = serde_json::to_value(string).map_err(RattlerBuildError::from)?; + let py_value = pythonize::pythonize(py, &json_value) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)))?; + Ok(Some(py_value.into())) + } + None => Ok(None), + } + } + + /// Get the build script configuration + #[getter] + fn script(&self, py: Python<'_>) -> PyResult> { + let json_value = + serde_json::to_value(&self.inner.script).map_err(RattlerBuildError::from)?; + pythonize::pythonize(py, &json_value) + .map(|obj| obj.into()) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)).into()) + } + + /// Get the noarch type (may be a template or None) + #[getter] + fn noarch(&self, py: Python<'_>) -> PyResult>> { + match &self.inner.noarch { + Some(noarch) => { + let json_value = serde_json::to_value(noarch).map_err(RattlerBuildError::from)?; + let py_value = pythonize::pythonize(py, &json_value) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)))?; + Ok(Some(py_value.into())) + } + None => Ok(None), + } + } + + fn to_dict(&self, py: Python<'_>) -> PyResult> { + let json_value = serde_json::to_value(&self.inner).map_err(RattlerBuildError::from)?; + pythonize::pythonize(py, &json_value) + .map(|obj| obj.into()) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)).into()) + } +} + +/// Stage0 Requirements +#[pyclass(name = "Stage0Requirements")] +#[derive(Clone)] +pub struct PyStage0Requirements { + pub(crate) inner: stage0::Requirements, +} + +#[pymethods] +impl PyStage0Requirements { + /// Get build-time requirements (list of matchspecs or templates) + #[getter] + fn build(&self, py: Python<'_>) -> PyResult> { + let json_value = + serde_json::to_value(&self.inner.build).map_err(RattlerBuildError::from)?; + pythonize::pythonize(py, &json_value) + .map(|obj| obj.into()) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)).into()) + } + + /// Get host-time requirements (list of matchspecs or templates) + #[getter] + fn host(&self, py: Python<'_>) -> PyResult> { + let json_value = serde_json::to_value(&self.inner.host).map_err(RattlerBuildError::from)?; + pythonize::pythonize(py, &json_value) + .map(|obj| obj.into()) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)).into()) + } + + /// Get run-time requirements (list of matchspecs or templates) + #[getter] + fn run(&self, py: Python<'_>) -> PyResult> { + let json_value = serde_json::to_value(&self.inner.run).map_err(RattlerBuildError::from)?; + pythonize::pythonize(py, &json_value) + .map(|obj| obj.into()) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)).into()) + } + + /// Get run-time constraints (list of matchspecs or templates) + #[getter] + fn run_constraints(&self, py: Python<'_>) -> PyResult> { + let json_value = + serde_json::to_value(&self.inner.run_constraints).map_err(RattlerBuildError::from)?; + pythonize::pythonize(py, &json_value) + .map(|obj| obj.into()) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)).into()) + } + + fn to_dict(&self, py: Python<'_>) -> PyResult> { + let json_value = serde_json::to_value(&self.inner).map_err(RattlerBuildError::from)?; + pythonize::pythonize(py, &json_value) + .map(|obj| obj.into()) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)).into()) + } +} + +/// Stage0 About metadata +#[pyclass(name = "Stage0About")] +#[derive(Clone)] +pub struct PyStage0About { + pub(crate) inner: stage0::About, +} + +#[pymethods] +impl PyStage0About { + /// Get the homepage URL (may be a template or None) + #[getter] + fn homepage(&self, py: Python<'_>) -> PyResult>> { + match &self.inner.homepage { + Some(homepage) => { + let json_value = serde_json::to_value(homepage).map_err(RattlerBuildError::from)?; + let py_value = pythonize::pythonize(py, &json_value) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)))?; + Ok(Some(py_value.into())) + } + None => Ok(None), + } + } + + /// Get the license (may be a template or None) + #[getter] + fn license(&self, py: Python<'_>) -> PyResult>> { + match &self.inner.license { + Some(license) => { + let json_value = serde_json::to_value(license).map_err(RattlerBuildError::from)?; + let py_value = pythonize::pythonize(py, &json_value) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)))?; + Ok(Some(py_value.into())) + } + None => Ok(None), + } + } + + /// Get the license family (deprecated, may be a template or None) + #[getter] + fn license_family(&self, py: Python<'_>) -> PyResult>> { + match &self.inner.license_family { + Some(license_family) => { + let json_value = + serde_json::to_value(license_family).map_err(RattlerBuildError::from)?; + let py_value = pythonize::pythonize(py, &json_value) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)))?; + Ok(Some(py_value.into())) + } + None => Ok(None), + } + } + + /// Get the summary (may be a template or None) + #[getter] + fn summary(&self, py: Python<'_>) -> PyResult>> { + match &self.inner.summary { + Some(summary) => { + let json_value = serde_json::to_value(summary).map_err(RattlerBuildError::from)?; + let py_value = pythonize::pythonize(py, &json_value) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)))?; + Ok(Some(py_value.into())) + } + None => Ok(None), + } + } + + /// Get the description (may be a template or None) + #[getter] + fn description(&self, py: Python<'_>) -> PyResult>> { + match &self.inner.description { + Some(description) => { + let json_value = + serde_json::to_value(description).map_err(RattlerBuildError::from)?; + let py_value = pythonize::pythonize(py, &json_value) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)))?; + Ok(Some(py_value.into())) + } + None => Ok(None), + } + } + + /// Get the documentation URL (may be a template or None) + #[getter] + fn documentation(&self, py: Python<'_>) -> PyResult>> { + match &self.inner.documentation { + Some(documentation) => { + let json_value = + serde_json::to_value(documentation).map_err(RattlerBuildError::from)?; + let py_value = pythonize::pythonize(py, &json_value) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)))?; + Ok(Some(py_value.into())) + } + None => Ok(None), + } + } + + /// Get the repository URL (may be a template or None) + #[getter] + fn repository(&self, py: Python<'_>) -> PyResult>> { + match &self.inner.repository { + Some(repository) => { + let json_value = + serde_json::to_value(repository).map_err(RattlerBuildError::from)?; + let py_value = pythonize::pythonize(py, &json_value) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)))?; + Ok(Some(py_value.into())) + } + None => Ok(None), + } + } + + fn to_dict(&self, py: Python<'_>) -> PyResult> { + let json_value = serde_json::to_value(&self.inner).map_err(RattlerBuildError::from)?; + pythonize::pythonize(py, &json_value) + .map(|obj| obj.into()) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)).into()) + } +} + +/// Stage0 Package Output +#[pyclass(name = "Stage0PackageOutput")] +#[derive(Clone)] +pub struct PyStage0PackageOutput { + pub(crate) inner: stage0::PackageOutput, +} + +#[pymethods] +impl PyStage0PackageOutput { + #[getter] + fn package(&self) -> PyStage0PackageMetadata { + PyStage0PackageMetadata { + inner: self.inner.package.clone(), + } + } + + fn to_dict(&self, py: Python<'_>) -> PyResult> { + let json_value = serde_json::to_value(&self.inner).map_err(RattlerBuildError::from)?; + pythonize::pythonize(py, &json_value) + .map(|obj| obj.into()) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)).into()) + } +} + +/// Stage0 Staging Output +#[pyclass(name = "Stage0StagingOutput")] +#[derive(Clone)] +pub struct PyStage0StagingOutput { + pub(crate) inner: stage0::StagingOutput, +} + +#[pymethods] +impl PyStage0StagingOutput { + fn to_dict(&self, py: Python<'_>) -> PyResult> { + let json_value = serde_json::to_value(&self.inner).map_err(RattlerBuildError::from)?; + pythonize::pythonize(py, &json_value) + .map(|obj| obj.into()) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)).into()) + } +} + +pub fn register_stage0_module(py: Python<'_>, parent_module: &Bound<'_, PyModule>) -> PyResult<()> { + let stage0_module = PyModule::new(py, "stage0")?; + + stage0_module.add_class::()?; + stage0_module.add_class::()?; + stage0_module.add_class::()?; + stage0_module.add_class::()?; + stage0_module.add_class::()?; + stage0_module.add_class::()?; + stage0_module.add_class::()?; + stage0_module.add_class::()?; + stage0_module.add_class::()?; + stage0_module.add_class::()?; + stage0_module.add_class::()?; + + parent_module.add_submodule(&stage0_module)?; + Ok(()) +} diff --git a/py-rattler-build/src/stage1.rs b/py-rattler-build/src/stage1.rs new file mode 100644 index 000000000..a94a0d173 --- /dev/null +++ b/py-rattler-build/src/stage1.rs @@ -0,0 +1,407 @@ +// Python bindings for rattler-build stage1 types (evaluated recipe) + +use crate::error::RattlerBuildError; +use pyo3::prelude::*; +use pyo3::types::PyDict; +use rattler_build_recipe::stage1; + +/// Stage1 Recipe - The fully evaluated recipe ready for building +#[pyclass(name = "Stage1Recipe")] +#[derive(Clone)] +pub struct PyStage1Recipe { + pub(crate) inner: stage1::Recipe, +} + +#[pymethods] +impl PyStage1Recipe { + #[getter] + fn package(&self) -> PyStage1Package { + PyStage1Package { + inner: self.inner.package.clone(), + } + } + + #[getter] + fn build(&self) -> PyStage1Build { + PyStage1Build { + inner: self.inner.build.clone(), + } + } + + #[getter] + fn requirements(&self) -> PyStage1Requirements { + PyStage1Requirements { + inner: self.inner.requirements.clone(), + } + } + + #[getter] + fn about(&self) -> PyStage1About { + PyStage1About { + inner: self.inner.about.clone(), + } + } + + #[getter] + fn context(&self, py: Python<'_>) -> PyResult> { + let dict = PyDict::new(py); + for (key, value) in &self.inner.context { + let json_value = serde_json::to_value(value).map_err(RattlerBuildError::from)?; + let py_value = pythonize::pythonize(py, &json_value) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)))?; + dict.set_item(key, py_value)?; + } + Ok(dict.into()) + } + + #[getter] + fn used_variant(&self, py: Python<'_>) -> PyResult> { + let dict = PyDict::new(py); + for (key, value) in &self.inner.used_variant { + let json_value = serde_json::to_value(value).map_err(RattlerBuildError::from)?; + let py_value = pythonize::pythonize(py, &json_value) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)))?; + dict.set_item(key.normalize(), py_value)?; + } + Ok(dict.into()) + } + + #[getter] + fn sources(&self) -> Vec { + self.inner + .source + .iter() + .map(|s| PyStage1Source { inner: s.clone() }) + .collect() + } + + #[getter] + fn staging_caches(&self) -> Vec { + self.inner + .staging_caches + .iter() + .map(|s| PyStage1StagingCache { inner: s.clone() }) + .collect() + } + + #[getter] + fn inherits_from(&self, py: Python<'_>) -> PyResult>> { + if let Some(ref inherits) = self.inner.inherits_from { + let json_value = serde_json::to_value(inherits).map_err(RattlerBuildError::from)?; + Ok(Some( + pythonize::pythonize(py, &json_value) + .map(|obj| obj.into()) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)))?, + )) + } else { + Ok(None) + } + } + + /// Convert to Python dictionary + fn to_dict(&self, py: Python<'_>) -> PyResult> { + let json_value = serde_json::to_value(&self.inner).map_err(RattlerBuildError::from)?; + pythonize::pythonize(py, &json_value) + .map(|obj| obj.into()) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)).into()) + } + + fn __repr__(&self) -> String { + format!( + "Stage1Recipe(package='{}', version='{}')", + self.inner.package.name.as_normalized(), + self.inner.package.version + ) + } +} + +/// Stage1 Package metadata +#[pyclass(name = "Stage1Package")] +#[derive(Clone)] +pub struct PyStage1Package { + pub(crate) inner: stage1::Package, +} + +#[pymethods] +impl PyStage1Package { + #[getter] + fn name(&self) -> String { + self.inner.name.as_normalized().to_string() + } + + #[getter] + fn version(&self) -> String { + self.inner.version.to_string() + } + + fn to_dict(&self, py: Python<'_>) -> PyResult> { + let json_value = serde_json::to_value(&self.inner).map_err(RattlerBuildError::from)?; + pythonize::pythonize(py, &json_value) + .map(|obj| obj.into()) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)).into()) + } + + fn __repr__(&self) -> String { + format!( + "Stage1Package(name='{}', version='{}')", + self.inner.name.as_normalized(), + self.inner.version + ) + } +} + +/// Stage1 Build configuration +#[pyclass(name = "Stage1Build")] +#[derive(Clone)] +pub struct PyStage1Build { + pub(crate) inner: stage1::Build, +} + +#[pymethods] +impl PyStage1Build { + #[getter] + fn number(&self) -> u64 { + self.inner.number + } + + #[getter] + fn string(&self, py: Python<'_>) -> PyResult> { + let json_value = + serde_json::to_value(&self.inner.string).map_err(RattlerBuildError::from)?; + pythonize::pythonize(py, &json_value) + .map(|obj| obj.into()) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)).into()) + } + + #[getter] + fn script(&self, py: Python<'_>) -> PyResult> { + let json_value = + serde_json::to_value(&self.inner.script).map_err(RattlerBuildError::from)?; + pythonize::pythonize(py, &json_value) + .map(|obj| obj.into()) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)).into()) + } + + #[getter] + fn noarch(&self, py: Python<'_>) -> PyResult>> { + if let Some(ref noarch) = self.inner.noarch { + let json_value = serde_json::to_value(noarch).map_err(RattlerBuildError::from)?; + Ok(Some( + pythonize::pythonize(py, &json_value) + .map(|obj| obj.into()) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)))?, + )) + } else { + Ok(None) + } + } + + fn to_dict(&self, py: Python<'_>) -> PyResult> { + let json_value = serde_json::to_value(&self.inner).map_err(RattlerBuildError::from)?; + pythonize::pythonize(py, &json_value) + .map(|obj| obj.into()) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)).into()) + } + + fn __repr__(&self) -> String { + format!("Stage1Build(number={})", self.inner.number) + } +} + +/// Stage1 Requirements +#[pyclass(name = "Stage1Requirements")] +#[derive(Clone)] +pub struct PyStage1Requirements { + pub(crate) inner: stage1::Requirements, +} + +#[pymethods] +impl PyStage1Requirements { + #[getter] + fn build(&self, py: Python<'_>) -> PyResult>> { + self.inner + .build + .iter() + .map(|dep| { + let json_value = serde_json::to_value(dep).map_err(RattlerBuildError::from)?; + pythonize::pythonize(py, &json_value) + .map(|obj| obj.into()) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)).into()) + }) + .collect() + } + + #[getter] + fn host(&self, py: Python<'_>) -> PyResult>> { + self.inner + .host + .iter() + .map(|dep| { + let json_value = serde_json::to_value(dep).map_err(RattlerBuildError::from)?; + pythonize::pythonize(py, &json_value) + .map(|obj| obj.into()) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)).into()) + }) + .collect() + } + + #[getter] + fn run(&self, py: Python<'_>) -> PyResult>> { + self.inner + .run + .iter() + .map(|dep| { + let json_value = serde_json::to_value(dep).map_err(RattlerBuildError::from)?; + pythonize::pythonize(py, &json_value) + .map(|obj| obj.into()) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)).into()) + }) + .collect() + } + + fn to_dict(&self, py: Python<'_>) -> PyResult> { + let json_value = serde_json::to_value(&self.inner).map_err(RattlerBuildError::from)?; + pythonize::pythonize(py, &json_value) + .map(|obj| obj.into()) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)).into()) + } + + fn __repr__(&self) -> String { + format!( + "Stage1Requirements(build={}, host={}, run={})", + self.inner.build.len(), + self.inner.host.len(), + self.inner.run.len() + ) + } +} + +/// Stage1 About metadata +#[pyclass(name = "Stage1About")] +#[derive(Clone)] +pub struct PyStage1About { + pub(crate) inner: stage1::About, +} + +#[pymethods] +impl PyStage1About { + #[getter] + fn homepage(&self) -> Option { + self.inner.homepage.as_ref().map(|u| u.to_string()) + } + + #[getter] + fn repository(&self) -> Option { + self.inner.repository.as_ref().map(|u| u.to_string()) + } + + #[getter] + fn documentation(&self) -> Option { + self.inner.documentation.as_ref().map(|u| u.to_string()) + } + + #[getter] + fn license(&self) -> Option { + self.inner + .license + .as_ref() + .map(|l| l.0.as_ref().to_string()) + } + + #[getter] + fn summary(&self) -> Option { + self.inner.summary.clone() + } + + #[getter] + fn description(&self) -> Option { + self.inner.description.clone() + } + + fn to_dict(&self, py: Python<'_>) -> PyResult> { + let json_value = serde_json::to_value(&self.inner).map_err(RattlerBuildError::from)?; + pythonize::pythonize(py, &json_value) + .map(|obj| obj.into()) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)).into()) + } + + fn __repr__(&self) -> String { + format!( + "Stage1About(license={:?}, summary={:?})", + self.license(), + self.summary() + ) + } +} + +/// Stage1 Source +#[pyclass(name = "Stage1Source")] +#[derive(Clone)] +pub struct PyStage1Source { + pub(crate) inner: stage1::Source, +} + +#[pymethods] +impl PyStage1Source { + fn to_dict(&self, py: Python<'_>) -> PyResult> { + let json_value = serde_json::to_value(&self.inner).map_err(RattlerBuildError::from)?; + pythonize::pythonize(py, &json_value) + .map(|obj| obj.into()) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)).into()) + } +} + +/// Stage1 Staging Cache +#[pyclass(name = "Stage1StagingCache")] +#[derive(Clone)] +pub struct PyStage1StagingCache { + pub(crate) inner: stage1::StagingCache, +} + +#[pymethods] +impl PyStage1StagingCache { + #[getter] + fn name(&self) -> String { + self.inner.name.clone() + } + + #[getter] + fn build(&self) -> PyStage1Build { + PyStage1Build { + inner: self.inner.build.clone(), + } + } + + #[getter] + fn requirements(&self) -> PyStage1Requirements { + PyStage1Requirements { + inner: self.inner.requirements.clone(), + } + } + + fn to_dict(&self, py: Python<'_>) -> PyResult> { + let json_value = serde_json::to_value(&self.inner).map_err(RattlerBuildError::from)?; + pythonize::pythonize(py, &json_value) + .map(|obj| obj.into()) + .map_err(|e| RattlerBuildError::RecipeParse(format!("{}", e)).into()) + } + + fn __repr__(&self) -> String { + format!("Stage1StagingCache(name='{}')", self.inner.name) + } +} + +pub fn register_stage1_module(py: Python<'_>, parent_module: &Bound<'_, PyModule>) -> PyResult<()> { + let stage1_module = PyModule::new(py, "stage1")?; + + stage1_module.add_class::()?; + stage1_module.add_class::()?; + stage1_module.add_class::()?; + stage1_module.add_class::()?; + stage1_module.add_class::()?; + stage1_module.add_class::()?; + stage1_module.add_class::()?; + + parent_module.add_submodule(&stage1_module)?; + Ok(()) +} diff --git a/py-rattler-build/src/tool_config.rs b/py-rattler-build/src/tool_config.rs new file mode 100644 index 000000000..13457a6ea --- /dev/null +++ b/py-rattler-build/src/tool_config.rs @@ -0,0 +1,237 @@ +use clap::ValueEnum; +use pyo3::prelude::*; +use rattler_build::tool_configuration::{ + Configuration, ContinueOnFailure, SkipExisting, TestStrategy, +}; +use rattler_conda_types::Platform; +use rattler_solve::ChannelPriority; + +use crate::error::RattlerBuildError; + +/// Python wrapper for ToolConfiguration +#[pyclass(name = "ToolConfiguration")] +#[derive(Clone)] +pub struct PyToolConfiguration { + pub(crate) inner: Configuration, +} + +#[pymethods] +impl PyToolConfiguration { + /// Create a new tool configuration with default settings + #[new] + #[pyo3(signature = ( + keep_build=false, + compression_threads=None, + io_concurrency_limit=None, + test_strategy=None, + skip_existing=None, + continue_on_failure=false, + noarch_build_platform=None, + channel_priority=None, + allow_insecure_host=None, + error_prefix_in_binary=false, + allow_symlinks_on_windows=false, + use_zstd=true, + use_bz2=true, + use_sharded=true, + use_jlap=false + ))] + #[allow(clippy::too_many_arguments)] + fn new( + keep_build: bool, + compression_threads: Option, + io_concurrency_limit: Option, + test_strategy: Option, + skip_existing: Option, + continue_on_failure: bool, + noarch_build_platform: Option, + channel_priority: Option, + allow_insecure_host: Option>, + error_prefix_in_binary: bool, + allow_symlinks_on_windows: bool, + use_zstd: bool, + use_bz2: bool, + use_sharded: bool, + use_jlap: bool, + ) -> PyResult { + let channel_priority = channel_priority + .map(|c| match c.to_lowercase().as_str() { + "strict" => Ok(ChannelPriority::Strict), + "disabled" => Ok(ChannelPriority::Disabled), + _ => Err(RattlerBuildError::Other(format!( + "Invalid channel priority: {}. Must be 'strict' or 'disabled'", + c + ))), + }) + .transpose()? + .unwrap_or(ChannelPriority::Strict); + + let test_strategy = test_strategy + .map(|t| { + TestStrategy::from_str(&t, false) + .map_err(|e| RattlerBuildError::Other(format!("Invalid test strategy: {}", e))) + }) + .transpose()? + .unwrap_or(TestStrategy::Skip); + + let skip_existing = skip_existing + .map(|s| { + SkipExisting::from_str(&s, false) + .map_err(|e| RattlerBuildError::Other(format!("Invalid skip existing: {}", e))) + }) + .transpose()? + .unwrap_or(SkipExisting::None); + + let noarch_build_platform = noarch_build_platform + .map(|p| { + p.parse::() + .map_err(|e| RattlerBuildError::Other(format!("Invalid platform: {}", e))) + }) + .transpose()?; + + let config = rattler_config::config::ConfigBase::<()>::default(); + + let mut builder = Configuration::builder() + .with_keep_build(keep_build) + .with_test_strategy(test_strategy) + .with_skip_existing(skip_existing) + .with_continue_on_failure(ContinueOnFailure::from(continue_on_failure)) + .with_channel_priority(channel_priority) + .with_error_prefix_in_binary(error_prefix_in_binary) + .with_allow_symlinks_on_windows(allow_symlinks_on_windows) + .with_zstd_repodata_enabled(use_zstd) + .with_bz2_repodata_enabled(use_bz2) + .with_sharded_repodata_enabled(use_sharded) + .with_jlap_enabled(use_jlap) + .with_channel_config(config.channel_config); + + if let Some(threads) = compression_threads { + builder = builder.with_compression_threads(Some(threads)); + } + + if let Some(limit) = io_concurrency_limit { + builder = builder.with_io_concurrency_limit(Some(limit)); + } + + if let Some(platform) = noarch_build_platform { + builder = builder.with_noarch_build_platform(Some(platform)); + } + + if let Some(hosts) = allow_insecure_host { + builder = builder.with_allow_insecure_host(Some(hosts)); + } + + Ok(Self { + inner: builder.finish(), + }) + } + + /// Whether to keep the build directory after the build is done + #[getter] + fn keep_build(&self) -> bool { + self.inner.no_clean + } + + /// Set whether to keep the build directory + #[setter] + fn set_keep_build(&mut self, value: bool) { + self.inner.no_clean = value; + } + + /// The test strategy to use + #[getter] + fn test_strategy(&self) -> String { + format!("{:?}", self.inner.test_strategy) + } + + /// Whether to skip existing packages + #[getter] + fn skip_existing(&self) -> String { + format!("{:?}", self.inner.skip_existing) + } + + /// Whether to continue building on failure + #[getter] + fn continue_on_failure(&self) -> bool { + matches!(self.inner.continue_on_failure, ContinueOnFailure::Yes) + } + + /// The channel priority to use in solving + #[getter] + fn channel_priority(&self) -> String { + format!("{:?}", self.inner.channel_priority) + } + + /// Whether to use zstd compression + #[getter] + fn use_zstd(&self) -> bool { + self.inner.use_zstd + } + + /// Whether to use bzip2 compression + #[getter] + fn use_bz2(&self) -> bool { + self.inner.use_bz2 + } + + /// Whether to use sharded repodata + #[getter] + fn use_sharded(&self) -> bool { + self.inner.use_sharded + } + + /// Whether to use JLAP + #[getter] + fn use_jlap(&self) -> bool { + self.inner.use_jlap + } + + /// Compression threads + #[getter] + fn compression_threads(&self) -> Option { + self.inner.compression_threads + } + + /// IO concurrency limit + #[getter] + fn io_concurrency_limit(&self) -> Option { + self.inner.io_concurrency_limit + } + + /// List of hosts for which SSL certificate verification should be skipped + #[getter] + fn allow_insecure_host(&self) -> Option> { + self.inner.allow_insecure_host.clone() + } + + /// Whether to error if the host prefix is detected in binary files + #[getter] + fn error_prefix_in_binary(&self) -> bool { + self.inner.error_prefix_in_binary + } + + /// Whether to allow symlinks in packages on Windows + #[getter] + fn allow_symlinks_on_windows(&self) -> bool { + self.inner.allow_symlinks_on_windows + } + + fn __repr__(&self) -> String { + format!( + "ToolConfiguration(keep_build={}, test_strategy={:?}, skip_existing={:?}, continue_on_failure={}, channel_priority={:?})", + self.inner.no_clean, + self.inner.test_strategy, + self.inner.skip_existing, + matches!(self.inner.continue_on_failure, ContinueOnFailure::Yes), + self.inner.channel_priority + ) + } +} + +/// Register the tool_config module with Python +pub fn register_tool_config_module(py: Python<'_>, parent: &Bound<'_, PyModule>) -> PyResult<()> { + let m = PyModule::new(py, "tool_config")?; + m.add_class::()?; + parent.add_submodule(&m)?; + Ok(()) +} diff --git a/py-rattler-build/src/tracing_subscriber.rs b/py-rattler-build/src/tracing_subscriber.rs new file mode 100644 index 000000000..8d23ab060 --- /dev/null +++ b/py-rattler-build/src/tracing_subscriber.rs @@ -0,0 +1,92 @@ +use pyo3::prelude::*; +use tracing::{Level, Subscriber}; +use tracing_subscriber::Layer; +use tracing_subscriber::layer::{Context, SubscriberExt}; + +use crate::progress_callback::PyProgressCallback; + +/// A tracing layer that forwards log events to a Python progress callback +pub struct PythonTracingLayer { + callback: PyProgressCallback, +} + +impl PythonTracingLayer { + pub fn new(callback: PyProgressCallback) -> Self { + Self { callback } + } +} + +impl Layer for PythonTracingLayer +where + S: Subscriber + for<'lookup> tracing_subscriber::registry::LookupSpan<'lookup>, +{ + fn on_event(&self, event: &tracing::Event<'_>, _ctx: Context<'_, S>) { + let metadata = event.metadata(); + let level = match *metadata.level() { + Level::ERROR => "error", + Level::WARN => "warn", + Level::INFO => "info", + Level::DEBUG => "debug", + Level::TRACE => "trace", + }; + + // Extract the message from the event + let mut visitor = MessageVisitor::default(); + event.record(&mut visitor); + + // Extract span name if available + let span = _ctx.event_span(event).map(|s| s.name().to_string()); + + // Forward to Python callback + self.callback + .on_log(level, &visitor.message, span.as_deref()); + } +} + +/// Visitor to extract the message from a tracing event +#[derive(Default)] +struct MessageVisitor { + message: String, +} + +impl tracing::field::Visit for MessageVisitor { + fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) { + if field.name() == "message" { + self.message = format!("{:?}", value); + // Remove quotes if present + if self.message.starts_with('"') && self.message.ends_with('"') { + self.message = self.message[1..self.message.len() - 1].to_string(); + } + } + } + + fn record_str(&mut self, field: &tracing::field::Field, value: &str) { + if field.name() == "message" { + self.message = value.to_string(); + } + } +} + +/// Install a Python tracing subscriber for the duration of the build +pub fn with_python_tracing(callback: Option>, f: F) -> R +where + F: FnOnce() -> R, +{ + if let Some(py_callback) = callback { + let callback = PyProgressCallback::new(py_callback); + let layer = PythonTracingLayer::new(callback); + + // Create a subscriber with the Python layer and filter + // Only capture info/warn/error, not debug/trace which are too noisy + use tracing_subscriber::filter::LevelFilter; + let subscriber = tracing_subscriber::registry() + .with(layer) + .with(LevelFilter::INFO); + + // Set this subscriber for the duration of the closure + tracing::subscriber::with_default(subscriber, f) + } else { + // No callback provided, just run the function + f() + } +} diff --git a/py-rattler-build/src/upload.rs b/py-rattler-build/src/upload.rs new file mode 100644 index 000000000..eb6e59255 --- /dev/null +++ b/py-rattler-build/src/upload.rs @@ -0,0 +1,141 @@ +use std::path::PathBuf; + +use pyo3::prelude::*; +use rattler_build::tool_configuration; +use rattler_upload::upload; +use rattler_upload::upload::opt::{ + AnacondaData, ArtifactoryData, CondaForgeData, PrefixData, QuetzData, +}; +use url::Url; + +use crate::{error::RattlerBuildError, run_async_task}; + +#[pyfunction] +#[pyo3(signature = (package_files, url, channels, api_key, auth_file))] +pub fn upload_package_to_quetz_py( + package_files: Vec, + url: String, + channels: String, + api_key: Option, + auth_file: Option, +) -> PyResult<()> { + let store = tool_configuration::get_auth_store(auth_file).map_err(RattlerBuildError::Auth)?; + + let url = Url::parse(&url).map_err(RattlerBuildError::from)?; + let quetz_data = QuetzData::new(url, channels, api_key); + + run_async_task(async { + upload::upload_package_to_quetz(&store, &package_files, quetz_data).await?; + Ok(()) + }) +} + +#[pyfunction] +#[pyo3(signature = (package_files, url, channels, token, auth_file))] +pub fn upload_package_to_artifactory_py( + package_files: Vec, + url: String, + channels: String, + token: Option, + auth_file: Option, +) -> PyResult<()> { + let store = tool_configuration::get_auth_store(auth_file).map_err(RattlerBuildError::Auth)?; + let url = Url::parse(&url).map_err(RattlerBuildError::from)?; + let artifactory_data = ArtifactoryData::new(url, channels, token); + + run_async_task(async { + upload::upload_package_to_artifactory(&store, &package_files, artifactory_data).await?; + Ok(()) + }) +} + +#[pyfunction] +#[pyo3(signature = (package_files, url, channel, api_key, auth_file, skip_existing, attestation_file=None,))] +pub fn upload_package_to_prefix_py( + package_files: Vec, + url: String, + channel: String, + api_key: Option, + auth_file: Option, + skip_existing: bool, + attestation_file: Option, +) -> PyResult<()> { + let store = tool_configuration::get_auth_store(auth_file).map_err(RattlerBuildError::Auth)?; + + let url = Url::parse(&url).map_err(RattlerBuildError::from)?; + let prefix_data = PrefixData::new(url, channel, api_key, attestation_file, skip_existing); + + run_async_task(async { + upload::upload_package_to_prefix(&store, &package_files, prefix_data).await?; + Ok(()) + }) +} + +#[pyfunction] +#[pyo3(signature = (package_files, owner, channel, api_key, url, force, auth_file))] +pub fn upload_package_to_anaconda_py( + package_files: Vec, + owner: String, + channel: Option>, + api_key: Option, + url: Option, + force: bool, + auth_file: Option, +) -> PyResult<()> { + let store = tool_configuration::get_auth_store(auth_file).map_err(RattlerBuildError::Auth)?; + + let url = url + .map(|u| Url::parse(&u)) + .transpose() + .map_err(RattlerBuildError::from)?; + let anaconda_data = AnacondaData::new(owner, channel, api_key, url, force); + + run_async_task(async { + upload::upload_package_to_anaconda(&store, &package_files, anaconda_data).await?; + Ok(()) + }) +} + +#[pyfunction] +#[pyo3(signature = (package_files, staging_token, feedstock, feedstock_token, staging_channel, anaconda_url, validation_endpoint, provider, dry_run))] +#[allow(clippy::too_many_arguments)] +pub fn upload_packages_to_conda_forge_py( + package_files: Vec, + staging_token: String, + feedstock: String, + feedstock_token: String, + staging_channel: Option, + anaconda_url: Option, + validation_endpoint: Option, + provider: Option, + dry_run: bool, +) -> PyResult<()> { + let anaconda_url = anaconda_url + .map(|u| Url::parse(&u)) + .transpose() + .map_err(|e| RattlerBuildError::Other(format!("Error parsing anaconda_url: {e}")))?; + + let validation_endpoint = validation_endpoint + .map(|u| Url::parse(&u)) + .transpose() + .map_err(|e| { + RattlerBuildError::Other(format!("Error parsing validation_endpoint: {e}",)) + })?; + + let conda_forge_data = CondaForgeData::new( + staging_token, + feedstock, + feedstock_token, + staging_channel, + anaconda_url, + validation_endpoint, + provider, + dry_run, + ); + + run_async_task(async { + upload::conda_forge::upload_packages_to_conda_forge(&package_files, conda_forge_data) + .await?; + Ok(()) + }) +} diff --git a/py-rattler-build/src/variant_config.rs b/py-rattler-build/src/variant_config.rs new file mode 100644 index 000000000..efdff9f4d --- /dev/null +++ b/py-rattler-build/src/variant_config.rs @@ -0,0 +1,226 @@ +// Python bindings for VariantConfig + +use crate::error::RattlerBuildError; +use crate::jinja_config::PyJinjaConfig; +use pyo3::prelude::*; +use pyo3::types::PyDict; +use rattler_build_jinja::Variable; +use rattler_build_types::NormalizedKey; +use rattler_build_variant_config::config::VariantConfig; +use std::path::PathBuf; + +/// Python wrapper for VariantConfig +#[pyclass(name = "VariantConfig")] +#[derive(Clone)] +pub struct PyVariantConfig { + pub(crate) inner: VariantConfig, +} + +#[pymethods] +impl PyVariantConfig { + /// Create a new empty VariantConfig + #[new] + fn new() -> Self { + PyVariantConfig { + inner: VariantConfig::default(), + } + } + + /// Load VariantConfig from a YAML file (variants.yaml format) + #[staticmethod] + fn from_file(path: PathBuf) -> PyResult { + let config = VariantConfig::from_file(&path) + .map_err(|e| RattlerBuildError::Variant(format!("{:?}", e)))?; + Ok(PyVariantConfig { inner: config }) + } + + /// Load VariantConfig from a YAML file with a JinjaConfig context (variants.yaml format) + /// + /// This allows evaluation of conditionals and templates in the variant file. + /// The `jinja_config` provides platform information and other context needed for evaluation. + #[staticmethod] + fn from_file_with_context(path: PathBuf, jinja_config: &PyJinjaConfig) -> PyResult { + let config = VariantConfig::from_file_with_context(&path, &jinja_config.inner) + .map_err(|e| RattlerBuildError::Variant(format!("{:?}", e)))?; + Ok(PyVariantConfig { inner: config }) + } + + /// Load VariantConfig from a conda_build_config.yaml file + /// + /// This supports the legacy conda-build format with `# [selector]` syntax. + /// Selectors are evaluated using the provided JinjaConfig. + #[staticmethod] + fn from_conda_build_config(path: PathBuf, jinja_config: &PyJinjaConfig) -> PyResult { + let config = rattler_build_variant_config::conda_build_config::load_conda_build_config( + &path, + &jinja_config.inner, + ) + .map_err(|e| RattlerBuildError::Variant(format!("{:?}", e)))?; + Ok(PyVariantConfig { inner: config }) + } + + /// Load VariantConfig from a YAML string (variants.yaml format) + #[staticmethod] + fn from_yaml(yaml: &str) -> PyResult { + let config = VariantConfig::from_yaml_str(yaml) + .map_err(|e| RattlerBuildError::Variant(format!("{:?}", e)))?; + Ok(PyVariantConfig { inner: config }) + } + + /// Load VariantConfig from a YAML string with a JinjaConfig context (variants.yaml format) + /// + /// This allows evaluation of conditionals and templates in the variant YAML. + /// The `jinja_config` provides platform information and other context needed for evaluation. + #[staticmethod] + fn from_yaml_with_context(yaml: &str, jinja_config: &PyJinjaConfig) -> PyResult { + let config = VariantConfig::from_yaml_str_with_context(yaml, &jinja_config.inner) + .map_err(|e| RattlerBuildError::Variant(format!("{:?}", e)))?; + Ok(PyVariantConfig { inner: config }) + } + + /// Get all variant keys + fn keys(&self) -> Vec { + self.inner.keys().map(|k| k.normalize()).collect() + } + + /// Get zip_keys - groups of keys that should be zipped together + #[getter] + fn zip_keys(&self) -> Option>> { + self.inner.zip_keys.as_ref().map(|zip_keys| { + zip_keys + .iter() + .map(|group| group.iter().map(|k| k.normalize()).collect()) + .collect() + }) + } + + /// Set zip_keys - groups of keys that should be zipped together + #[setter] + fn set_zip_keys(&mut self, zip_keys: Option>>) { + self.inner.zip_keys = zip_keys.map(|zk| { + zk.into_iter() + .map(|group| group.into_iter().map(NormalizedKey::from).collect()) + .collect() + }); + } + + /// Get values for a specific variant key + fn get_values(&self, key: &str, py: Python<'_>) -> PyResult>>> { + let normalized_key = NormalizedKey::from(key); + if let Some(values) = self.inner.get(&normalized_key) { + let py_values = values + .iter() + .map(|v| { + let json_value = serde_json::to_value(v).map_err(RattlerBuildError::from)?; + pythonize::pythonize(py, &json_value) + .map(|obj| obj.into()) + .map_err(|e| RattlerBuildError::Variant(format!("{}", e)).into()) + }) + .collect::>>()?; + Ok(Some(py_values)) + } else { + Ok(None) + } + } + + /// Set values for a variant key + fn set_values( + &mut self, + key: &str, + values: Vec>, + _py: Python<'_>, + ) -> PyResult<()> { + let normalized_key = NormalizedKey::from(key); + let variables: Vec = values + .iter() + .map(|v| { + let json_value: serde_json::Value = pythonize::depythonize(v) + .map_err(|e| RattlerBuildError::Variant(format!("{}", e)))?; + + match &json_value { + serde_json::Value::String(s) => Ok(Variable::from_string(s)), + serde_json::Value::Bool(b) => Ok(Variable::from(*b)), + serde_json::Value::Number(n) => { + if let Some(i) = n.as_i64() { + Ok(Variable::from(i)) + } else { + Ok(Variable::from_string(&n.to_string())) + } + } + _ => { + Err(RattlerBuildError::Variant("Unsupported value type".to_string()).into()) + } + } + }) + .collect::>()?; + + self.inner.insert(normalized_key, variables); + Ok(()) + } + + /// Get all variants as a dictionary + fn to_dict(&self, py: Python<'_>) -> PyResult> { + let dict = PyDict::new(py); + for key in self.inner.keys() { + if let Some(values) = self.inner.get(key) { + let py_values: Vec> = values + .iter() + .map(|v| { + let json_value = + serde_json::to_value(v).map_err(RattlerBuildError::from)?; + pythonize::pythonize(py, &json_value) + .map(|obj| obj.into()) + .map_err(|e| RattlerBuildError::Variant(format!("{}", e)).into()) + }) + .collect::>>()?; + dict.set_item(key.normalize(), py_values)?; + } + } + Ok(dict.into()) + } + + /// Merge another VariantConfig into this one + fn merge(&mut self, other: &PyVariantConfig) { + self.inner.merge(other.inner.clone()); + } + + /// Generate combinations of variants + fn combinations(&self, py: Python<'_>) -> PyResult>> { + // Use all keys for combinations + let used_vars = self.inner.keys().cloned().collect(); + let combos = self + .inner + .combinations(&used_vars) + .map_err(|e| RattlerBuildError::Variant(format!("{:?}", e)))?; + combos + .into_iter() + .map(|combo| { + let dict = PyDict::new(py); + for (key, value) in combo { + let json_value = + serde_json::to_value(&value).map_err(RattlerBuildError::from)?; + let py_value = pythonize::pythonize(py, &json_value) + .map_err(|e| RattlerBuildError::Variant(format!("{}", e)))?; + dict.set_item(key.normalize(), py_value)?; + } + Ok(dict.into()) + }) + .collect() + } + + fn __len__(&self) -> usize { + self.inner.keys().count() + } + + fn __repr__(&self) -> String { + format!("VariantConfig(keys={})", self.inner.keys().count()) + } +} + +pub fn register_variant_config_module( + _py: Python<'_>, + parent_module: &Bound<'_, PyModule>, +) -> PyResult<()> { + parent_module.add_class::()?; + Ok(()) +} diff --git a/py-rattler-build/tests/data/recipes/test-package/recipe.yaml b/py-rattler-build/tests/data/recipes/comprehensive-test/recipe.yaml similarity index 100% rename from py-rattler-build/tests/data/recipes/test-package/recipe.yaml rename to py-rattler-build/tests/data/recipes/comprehensive-test/recipe.yaml diff --git a/py-rattler-build/tests/data/recipes/with-staging.yaml b/py-rattler-build/tests/data/recipes/with-staging.yaml new file mode 100644 index 000000000..c21ed8468 --- /dev/null +++ b/py-rattler-build/tests/data/recipes/with-staging.yaml @@ -0,0 +1,77 @@ +schema_version: 1 + +context: + version: "1.2.3" + +recipe: + name: example-package + version: ${{ version }} + +source: + url: https://example.com/mixed-inheritance-2.0.0.tar.gz + sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 + +build: + number: 0 + script: + - if: unix + then: | + echo "Top-level build script" + mkdir -p $PREFIX/share + echo "data.txt" > $PREFIX/share/data.txt + - if: win + then: | + echo Top-level build script + mkdir %PREFIX%\share + echo data.txt > %PREFIX%\share\data.txt + dynamic_linking: + rpaths: + - lib/ + - abc/ + +about: + summary: Project with mixed inheritance + license: Apache-2.0 + license_file: LICENSE + repository: https://github.com/foobar/repo + +outputs: + # Staging output for compiled code + - staging: + name: compile-stage + requirements: + build: + - ${{ compiler('cxx') }} + - cmake + build: + script: + - if: unix + then: | + mkdir -p $PREFIX/lib + echo "compiled.so" > $PREFIX/lib/compiled.so + - if: win + then: | + mkdir %PREFIX%\lib + echo compiled.dll > %PREFIX%\lib\compiled.dll + + # Package that inherits from staging (gets compiled library) + - package: + name: mixed-compiled + + inherit: compile-stage + build: + files: + - lib/** + about: + summary: Compiled library package + + # Package that inherits from top-level (gets data files from top-level build) + - package: + name: mixed-data + version: "2.0.0" + inherit: null # Explicitly inherit from top-level + build: + files: + - share/** + about: + summary: Data files package diff --git a/py-rattler-build/tests/unit/test_basic.py b/py-rattler-build/tests/unit/test_basic.py index 590e5355a..962985c43 100644 --- a/py-rattler-build/tests/unit/test_basic.py +++ b/py-rattler-build/tests/unit/test_basic.py @@ -3,7 +3,6 @@ import rattler_build import tomli as tomllib import shutil -import pytest def test_version_match_parent_cargo() -> None: @@ -36,42 +35,3 @@ def test_test(tmp_path: Path, recipes_dir: Path) -> None: for conda_file in output_dir.glob("**/*.conda"): rattler_build.test_package(conda_file) assert output_dir.joinpath("noarch").is_dir() - - -def test_upload_to_quetz_no_token() -> None: - url = "https://quetz.io" - channel = "some_channel" - with pytest.raises(rattler_build.RattlerBuildError, match="No quetz api key was given"): - rattler_build.upload_package_to_quetz([], url, channel) - - -def test_upload_to_artifactory_no_token() -> None: - url = "https://artifactory.io" - channel = "some_channel" - with pytest.raises(rattler_build.RattlerBuildError, match="No bearer token was given"): - rattler_build.upload_package_to_artifactory([], url, channel) - - -def test_upload_to_prefix_no_token() -> None: - url = "https://prefix.dev" - channel = "some_channel" - with pytest.raises(rattler_build.RattlerBuildError, match="No prefix.dev api key was given"): - rattler_build.upload_package_to_prefix([], url, channel) - - -def test_upload_to_anaconda_no_token() -> None: - url = "https://anaconda.org" - with pytest.raises(rattler_build.RattlerBuildError, match="No anaconda.org api key was given"): - rattler_build.upload_package_to_anaconda([], url) - - -def test_upload_packages_to_conda_forge_invalid_url() -> None: - staging_token = "xxx" - feedstock = "some_feedstock" - feedstock_token = "xxx" - anaconda_url = "invalid-url" - - with pytest.raises(rattler_build.RattlerBuildError, match="relative URL without a base"): - rattler_build.upload_packages_to_conda_forge( - [], staging_token, feedstock, feedstock_token, anaconda_url=anaconda_url - ) diff --git a/py-rattler-build/tests/unit/test_error_handling.py b/py-rattler-build/tests/unit/test_error_handling.py new file mode 100644 index 000000000..ef73dc3b0 --- /dev/null +++ b/py-rattler-build/tests/unit/test_error_handling.py @@ -0,0 +1,226 @@ +""" +Tests for error handling and type validation in the recipe pipeline. + +These tests ensure that we get clear, helpful error messages when things go wrong. +""" + +import pytest +from rattler_build.stage0 import Recipe as Stage0Recipe, SingleOutputRecipe +from rattler_build.rattler_build import RecipeParseError + + +def test_from_dict_missing_required_field() -> None: + """Test that from_dict gives clear error for missing required fields.""" + # Missing 'name' in package + recipe_dict = { + "package": { + "version": "1.0.0" + # missing 'name' + } + } + + with pytest.raises(RecipeParseError) as exc_info: + Stage0Recipe.from_dict(recipe_dict) + + # Error should mention the missing field + error_msg = str(exc_info.value) + assert "name" in error_msg.lower() or "package" in error_msg.lower() + + +def test_from_dict_wrong_type_for_version() -> None: + """Test that from_dict validates version types.""" + recipe_dict = { + "package": { + "name": "test-package", + "version": 123, # Should be string + } + } + + # This should either work (converting to string) or give a clear error + # The behavior depends on the parser's strictness + try: + stage0 = Stage0Recipe.from_dict(recipe_dict) + # If it works, version should be converted or accepted + assert stage0 is not None + except RecipeParseError as e: + # If it fails, error should be clear + assert "version" in str(e).lower() or "type" in str(e).lower() + + +def test_from_dict_invalid_build_number() -> None: + """Test that from_dict validates build number types.""" + recipe_dict = { + "package": {"name": "test-package", "version": "1.0.0"}, + "build": { + "number": "not a number" # Should be int + }, + } + + with pytest.raises(RecipeParseError) as exc_info: + Stage0Recipe.from_dict(recipe_dict) + + error_msg = str(exc_info.value) + # Error should mention build or number or type mismatch + assert any(word in error_msg.lower() for word in ["build", "number", "int", "type", "invalid"]) + + +def test_from_dict_invalid_structure() -> None: + """Test that from_dict rejects completely invalid structures.""" + # Not a dict at all + with pytest.raises((RecipeParseError, TypeError, AttributeError)): + Stage0Recipe.from_dict({"invalid_key": "value"}) + + +def test_from_dict_unknown_top_level_field() -> None: + """Test error handling for unknown top-level fields.""" + recipe_dict = {"package": {"name": "test-package", "version": "1.0.0"}, "unknown_field": "should cause error"} + + with pytest.raises(RecipeParseError) as exc_info: + Stage0Recipe.from_dict(recipe_dict) + + error_msg = str(exc_info.value) + # Error should mention the unknown field + assert "unknown" in error_msg.lower() or "unexpected" in error_msg.lower() or "unknown_field" in error_msg + + +def test_from_dict_invalid_requirements_structure() -> None: + """Test error handling for invalid requirements structure.""" + recipe_dict = { + "package": {"name": "test-package", "version": "1.0.0"}, + "requirements": { + "run": "should be a list" # Should be list, not string + }, + } + + with pytest.raises(RecipeParseError) as exc_info: + Stage0Recipe.from_dict(recipe_dict) + + error_msg = str(exc_info.value) + # Error should mention sequence or type mismatch + assert "sequence" in error_msg.lower() or "array" in error_msg.lower() or "type" in error_msg.lower() + + +def test_from_yaml_invalid_yaml_syntax() -> None: + """Test error handling for invalid YAML syntax.""" + invalid_yaml = """ + package: + name: test + version: [unclosed bracket + """ + + with pytest.raises(RecipeParseError): + Stage0Recipe.from_yaml(invalid_yaml) + + +def test_from_yaml_missing_package_section() -> None: + """Test error handling when package section is missing.""" + yaml_without_package = """ +build: + number: 0 +""" + + with pytest.raises(RecipeParseError) as exc_info: + Stage0Recipe.from_yaml(yaml_without_package) + + error_msg = str(exc_info.value) + # Should mention package is required + assert "package" in error_msg.lower() + + +def test_from_dict_empty_dict() -> None: + """Test error handling for empty dictionary.""" + with pytest.raises(RecipeParseError) as exc_info: + Stage0Recipe.from_dict({}) + + error_msg = str(exc_info.value) + assert "package" in error_msg.lower() or "required" in error_msg.lower() + + +def test_from_dict_nested_validation() -> None: + """Test that nested field validation provides clear errors.""" + recipe_dict = { + "package": {"name": "test-package", "version": "1.0.0"}, + "build": { + "number": 0, + "python": { + "entry_points": "should be a list" # Should be list + }, + }, + } + + with pytest.raises(RecipeParseError) as exc_info: + Stage0Recipe.from_dict(recipe_dict) + + error_msg = str(exc_info.value) + # Error should mention sequence or type mismatch + assert "sequence" in error_msg.lower() or "type" in error_msg.lower() + + +def test_from_dict_provides_helpful_message() -> None: + """Test that from_dict accepts integer names and converts them.""" + recipe_dict = { + "package": { + "name": 123, # Will be converted to string + "version": "1.0.0", + } + } + + # Integer names are accepted and converted to strings + stage0 = Stage0Recipe.from_dict(recipe_dict) + assert stage0 is not None + + +def test_from_dict_list_of_strings_vs_object() -> None: + """Test clear errors when expecting list but getting something else.""" + recipe_dict = { + "package": {"name": "test-package", "version": "1.0.0"}, + "requirements": { + "host": {"not": "a list"} # Should be list + }, + } + + with pytest.raises(RecipeParseError) as exc_info: + Stage0Recipe.from_dict(recipe_dict) + + error_msg = str(exc_info.value) + # Should indicate type mismatch (sequence expected) + assert "sequence" in error_msg.lower() or "type" in error_msg.lower() + + +def test_error_includes_field_path() -> None: + """Test that errors include the path to the problematic field.""" + recipe_dict = { + "package": {"name": "test-package", "version": "1.0.0"}, + "build": { + "dynamic_linking": { + "binary_relocation": "not a boolean" # Should be bool + } + }, + } + + with pytest.raises(RecipeParseError) as exc_info: + Stage0Recipe.from_dict(recipe_dict) + + error_msg = str(exc_info.value) + # Error should help locate the problem + # It might mention the field name or path + assert any( + word in error_msg.lower() for word in ["dynamic_linking", "binary_relocation", "build", "bool", "boolean"] + ) + + +def test_from_dict_valid_minimal_recipe() -> None: + """Test that a minimal valid recipe works.""" + # This should NOT raise an error + recipe_dict = {"package": {"name": "minimal-package", "version": "1.0.0"}} + + stage0 = Stage0Recipe.from_dict(recipe_dict) + assert isinstance(stage0, SingleOutputRecipe) + + +def test_from_dict_with_schema_version() -> None: + """Test that schema_version is accepted.""" + recipe_dict = {"schema_version": 1, "package": {"name": "versioned-package", "version": "1.0.0"}} + + stage0 = Stage0Recipe.from_dict(recipe_dict) + assert stage0 is not None diff --git a/py-rattler-build/tests/unit/test_jinja_config.py b/py-rattler-build/tests/unit/test_jinja_config.py new file mode 100644 index 000000000..80c5aceb1 --- /dev/null +++ b/py-rattler-build/tests/unit/test_jinja_config.py @@ -0,0 +1,104 @@ +"""Tests for JinjaConfig (JinjaConfig) Python bindings.""" + +import pytest +from rattler_build.jinja_config import JinjaConfig + + +def test_jinja_config_creation() -> None: + """Test creating a JinjaConfig with default values.""" + config = JinjaConfig() + + assert config.target_platform is not None + assert config.host_platform is not None + assert config.build_platform is not None + assert isinstance(config.experimental, bool) + assert isinstance(config.allow_undefined, bool) + + +def test_jinja_config_with_platforms() -> None: + """Test creating a JinjaConfig with specific platforms.""" + config = JinjaConfig(target_platform="linux-64", host_platform="linux-64", build_platform="linux-64") + + assert config.target_platform == "linux-64" + assert config.host_platform == "linux-64" + assert config.build_platform == "linux-64" + + +def test_jinja_config_platform_setters() -> None: + """Test setting platforms after creation.""" + config = JinjaConfig() + + config.target_platform = "win-64" + assert config.target_platform == "win-64" + + config.host_platform = "osx-arm64" + assert config.host_platform == "osx-arm64" + + config.build_platform = "linux-aarch64" + assert config.build_platform == "linux-aarch64" + + +def test_selector_config_experimental() -> None: + """Test experimental flag.""" + config = JinjaConfig(experimental=True) + assert config.experimental is True + + config.experimental = False + assert config.experimental is False + + +def test_selector_config_allow_undefined() -> None: + """Test allow_undefined flag.""" + config = JinjaConfig(allow_undefined=True) + assert config.allow_undefined is True + + config.allow_undefined = False + assert config.allow_undefined is False + + +def test_selector_config_with_variant() -> None: + """Test creating a JinjaConfig with variant.""" + variant = {"python": "3.11", "numpy": "1.21"} + config = JinjaConfig(variant=variant) + + assert config.variant is not None + # Check that variant was set (exact structure depends on implementation) + assert isinstance(config.variant, dict) + + +def test_selector_config_variant_setter() -> None: + """Test setting variant after creation.""" + config = JinjaConfig() + + variant = {"python": "3.10", "build_number": 1} + config.variant = variant + + assert config.variant is not None + assert isinstance(config.variant, dict) + + +def test_selector_config_config_property() -> None: + """Test that the config property returns the underlying PyJinjaConfig.""" + config = JinjaConfig(target_platform="linux-64") + + # The config property should return the internal _config + underlying_config = config.config + assert underlying_config is not None + + # It should be the same object + assert underlying_config is config._config + + +def test_selector_config_repr() -> None: + """Test the string representation.""" + config = JinjaConfig(target_platform="osx-64") + repr_str = repr(config) + + assert "JinjaConfig" in repr_str + assert "osx-64" in repr_str + + +def test_selector_config_invalid_platform() -> None: + """Test that invalid platforms are rejected.""" + with pytest.raises(Exception): # Should raise some error + JinjaConfig(target_platform="invalid-platform-name-12345") diff --git a/py-rattler-build/tests/unit/test_pipeline.py b/py-rattler-build/tests/unit/test_pipeline.py new file mode 100644 index 000000000..24ad6d854 --- /dev/null +++ b/py-rattler-build/tests/unit/test_pipeline.py @@ -0,0 +1,595 @@ +""" +Comprehensive tests for the full rattler-build pipeline. + +Tests the complete flow: Stage0 Recipe -> Render -> Stage1 Recipe +""" + +import pytest +from rattler_build.stage0 import MultiOutputRecipe, Recipe as Stage0Recipe, SingleOutputRecipe +from rattler_build.variant_config import VariantConfig +from rattler_build.render import render_recipe, RenderConfig + + +def test_pipeline_from_yaml_to_stage1() -> None: + """Test complete pipeline from YAML to Stage1 recipe.""" + yaml_content = """ +package: + name: my-package + version: 1.0.0 + +build: + number: 0 + +requirements: + host: + - python + run: + - python +""" + + # Step 1: Parse to Stage0 + stage0 = Stage0Recipe.from_yaml(yaml_content) + assert isinstance(stage0, SingleOutputRecipe) + + # Step 2: Create variant config + variant_config = VariantConfig() + + # Step 3: Render to get Stage1 + rendered = render_recipe(stage0, variant_config) + assert len(rendered) == 1 + + # Step 4: Access Stage1 recipe + stage1 = rendered[0].recipe() + # Note: The Python wrapper class name might differ from the import + assert stage1 is not None + assert stage1.package.name == "my-package" + assert str(stage1.package.version) == "1.0.0" + + +def test_pipeline_from_dict_to_stage1() -> None: + """Test complete pipeline from Python dict to Stage1 recipe.""" + recipe_dict = { + "package": {"name": "dict-package", "version": "2.0.0"}, + "build": {"number": 0}, + "requirements": {"run": ["numpy"]}, + } + + # Step 1: Create Stage0 from dict + stage0 = Stage0Recipe.from_dict(recipe_dict) + assert isinstance(stage0, SingleOutputRecipe) + + # Step 2: Render + variant_config = VariantConfig() + rendered = render_recipe(stage0, variant_config) + + # Step 3: Verify Stage1 + stage1 = rendered[0].recipe() + assert stage1.package.name == "dict-package" + assert stage1.package.version == "2.0.0" + + +def test_pipeline_with_variants() -> None: + """Test pipeline with variant combinations.""" + yaml_content = """ +package: + name: variant-package + version: 1.0.0 + +requirements: + host: + - python +""" + + stage0 = Stage0Recipe.from_yaml(yaml_content) + + # Create variant config with multiple python versions + variant_config = VariantConfig() + variant_config.set_values("python", ["3.9", "3.10", "3.11"]) + + # Render with variants + rendered = render_recipe(stage0, variant_config) + + # Should have 3 variants + assert len(rendered) == 3 + + # Each variant should have different python version in used_variant + for i, variant in enumerate(rendered): + stage1 = variant.recipe() + assert stage1.package.name == "variant-package" + # Check that the variant was used + assert "python" in variant.variant() + + +def test_pipeline_multi_output() -> None: + """Test pipeline with multi-output recipe.""" + yaml_content = """ +recipe: + name: multi-package + version: 1.0.0 + +outputs: + - package: + name: multi-lib + requirements: + run: + - libfoo + + - package: + name: multi-dev + requirements: + run: + - multi-lib +""" + + # Parse Stage0 + stage0 = Stage0Recipe.from_yaml(yaml_content) + assert isinstance(stage0, MultiOutputRecipe) + + # Render + variant_config = VariantConfig() + rendered = render_recipe(stage0, variant_config) + + # Should have 2 outputs + assert len(rendered) == 2 + + # Check output names + names = {r.recipe().package.name for r in rendered} + assert names == {"multi-lib", "multi-dev"} + + +def test_pipeline_with_render_config() -> None: + """Test pipeline with custom RenderConfig.""" + yaml_content = """ +package: + name: platform-package + version: 1.0.0 +""" + + stage0 = Stage0Recipe.from_yaml(yaml_content) + variant_config = VariantConfig() + + # Create custom render config for linux + render_config = RenderConfig(target_platform="linux-64") + + # Render with custom config + rendered = render_recipe(stage0, variant_config, render_config) + + assert len(rendered) == 1 + stage1 = rendered[0].recipe() + assert stage1.package.name == "platform-package" + + +def test_pipeline_hash_info() -> None: + """Test that hash_info is available after rendering.""" + yaml_content = """ +package: + name: hash-package + version: 1.0.0 +""" + + stage0 = Stage0Recipe.from_yaml(yaml_content) + variant_config = VariantConfig() + rendered = render_recipe(stage0, variant_config) + + # Hash info should be available + hash_info = rendered[0].hash_info() + assert hash_info is not None + assert isinstance(hash_info.hash, str) + assert len(hash_info.hash) == 7 + + +def test_pipeline_pin_subpackages() -> None: + """Test pin_subpackage information in pipeline.""" + yaml_content = """ +recipe: + name: pin-test + +outputs: + - package: + name: pin-lib + version: 1.0.0 + + - package: + name: pin-app + version: 1.0.0 + requirements: + host: + - ${{ pin_subpackage('pin-lib', exact=True) }} +""" + + stage0 = Stage0Recipe.from_yaml(yaml_content) + variant_config = VariantConfig() + rendered = render_recipe(stage0, variant_config) + + # Find the pin-app output + app_variant = None + for variant in rendered: + if variant.recipe().package.name == "pin-app": + app_variant = variant + break + + assert app_variant is not None + + # Check pin_subpackages + pins = app_variant.pin_subpackages() + assert "pin-lib" in pins + assert pins["pin-lib"].exact is True + + +def test_pipeline_stage0_to_dict() -> None: + """Test that Stage0 can be converted to dict.""" + yaml_content = """ +package: + name: roundtrip-package + version: 1.0.0 + +build: + number: 5 +""" + + # Parse to Stage0 + stage0_original = Stage0Recipe.from_yaml(yaml_content) + + # Convert to dict + recipe_dict = stage0_original.to_dict() + assert isinstance(recipe_dict, dict) + assert recipe_dict["package"]["name"] == "roundtrip-package" + + # Note: Round-tripping from to_dict() output may not work directly + # because to_dict() includes all defaults with their serialized form + # which may not always be valid for from_dict() + # For practical use, users should construct minimal dicts manually + + # Just verify we can render the original + variant_config = VariantConfig() + rendered_original = render_recipe(stage0_original, variant_config) + + assert len(rendered_original) == 1 + assert rendered_original[0].recipe().package.name == "roundtrip-package" + + +def test_pipeline_stage1_properties() -> None: + """Test accessing all Stage1 recipe properties.""" + yaml_content = """ +context: + version: 1.0.0 + +package: + name: props-package + version: ${{ version }} + +build: + number: 0 + +requirements: + build: + - cmake + host: + - python + run: + - python + +about: + summary: A test package + license: MIT +""" + + stage0 = Stage0Recipe.from_yaml(yaml_content) + variant_config = VariantConfig() + rendered = render_recipe(stage0, variant_config) + + stage1 = rendered[0].recipe() + + # Test all properties + assert stage1.package.name == "props-package" + assert stage1.package.version == "1.0.0" + + assert stage1.build is not None + + assert stage1.requirements is not None + + assert stage1.about is not None + + assert isinstance(stage1.context, dict) + + assert isinstance(stage1.used_variant, dict) + + +def test_pipeline_used_variant() -> None: + """Test that used_variant contains the variant values.""" + yaml_content = """ +package: + name: variant-tracking + version: 1.0.0 + +requirements: + host: + - python +""" + + stage0 = Stage0Recipe.from_yaml(yaml_content) + + variant_config = VariantConfig() + variant_config.set_values("python", ["3.10"]) + + rendered = render_recipe(stage0, variant_config) + stage1 = rendered[0].recipe() + + # used_variant should contain python + used_variant = stage1.used_variant + assert "python" in used_variant + + +def test_pipeline_context_preservation() -> None: + """Test that context is preserved through the pipeline.""" + yaml_content = """ +context: + my_var: custom_value + version: 2.0.0 + +package: + name: context-package + version: ${{ version }} +""" + + stage0 = Stage0Recipe.from_yaml(yaml_content) + + # Check Stage0 context + stage0_context = stage0.context + + assert isinstance(stage0, SingleOutputRecipe) + assert "my_var" in stage0_context + assert stage0_context["my_var"] == "custom_value" + + # Render and check Stage1 context + variant_config = VariantConfig() + rendered = render_recipe(stage0, variant_config) + stage1 = rendered[0].recipe() + + stage1_context = stage1.context + assert "my_var" in stage1_context + assert stage1_context["my_var"] == "custom_value" + assert stage1.package.version == "2.0.0" + + +def test_pipeline_multiple_renders() -> None: + """Test rendering the same Stage0 recipe multiple times.""" + yaml_content = """ +package: + name: reusable-recipe + version: 1.0.0 +""" + + stage0 = Stage0Recipe.from_yaml(yaml_content) + variant_config = VariantConfig() + + # Render multiple times + rendered1 = render_recipe(stage0, variant_config) + rendered2 = render_recipe(stage0, variant_config) + + # Both should work + assert len(rendered1) == 1 + assert len(rendered2) == 1 + + assert rendered1[0].recipe().package.name == "reusable-recipe" + assert rendered2[0].recipe().package.name == "reusable-recipe" + + +def test_pipeline_from_dict_error_missing_package() -> None: + """Test that creating recipe from dict with missing package field gives good error.""" + recipe_dict = {"build": {"number": 0}} + + with pytest.raises(Exception) as exc_info: + Stage0Recipe.from_dict(recipe_dict) + + error_msg = str(exc_info.value).lower() + assert "failed to deserialize recipe" in error_msg or "missing" in error_msg + + +def test_pipeline_from_dict_error_wrong_type() -> None: + """Test that creating recipe from dict accepts numeric version (gets converted to string).""" + # Note: Version 123 gets converted to "123" automatically by serde + # This is actually valid behavior + recipe_dict = {"package": {"name": "test", "version": 123}} + + # This should actually work - numeric values get stringified + recipe = Stage0Recipe.from_dict(recipe_dict) + assert recipe is not None + + +def test_pipeline_from_dict_error_invalid_structure() -> None: + """Test that creating recipe from dict with invalid structure gives good error.""" + recipe_dict = {"invalid_key": "value", "another_invalid": 123} + + with pytest.raises(Exception) as exc_info: + Stage0Recipe.from_dict(recipe_dict) + + error_msg = str(exc_info.value) + # The error message contains debug format, check for "missing" or "package" field + assert "missing" in error_msg.lower() or "package" in error_msg.lower() + + +def test_pipeline_stage1_requirements_detail() -> None: + """Test detailed access to Stage1 requirements lists.""" + yaml_content = """ +package: + name: req-detail-test + version: 1.0.0 + +requirements: + build: + - cmake + - gcc + host: + - python >=3.8 + - numpy + run: + - python >=3.8 + - numpy + - pandas +""" + + stage0 = Stage0Recipe.from_yaml(yaml_content) + variant_config = VariantConfig() + rendered = render_recipe(stage0, variant_config) + + stage1 = rendered[0].recipe() + reqs = stage1.requirements + + # Test that requirements are accessible as lists + build_reqs = reqs.build + assert isinstance(build_reqs, list) + assert len(build_reqs) >= 2 + + host_reqs = reqs.host + assert isinstance(host_reqs, list) + assert len(host_reqs) >= 2 + + run_reqs = reqs.run + assert isinstance(run_reqs, list) + assert len(run_reqs) >= 3 + + +def test_pipeline_stage1_about_detail() -> None: + """Test detailed access to Stage1 about metadata.""" + yaml_content = """ +package: + name: about-detail-test + version: 1.0.0 + +about: + summary: This is a detailed test + license: Apache-2.0 + homepage: https://example.com + repository: https://github.com/example/repo + documentation: https://docs.example.com + description: | + A longer description + that spans multiple lines. +""" + + stage0 = Stage0Recipe.from_yaml(yaml_content) + variant_config = VariantConfig() + rendered = render_recipe(stage0, variant_config) + + stage1 = rendered[0].recipe() + about = stage1.about + + assert about.summary == "This is a detailed test" + assert about.license == "Apache-2.0" + # URLs may have trailing slash added + assert "https://example.com" in about.homepage + assert "https://github.com/example/repo" in about.repository + assert "https://docs.example.com" in about.documentation + assert about.description is not None + assert "longer description" in about.description + + +def test_pipeline_stage1_build_properties() -> None: + """Test detailed access to Stage1 build properties.""" + yaml_content = """ +package: + name: build-detail-test + version: 1.0.0 + +build: + number: 42 + string: custom_string + script: + - echo "Building" + - make install +""" + + stage0 = Stage0Recipe.from_yaml(yaml_content) + variant_config = VariantConfig() + rendered = render_recipe(stage0, variant_config) + + stage1 = rendered[0].recipe() + build = stage1.build + + assert build.number == 42 + # Note: build.string may be evaluated differently in Stage1 + assert build.string is not None + + +def test_pipeline_stage1_sources() -> None: + """Test that Stage1 sources list is accessible.""" + yaml_content = """ +package: + name: source-test + version: 1.0.0 + +source: + url: https://example.com/package.tar.gz + sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +""" + + stage0 = Stage0Recipe.from_yaml(yaml_content) + variant_config = VariantConfig() + rendered = render_recipe(stage0, variant_config) + + stage1 = rendered[0].recipe() + + # sources should be a list + sources = stage1.sources + assert isinstance(sources, list) + + +def test_pipeline_rendered_variant_repr() -> None: + """Test that RenderedVariant has a useful repr.""" + yaml_content = """ +package: + name: repr-test + version: 1.0.0 +""" + + stage0 = Stage0Recipe.from_yaml(yaml_content) + variant_config = VariantConfig() + rendered = render_recipe(stage0, variant_config) + + variant = rendered[0] + repr_str = repr(variant) + + assert "RenderedVariant" in repr_str + assert "repr-test" in repr_str + + +def test_pipeline_stage1_to_dict_comprehensive() -> None: + """Test that Stage1 to_dict produces a comprehensive dictionary.""" + yaml_content = """ +package: + name: comprehensive-test + version: 1.0.0 + +build: + number: 5 + +requirements: + host: + - python + run: + - python + +about: + summary: Test + license: MIT +""" + + stage0 = Stage0Recipe.from_yaml(yaml_content) + variant_config = VariantConfig() + rendered = render_recipe(stage0, variant_config) + + stage1 = rendered[0].recipe() + stage1_dict = stage1.to_dict() + + # Verify it's a proper dictionary with expected keys + assert isinstance(stage1_dict, dict) + assert "package" in stage1_dict + assert "build" in stage1_dict + assert "requirements" in stage1_dict + assert "about" in stage1_dict + + # Verify nested structure + assert "name" in stage1_dict["package"] + assert "version" in stage1_dict["package"] + assert stage1_dict["package"]["name"] == "comprehensive-test" diff --git a/py-rattler-build/tests/unit/test_recipe_api.py b/py-rattler-build/tests/unit/test_recipe_api.py new file mode 100644 index 000000000..dfc85bc84 --- /dev/null +++ b/py-rattler-build/tests/unit/test_recipe_api.py @@ -0,0 +1,445 @@ +""" +Modern recipe API tests using Stage0/Stage1/Render infrastructure. + +This replaces the old test_recipe_oop.py with the new pipeline architecture. +""" + +from pathlib import Path +from rattler_build.stage0 import MultiOutputRecipe, Recipe as Stage0Recipe, SingleOutputRecipe +from rattler_build.variant_config import VariantConfig +from rattler_build.render import render_recipe, RenderConfig, build_rendered_variants + + +TEST_DATA_DIR = Path(__file__).parent.parent / "data" / "recipes" / "comprehensive-test" +TEST_RECIPE_FILE = TEST_DATA_DIR / "recipe.yaml" + + +def test_recipe_all_sections() -> None: + """Test accessing all recipe sections through Stage0 and Stage1.""" + # Parse to Stage0 + stage0 = Stage0Recipe.from_file(str(TEST_RECIPE_FILE)) + assert stage0 is not None + assert isinstance(stage0, SingleOutputRecipe) + + # Test Stage0 Package + package_dict = stage0.package.to_dict() + assert package_dict["name"] == "test-package" + assert package_dict["version"] == "1.0.0" + + # Render to Stage1 for full access + variant_config = VariantConfig() + render_config = RenderConfig() + rendered = render_recipe(stage0, variant_config, render_config) + + assert len(rendered) == 1 + stage1 = rendered[0].recipe() + + # Stage1 Package - fully evaluated + assert stage1.package.name == "test-package" + assert str(stage1.package.version) == "1.0.0" + + # Stage1 Source + sources = stage1.sources + assert len(sources) == 1 + source_dict = sources[0].to_dict() + assert "url" in source_dict + + # Stage1 Build + build = stage1.build + assert build.number == 0 + assert build.noarch is None # Not a noarch build + + # Stage1 Requirements - fully evaluated for target platform + reqs = stage1.requirements + host_reqs = reqs.host + run_reqs = reqs.run + + # Should have python and pip at minimum + assert len(host_reqs) >= 2 + assert len(run_reqs) >= 1 + + # Stage1 About + about = stage1.about + assert about.summary == "A comprehensive test package" + assert about.license == "MIT" + assert "https://example.com" in about.homepage + assert "https://github.com/example/test-package" in about.repository + + +def test_recipe_representations() -> None: + """Test string representations of Stage0 and Stage1 objects.""" + stage0 = Stage0Recipe.from_file(str(TEST_RECIPE_FILE)) + + # Stage0 repr + recipe_repr = repr(stage0) + assert "Stage0Recipe" in recipe_repr or "Recipe" in recipe_repr + + # Render to Stage1 + variant_config = VariantConfig() + rendered = render_recipe(stage0, variant_config) + stage1 = rendered[0].recipe() + + # Stage1 recipe repr + stage1_repr = repr(stage1) + assert "Stage1Recipe" in stage1_repr + assert "test-package" in stage1_repr + + # Stage1 Package repr + package_repr = repr(stage1.package) + assert "Stage1Package" in package_repr + assert "test-package" in package_repr + assert "1.0.0" in package_repr + + # Other component reprs + assert "Stage1Build" in repr(stage1.build) + assert "Stage1Requirements" in repr(stage1.requirements) + assert "Stage1About" in repr(stage1.about) + + +def test_render_config_with_variants() -> None: + """Test RenderConfig with variant configuration.""" + render_config = RenderConfig(target_platform="linux-64") + assert render_config.target_platform == "linux-64" + + # Set extra context (similar to variants) + render_config.set_context("python", "3.11") + render_config.set_context("build_number", "1") + + assert render_config.get_context("python") == "3.11" + assert render_config.get_context("build_number") == "1" + + +def test_render_config_setters() -> None: + """Test RenderConfig property setters.""" + config = RenderConfig() + + # Test target_platform - create new config to change it + initial_target = config.target_platform + config_win = RenderConfig(target_platform="win-64") + assert config_win.target_platform == "win-64" + assert config_win.target_platform != initial_target + + # Test host_platform + config_host = RenderConfig(host_platform="win-64") + assert config_host.host_platform == "win-64" + + # Test build_platform + config_build = RenderConfig(build_platform="win-64") + assert config_build.build_platform == "win-64" + + # Test experimental + config_exp = RenderConfig(experimental=True) + assert config_exp.experimental is True + + # Test context (variant-like values) + config.set_context("python", "3.9") + config.set_context("numpy", "1.21") + + all_context = config.get_all_context() + assert "python" in all_context + assert all_context["python"] == "3.9" + assert "numpy" in all_context + assert all_context["numpy"] == "1.21" + + +def test_parse_recipe_with_platform_selectors() -> None: + """Test parsing recipe with platform selectors for different platforms.""" + stage0 = Stage0Recipe.from_file(str(TEST_RECIPE_FILE)) + variant_config = VariantConfig() + + # Render for Linux + linux_config = RenderConfig(target_platform="linux-64", build_platform="linux-64", host_platform="linux-64") + rendered_linux = render_recipe(stage0, variant_config, linux_config) + stage1_linux = rendered_linux[0].recipe() + + # Render for Windows + windows_config = RenderConfig(target_platform="win-64", build_platform="win-64", host_platform="win-64") + rendered_windows = render_recipe(stage0, variant_config, windows_config) + stage1_windows = rendered_windows[0].recipe() + + # Both should parse the same package + assert stage1_linux.package.name == "test-package" + assert stage1_windows.package.name == "test-package" + + # Both should have requirements, but they may differ due to selectors + assert len(stage1_linux.requirements.host) > 0 + assert len(stage1_windows.requirements.host) > 0 + + # The number of requirements might differ due to platform-specific selectors + # For example, gcc only on unix, pywin32 only on windows + linux_host_count = len(stage1_linux.requirements.host) + windows_host_count = len(stage1_windows.requirements.host) + + # At minimum both should have python and pip + assert linux_host_count >= 2 + assert windows_host_count >= 2 + + +def test_recipe_with_variants() -> None: + """Test recipe parsing with variant substitution.""" + yaml_content = """ +package: + name: variant-test + version: 1.0.0 + +requirements: + host: + - python ${{ python }}.* + run: + - python + +build: + number: ${{ build_number }} +""" + + stage0 = Stage0Recipe.from_yaml(yaml_content) + + # Create variant config with python version + variant_config = VariantConfig() + variant_config.set_values("python", ["3.11"]) + + # Render with context for build_number + render_config = RenderConfig() + render_config.set_context("build_number", "1") + + rendered = render_recipe(stage0, variant_config, render_config) + stage1 = rendered[0].recipe() + + assert stage1.package.name == "variant-test" + assert str(stage1.package.version) == "1.0.0" + + # Check that variant was used + variant_dict = rendered[0].variant() + assert "python" in variant_dict + assert variant_dict["python"] == "3.11" + + # Build number should be evaluated from context + assert stage1.build.number == 1 + + +def test_stage0_to_stage1_complete_flow() -> None: + """Test the complete flow from file to Stage1 with all features.""" + # Load from file + stage0 = Stage0Recipe.from_file(str(TEST_RECIPE_FILE)) + + assert stage0 is not None + + # Access Stage0 properties + stage0_dict = stage0.to_dict() + assert "package" in stage0_dict + assert "build" in stage0_dict + assert "requirements" in stage0_dict + assert "about" in stage0_dict + + # Render to Stage1 + variant_config = VariantConfig() + render_config = RenderConfig(target_platform="linux-64") + rendered = render_recipe(stage0, variant_config, render_config) + + # Access Stage1 + variant = rendered[0] + stage1 = variant.recipe() + + # Verify Stage1 properties are all accessible + assert stage1.package is not None + assert stage1.build is not None + assert stage1.requirements is not None + assert stage1.about is not None + assert stage1.context is not None + assert stage1.used_variant is not None + assert stage1.sources is not None + + # Convert Stage1 to dict + stage1_dict = stage1.to_dict() + assert isinstance(stage1_dict, dict) + assert "package" in stage1_dict + + +def test_multi_output_recipe_api() -> None: + """Test API with multi-output recipes.""" + yaml_content = """ +schema_version: 1 + +context: + name: multi-test + version: "2.0.0" + +recipe: + version: ${{ version }} + +outputs: + - package: + name: ${{ name }}-lib + requirements: + run: + - libfoo + + - package: + name: ${{ name }}-dev + requirements: + run: + - ${{ name }}-lib + +about: + summary: Multi-output test package + license: MIT +""" + + # Parse Stage0 + stage0 = Stage0Recipe.from_yaml(yaml_content) + assert isinstance(stage0, MultiOutputRecipe) + assert stage0 is not None + assert len(stage0.outputs) == 2 + + # Render to Stage1 + variant_config = VariantConfig() + rendered = render_recipe(stage0, variant_config) + + # Should have 2 outputs + assert len(rendered) == 2 + + # Check both outputs + names = {r.recipe().package.name for r in rendered} + assert names == {"multi-test-lib", "multi-test-dev"} + + # Both should be valid Stage1 recipes + for variant in rendered: + stage1 = variant.recipe() + assert stage1.package is not None + assert stage1.build is not None + assert stage1.requirements is not None + + +def test_recipe_with_jinja_context() -> None: + """Test recipe with Jinja2 context variables.""" + yaml_content = """ +context: + pkg_name: jinja-test + pkg_version: "3.2.1" + summary_text: "A test with Jinja" + +package: + name: ${{ pkg_name }} + version: ${{ pkg_version }} + +about: + summary: ${{ summary_text }} + license: BSD-3-Clause + +build: + number: 0 +""" + + # Parse Stage0 + stage0 = Stage0Recipe.from_yaml(yaml_content) + assert isinstance(stage0, SingleOutputRecipe) + assert stage0 is not None + + # Check Stage0 context is preserved + stage0_context = stage0.context + assert "pkg_name" in stage0_context + assert stage0_context["pkg_name"] == "jinja-test" + + # Render to Stage1 + variant_config = VariantConfig() + rendered = render_recipe(stage0, variant_config) + stage1 = rendered[0].recipe() + + # Jinja should be evaluated + assert stage1.package.name == "jinja-test" + assert str(stage1.package.version) == "3.2.1" + assert stage1.about.summary == "A test with Jinja" + + # Context should still be accessible in Stage1 + stage1_context = stage1.context + assert "pkg_name" in stage1_context + assert stage1_context["pkg_name"] == "jinja-test" + + +def test_recipe_from_dict_api() -> None: + """Test creating recipes from Python dictionaries.""" + recipe_dict = { + "package": {"name": "dict-api-test", "version": "4.5.6"}, + "build": {"number": 0, "script": "echo 'Building'"}, + "requirements": {"host": ["python"], "run": ["python"]}, + "about": {"summary": "Created from dict", "license": "Apache-2.0"}, + } + + # Create Stage0 from dict + stage0 = Stage0Recipe.from_dict(recipe_dict) + + # Render to Stage1 + variant_config = VariantConfig() + rendered = stage0.render(variant_config) + stage1 = rendered[0].recipe() + + # Verify all properties + assert stage1.package.name == "dict-api-test" + assert str(stage1.package.version) == "4.5.6" + assert stage1.build.number == 0 + assert len(stage1.requirements.host) >= 1 + assert len(stage1.requirements.run) >= 1 + assert stage1.about.summary == "Created from dict" + assert stage1.about.license == "Apache-2.0" + + +def test_rendered_variant_run_build() -> None: + """Test that RenderedVariant has a run_build() method.""" + yaml_content = """ +package: + name: build-test + version: 1.0.0 + +build: + number: 0 + script: echo "Building" + +requirements: + host: + - python + run: + - python +""" + # Parse and render + stage0 = Stage0Recipe.from_yaml(yaml_content) + variant_config = VariantConfig() + rendered = render_recipe(stage0, variant_config) + + # Verify that RenderedVariant has run_build method + assert hasattr(rendered[0], "run_build") + assert callable(rendered[0].run_build) + + # Note: We don't actually call run_build() in this test to avoid + # creating actual build artifacts during testing + + +def test_build_rendered_variants_function() -> None: + """Test the build_rendered_variants() free function.""" + yaml_content = """ +package: + name: multi-build-test + version: 2.0.0 + +build: + number: 0 + +requirements: + host: + - python ${{ python }} + run: + - python +""" + # Parse and render with multiple variants + stage0 = Stage0Recipe.from_yaml(yaml_content) + variant_config = VariantConfig() + variant_config.set_values("python", ["3.10.*", "3.11.*"]) + rendered = render_recipe(stage0, variant_config) + + # Should have 2 variants + assert len(rendered) == 2 + + # Verify build_rendered_variants exists and is callable + assert callable(build_rendered_variants) + + # Note: We don't actually call build_rendered_variants() to avoid + # creating actual build artifacts during testing diff --git a/py-rattler-build/tests/unit/test_recipe_oop.py b/py-rattler-build/tests/unit/test_recipe_oop.xpy similarity index 88% rename from py-rattler-build/tests/unit/test_recipe_oop.py rename to py-rattler-build/tests/unit/test_recipe_oop.xpy index 3c8690ce2..8f12f29c8 100644 --- a/py-rattler-build/tests/unit/test_recipe_oop.py +++ b/py-rattler-build/tests/unit/test_recipe_oop.xpy @@ -1,6 +1,6 @@ from pathlib import Path -from rattler_build import Recipe, SelectorConfig -from rattler_build.rattler_build import parse_recipe_py +from rattler_build import Recipe, JinjaConfig +# from rattler_build.rattler_build import parse_recipe_py TEST_DATA_DIR = Path(__file__).parent.parent / "data" / "recipes" / "test-package" TEST_RECIPE_FILE = TEST_DATA_DIR / "recipe.yaml" @@ -87,16 +87,16 @@ def test_recipe_representations() -> None: def test_selector_config_with_variants() -> None: - """Test SelectorConfig with variant configuration""" - config = SelectorConfig(target_platform="linux-64", variant={"python": "3.11", "build_number": 1}) + """Test JinjaConfig with variant configuration""" + config = JinjaConfig(target_platform="linux-64", variant={"python": "3.11", "build_number": 1}) assert config.target_platform == "linux-64" assert config.variant["python"] == "3.11" assert config.variant["build_number"] == 1 def test_selector_config_setters() -> None: - """Test SelectorConfig property setters""" - config = SelectorConfig() + """Test JinjaConfig property setters""" + config = JinjaConfig() initial_target = config.target_platform # Store initial value (may have default) config.target_platform = "win-64" @@ -139,8 +139,8 @@ def test_selector_config_setters() -> None: def test_parse_recipe_with_selectors() -> None: """Test parsing recipe with platform selectors using existing test data""" - linux_config = SelectorConfig(target_platform="linux-64") - windows_config = SelectorConfig(target_platform="win-64") + linux_config = JinjaConfig(target_platform="linux-64") + windows_config = JinjaConfig(target_platform="win-64") recipe_linux = parse_recipe_py(TEST_RECIPE_FILE.read_text(), linux_config.config) recipe_windows = parse_recipe_py(TEST_RECIPE_FILE.read_text(), windows_config.config) @@ -149,9 +149,9 @@ def test_parse_recipe_with_selectors() -> None: assert recipe_linux["package"]["name"] == "test-package" assert recipe_windows["package"]["name"] == "test-package" - # Check that we can parse both successfully - the main point is that SelectorConfig works + # Check that we can parse both successfully - the main point is that JinjaConfig works # However, as @wolf noted, the "intermediate recipe" in pixi-build will be migrating to rattler-build at some point. - # This would improve SelectorConfig to parse and validate while resolving selectors for different operating systems. + # This would improve JinjaConfig to parse and validate while resolving selectors for different operating systems. # For example, linux: selectors could potentially be validated on Windows. # But for now, this is all we can do. assert "requirements" in recipe_linux @@ -162,7 +162,7 @@ def test_parse_recipe_with_selectors() -> None: def test_recipe_with_variants() -> None: """Test recipe parsing with variant substitution using existing test data""" - config = SelectorConfig(target_platform="linux-64", variant={"python": "3.11", "build_number": 1}) + config = JinjaConfig(target_platform="linux-64", variant={"python": "3.11", "build_number": 1}) recipe = parse_recipe_py(TEST_RECIPE_FILE.read_text(), config.config) diff --git a/py-rattler-build/tests/unit/test_render.py b/py-rattler-build/tests/unit/test_render.py new file mode 100644 index 000000000..ed742779b --- /dev/null +++ b/py-rattler-build/tests/unit/test_render.py @@ -0,0 +1,505 @@ +""" +Tests for the render module - converting Stage0 to Stage1 recipes with variants. +""" + +from pathlib import Path +from inline_snapshot import snapshot +import pytest +from rattler_build.stage0 import Recipe +from rattler_build.variant_config import VariantConfig +from rattler_build.render import RenderConfig, RenderedVariant, render_recipe + + +@pytest.fixture +def test_data_dir() -> Path: + """Fixture providing the path to the test-data directory.""" + # Go up from tests/unit/ to py-rattler-build/, then up to rattler-build/, then to test-data/ + return Path(__file__).parent.parent / "data" + + +def test_render_config_creation() -> None: + """Test RenderConfig can be created with default settings.""" + config = RenderConfig() + assert config.target_platform is not None + assert config.build_platform is not None + assert config.host_platform is not None + assert not config.experimental + assert config.recipe_path is None + + +def test_render_config_with_platforms() -> None: + """Test RenderConfig with custom platforms.""" + config = RenderConfig(target_platform="linux-64", build_platform="linux-64", host_platform="linux-64") + assert config.target_platform == "linux-64" + assert config.build_platform == "linux-64" + assert config.host_platform == "linux-64" + + +def test_render_config_set_context() -> None: + """Test setting extra context variables.""" + config = RenderConfig() + config.set_context("my_var", "value") + config.set_context("my_bool", True) + config.set_context("my_number", 42) + config.set_context("my_list", [1, 2, 3]) + + # TODO: This should actually error as mixed types are not allowed + config.set_context("error_list", [1, "string", 3]) + + assert config.get_context("my_var") == "value" + assert config.get_context("my_bool") + assert isinstance(config.get_context("my_bool"), bool) + assert config.get_context("my_number") == 42 + assert config.get_context("my_list") == [1, 2, 3] + assert config.get_context("error_list") == [1, "string", 3] + + context = config.get_all_context() + assert context.keys() == {"my_var", "my_bool", "my_number", "my_list", "error_list"} + + +def test_render_config_platform_setters() -> None: + """Test platform property setters.""" + config = RenderConfig() + config.target_platform = "osx-arm64" + config.build_platform = "osx-64" + config.host_platform = "linux-64" + + assert config.target_platform == "osx-arm64" + assert config.build_platform == "osx-64" + assert config.host_platform == "linux-64" + + +def test_render_config_experimental() -> None: + """Test experimental flag.""" + config = RenderConfig(experimental=True) + assert config.experimental + + config.experimental = False + assert not config.experimental + + +def test_render_simple_recipe() -> None: + """Test rendering a simple recipe without variants.""" + recipe_yaml = """ +package: + name: test-package + version: 1.0.0 + +build: + number: 0 + +requirements: + host: + - python >=3.8 + run: + - python >=3.8 + +about: + summary: A test package + license: MIT +""" + + recipe = Recipe.from_yaml(recipe_yaml) + variant_config = VariantConfig() + + rendered = render_recipe(recipe, variant_config) + + assert len(rendered) == 1 + assert isinstance(rendered[0], RenderedVariant) + + +def test_render_recipe_with_variants() -> None: + """Test rendering a recipe with variant configuration.""" + recipe_yaml = """ +package: + name: test-package + version: 1.0.0 + +requirements: + host: + - python ${{ python }}.* + run: + - python +""" + + variant_yaml = """ +python: + - "3.9" + - "3.10" + - "3.11" +""" + + recipe = Recipe.from_yaml(recipe_yaml) + variant_config = VariantConfig.from_yaml(variant_yaml) + + rendered = render_recipe(recipe, variant_config) + + # Should have 3 variants (one for each Python version) + assert len(rendered) == 3 + + # Check that each variant has the correct python value + python_versions = {variant.variant().get("python") for variant in rendered} + assert python_versions == {"3.9", "3.10", "3.11"} + + +def test_render_recipe_with_custom_config() -> None: + """Test rendering with custom render configuration.""" + recipe_yaml = """ +package: + name: test-package + version: 1.0.0 +""" + + recipe = Recipe.from_yaml(recipe_yaml) + variant_config = VariantConfig() + config = RenderConfig(target_platform="linux-64", experimental=True) + + rendered = render_recipe(recipe, variant_config, config) + + assert len(rendered) >= 1 + # Verify the recipe was rendered + assert rendered[0].recipe() is not None + + +def test_rendered_variant_properties() -> None: + """Test RenderedVariant properties.""" + recipe_yaml = """ +package: + name: my-package + version: 1.2.3 + +build: + number: 5 +""" + + recipe = Recipe.from_yaml(recipe_yaml) + variant_config = VariantConfig() + + rendered = render_recipe(recipe, variant_config) + variant = rendered[0] + + # Test variant() method + variant_dict = variant.variant() + assert isinstance(variant_dict, dict) + + # Test recipe() method + stage1_recipe = variant.recipe() + assert stage1_recipe is not None + # Verify it's a Stage1 recipe by checking properties + package = stage1_recipe.package + assert package.name == "my-package" + assert str(package.version) == "1.2.3" + + # Test hash_info() method + hash_info = variant.hash_info() + if hash_info is not None: + assert hasattr(hash_info, "hash") + assert hasattr(hash_info, "prefix") + + # Test pin_subpackages() method + pin_subpackages = variant.pin_subpackages() + assert isinstance(pin_subpackages, dict) + + +def test_render_multi_output_recipe() -> None: + """Test rendering a multi-output recipe.""" + recipe_yaml = """ +schema_version: 1 + +context: + name: multi-pkg + version: "1.0.0" + +recipe: + version: ${{ version }} + +outputs: + - package: + name: ${{ name }}-lib + build: + noarch: generic + + - package: + name: ${{ name }} + build: + noarch: generic +""" + + recipe = Recipe.from_yaml(recipe_yaml) + variant_config = VariantConfig() + + rendered = render_recipe(recipe, variant_config) + + # Should have 2 outputs + assert len(rendered) == 2 + + # Check package names + names = {variant.recipe().package.name for variant in rendered} + assert names == {"multi-pkg-lib", "multi-pkg"} + + +def test_render_with_jinja_expressions() -> None: + """Test rendering with Jinja expressions.""" + recipe_yaml = """ +package: + name: jinja-test + version: 1.0.0 + +context: + my_value: "hello" + +build: + number: 0 + script: + - echo "${{ my_value }}" +""" + + recipe = Recipe.from_yaml(recipe_yaml) + variant_config = VariantConfig() + + rendered = render_recipe(recipe, variant_config) + + assert len(rendered) == 1 + # Jinja should have been evaluated + stage1_recipe = rendered[0].recipe() + assert stage1_recipe is not None + + +def test_render_with_free_specs() -> None: + """Test rendering with free specs (unversioned dependencies).""" + recipe_yaml = """ +package: + name: test-pkg + version: "1.0.0" + +requirements: + build: + - python +""" + + variant_yaml = """ +python: + - "3.9" + - "3.10" +""" + + recipe = Recipe.from_yaml(recipe_yaml) + variant_config = VariantConfig.from_yaml(variant_yaml) + + rendered = render_recipe(recipe, variant_config) + + # Should create variants based on free spec "python" + assert len(rendered) == 2 + + +def test_render_config_repr() -> None: + """Test RenderConfig __repr__.""" + config = RenderConfig(target_platform="linux-64", experimental=True) + repr_str = repr(config) + assert "RenderConfig" in repr_str + assert "linux-64" in repr_str + + +def test_rendered_variant_repr() -> None: + """Test RenderedVariant __repr__.""" + recipe_yaml = """ +package: + name: repr-test + version: 2.0.0 +""" + + recipe = Recipe.from_yaml(recipe_yaml) + variant_config = VariantConfig() + + rendered = render_recipe(recipe, variant_config) + repr_str = repr(rendered[0]) + assert "RenderedVariant" in repr_str + assert "repr-test" in repr_str + + +def test_render_with_pin_subpackage() -> None: + """Test rendering with pin_subpackage.""" + recipe_yaml = """ +schema_version: 1 + +context: + name: my-pkg + version: "0.1.0" + +recipe: + version: ${{ version }} + +build: + number: 0 + +outputs: + - package: + name: ${{ name }} + build: + noarch: generic + + - package: + name: ${{ name }}-extra + build: + noarch: generic + requirements: + run: + - ${{ pin_subpackage(name, exact=true) }} +""" + + recipe = Recipe.from_yaml(recipe_yaml) + variant_config = VariantConfig() + + rendered = render_recipe(recipe, variant_config) + + # Should have 2 outputs + assert len(rendered) == 2 + + # Find the -extra package + extra_pkg = None + for variant in rendered: + if variant.recipe().package.name == "my-pkg-extra": + extra_pkg = variant + break + + assert extra_pkg is not None + + # Check pin_subpackages + pin_subpackages = extra_pkg.pin_subpackages() + assert "my-pkg" in pin_subpackages or "my_pkg" in pin_subpackages + + +def test_render_invalid_platform() -> None: + """Test that invalid platform raises error.""" + with pytest.raises(Exception): + RenderConfig(target_platform="invalid-platform-name") + + +def test_render_context_nonexistent_key() -> None: + """Test getting non-existent context key returns None.""" + config = RenderConfig() + assert config.get_context("nonexistent") is None + + +def test_render_recipe_with_staging(test_data_dir: Path) -> None: + """Test rendering a recipe with staging.""" + recipe_path = test_data_dir / "recipes" / "with-staging.yaml" + recipe_yaml = recipe_path.read_text() + recipe = Recipe.from_yaml(recipe_yaml) + variant_config = VariantConfig() + rendered = render_recipe(recipe, variant_config) + assert len(rendered) == 2 + assert isinstance(rendered[0], RenderedVariant) + + assert rendered[0].recipe().package.name == "mixed-compiled" + assert rendered[0].recipe().package.version == "1.2.3" + assert len(rendered[0].recipe().staging_caches) == 1 + assert rendered[0].recipe().about.to_dict() == snapshot( + { + "repository": "https://github.com/foobar/repo", + "license": "Apache-2.0", + "license_file": ["LICENSE"], + "summary": "Compiled library package", + } + ) + + +def test_render_recipe_from_yaml_string() -> None: + """Test rendering with recipe as YAML string.""" + recipe_yaml = """ +package: + name: string-test + version: 1.0.0 +""" + variant_yaml = """ +python: + - "3.10" +""" + + # Pass recipe and variant_config as strings + rendered = render_recipe(recipe_yaml, variant_yaml) + + assert len(rendered) >= 1 + assert rendered[0].recipe().package.name == "string-test" + + +def test_render_recipe_from_path(test_data_dir: Path) -> None: + """Test rendering with recipe as Path object.""" + recipe_path = test_data_dir / "recipes" / "with-staging.yaml" + variant_config = VariantConfig() + + # Pass recipe as Path object + rendered = render_recipe(recipe_path, variant_config) + + assert len(rendered) == 2 + assert rendered[0].recipe().package.name == "mixed-compiled" + + +def test_render_recipe_list() -> None: + """Test rendering with a list of recipes.""" + recipe1_yaml = """ +package: + name: pkg1 + version: 1.0.0 +""" + recipe2_yaml = """ +package: + name: pkg2 + version: 2.0.0 +""" + + # Parse recipes + recipe1 = Recipe.from_yaml(recipe1_yaml) + recipe2 = Recipe.from_yaml(recipe2_yaml) + + variant_config = VariantConfig() + + # Pass list of recipes + rendered = render_recipe([recipe1, recipe2], variant_config) + + assert len(rendered) == 2 + names = {variant.recipe().package.name for variant in rendered} + assert names == {"pkg1", "pkg2"} + + +def test_render_variant_config_from_yaml_string() -> None: + """Test rendering with variant_config as YAML string.""" + recipe_yaml = """ +package: + name: test-pkg + version: 1.0.0 + +requirements: + host: + - python ${{ python }}.* +""" + + variant_yaml = """ +python: + - "3.9" + - "3.10" +""" + + # Pass variant_config as string + rendered = render_recipe(recipe_yaml, variant_yaml) + + assert len(rendered) == 2 + python_versions = {variant.variant().get("python") for variant in rendered} + assert python_versions == {"3.9", "3.10"} + + +def test_render_invalid_recipe_type() -> None: + """Test that invalid recipe type raises TypeError.""" + with pytest.raises(TypeError, match="Unsupported recipe type"): + render_recipe(123, VariantConfig()) # type: ignore[arg-type] + + +def test_render_invalid_variant_config_type() -> None: + """Test that invalid variant_config type raises TypeError.""" + recipe_yaml = """ +package: + name: test + version: 1.0.0 +""" + with pytest.raises(TypeError, match="Unsupported variant_config type"): + render_recipe(recipe_yaml, 123) # type: ignore[arg-type] diff --git a/py-rattler-build/tests/unit/test_render_types.py b/py-rattler-build/tests/unit/test_render_types.py new file mode 100644 index 000000000..0f9a179ce --- /dev/null +++ b/py-rattler-build/tests/unit/test_render_types.py @@ -0,0 +1,221 @@ +"""Tests for render module typed structures (HashInfo and PinSubpackageInfo).""" + +from rattler_build.render import ( + RenderConfig, + render_recipe, +) +from rattler_build.stage0 import Recipe +from rattler_build.variant_config import VariantConfig + + +def test_hash_info_type() -> None: + """Test that HashInfo is a proper typed structure.""" + # Create a simple recipe + recipe_yaml = """ +schema_version: 1 +package: + name: test-hash + version: "1.0.0" +""" + + # Parse recipe + recipe = Recipe.from_yaml(recipe_yaml) + + # Create empty variant config + variant_config = VariantConfig() + + # Render recipe + render_config = RenderConfig() + rendered = render_recipe(recipe, variant_config, render_config) + + assert len(rendered) == 1 + variant = rendered[0] + + # Test hash_info - it always exists now + hash_info = variant.hash_info() + + assert hash_info is not None + # Check that we can access properties like a typed object + assert hasattr(hash_info, "hash") + assert hasattr(hash_info, "prefix") + assert isinstance(hash_info.hash, str) + assert isinstance(hash_info.prefix, str) + # Hash should be 7 characters + assert len(hash_info.hash) == 7 + print(f"Hash: {hash_info.hash}, Prefix: {hash_info.prefix}") + + +def test_pin_subpackages_type() -> None: + """Test that PinSubpackageInfo is a proper typed structure.""" + # Create a recipe with multiple outputs and pin_subpackage + recipe_yaml = """ +schema_version: 1 +recipe: + name: test-pin + +outputs: + - package: + name: test-lib + version: "1.0.0" + + - package: + name: test-app + version: "1.0.0" + requirements: + host: + - ${{ pin_subpackage('test-lib', exact=True) }} +""" + + # Parse recipe + recipe = Recipe.from_yaml(recipe_yaml) + + # Create empty variant config + variant_config = VariantConfig() + + # Render recipe + render_config = RenderConfig() + rendered = render_recipe(recipe, variant_config, render_config) + + # Find the test-app output (should have pin_subpackages) + app_variant = None + for variant in rendered: + recipe_obj = variant.recipe() + # Access package property (not method) + if recipe_obj.package.name == "test-app": + app_variant = variant + break + + assert app_variant is not None, "test-app variant not found" + + # Test pin_subpackages + pin_subpackages = app_variant.pin_subpackages() + + assert isinstance(pin_subpackages, dict) + + # Check that we have the test-lib pin + if "test-lib" in pin_subpackages: + pin_info = pin_subpackages["test-lib"] + + # Check that we can access properties like a typed object + assert hasattr(pin_info, "name") + assert hasattr(pin_info, "version") + assert hasattr(pin_info, "build_string") + assert hasattr(pin_info, "exact") + + assert isinstance(pin_info.name, str) + assert isinstance(pin_info.version, str) + assert isinstance(pin_info.exact, bool) + + # Since we used exact=True, this should be True + assert pin_info.exact is True + assert pin_info.name == "test-lib" + assert pin_info.version == "1.0.0" + + print(f"Pin info - name: {pin_info.name}, version: {pin_info.version}, exact: {pin_info.exact}") + + +def test_hash_info_repr() -> None: + """Test the __repr__ of HashInfo.""" + recipe_yaml = """ +schema_version: 1 +package: + name: test-repr + version: "1.0.0" +""" + + recipe = Recipe.from_yaml(recipe_yaml) + variant_config = VariantConfig() + + render_config = RenderConfig() + rendered = render_recipe(recipe, variant_config, render_config) + + hash_info = rendered[0].hash_info() + if hash_info is not None: + repr_str = repr(hash_info) + assert "HashInfo" in repr_str + assert "hash=" in repr_str + assert "prefix=" in repr_str + print(f"HashInfo repr: {repr_str}") + + +def test_pin_subpackage_info_repr() -> None: + """Test the __repr__ of PinSubpackageInfo.""" + recipe_yaml = """ +schema_version: 1 +recipe: + name: test-pin-repr + +outputs: + - package: + name: lib + version: "2.0.0" + + - package: + name: app + version: "2.0.0" + requirements: + host: + - ${{ pin_subpackage('lib', upper_bound='x.x') }} +""" + + recipe = Recipe.from_yaml(recipe_yaml) + variant_config = VariantConfig() + + render_config = RenderConfig() + rendered = render_recipe(recipe, variant_config, render_config) + + # Find app output + for variant in rendered: + if variant.recipe().package.name == "app": + pin_subpackages = variant.pin_subpackages() + if "lib" in pin_subpackages: + pin_info = pin_subpackages["lib"] + repr_str = repr(pin_info) + assert "PinSubpackageInfo" in repr_str + assert "name=" in repr_str + assert "version=" in repr_str + assert "exact=" in repr_str + print(f"PinSubpackageInfo repr: {repr_str}") + break + + +def test_hash_info_always_present() -> None: + """Test that hash_info is always present.""" + recipe_yaml = """ +schema_version: 1 +package: + name: simple-package + version: "1.0.0" +""" + + recipe = Recipe.from_yaml(recipe_yaml) + variant_config = VariantConfig() + + render_config = RenderConfig() + rendered = render_recipe(recipe, variant_config, render_config) + + hash_info = rendered[0].hash_info() + # Hash info is always present + assert hash_info is not None + assert isinstance(hash_info.hash, str) + assert len(hash_info.hash) == 7 + + +def test_empty_pin_subpackages() -> None: + """Test that pin_subpackages returns empty dict when no pins are present.""" + recipe_yaml = """ +schema_version: 1 +package: + name: no-pins + version: "1.0.0" +""" + + recipe = Recipe.from_yaml(recipe_yaml) + variant_config = VariantConfig() + + render_config = RenderConfig() + rendered = render_recipe(recipe, variant_config, render_config) + + pin_subpackages = rendered[0].pin_subpackages() + assert isinstance(pin_subpackages, dict) + assert len(pin_subpackages) == 0 diff --git a/py-rattler-build/tests/unit/test_stage0_bindings.py b/py-rattler-build/tests/unit/test_stage0_bindings.py new file mode 100644 index 000000000..f45c88848 --- /dev/null +++ b/py-rattler-build/tests/unit/test_stage0_bindings.py @@ -0,0 +1,718 @@ +"""Tests for Stage0 Python bindings.""" + +import pytest +from inline_snapshot import snapshot +from rattler_build.stage0 import Recipe, SingleOutputRecipe, MultiOutputRecipe + + +# Sample YAML recipes for testing +SIMPLE_RECIPE_YAML = """ +package: + name: test-package + version: 1.0.0 + +build: + number: 0 + +requirements: + host: + - python + run: + - python + +about: + summary: A test package + license: MIT +""" + +MULTI_OUTPUT_RECIPE_YAML = """ +recipe: + name: test-multi + version: 1.0.0 + +build: + number: 0 + +outputs: + - package: + name: test-multi-lib + requirements: + run: + - libtest + + - package: + name: test-multi-dev + requirements: + run: + - test-multi-lib + +about: + summary: A multi-output test package + license: MIT +""" + + +def test_recipe_from_yaml_single_output() -> None: + """Test parsing a single-output recipe from YAML.""" + recipe = Recipe.from_yaml(SIMPLE_RECIPE_YAML) + + assert recipe is not None + assert isinstance(recipe, SingleOutputRecipe) + assert not isinstance(recipe, MultiOutputRecipe) + + +def test_recipe_from_yaml_multi_output() -> None: + """Test parsing a multi-output recipe from YAML.""" + recipe = Recipe.from_yaml(MULTI_OUTPUT_RECIPE_YAML) + + assert recipe is not None + assert isinstance(recipe, MultiOutputRecipe) + assert not isinstance(recipe, SingleOutputRecipe) + + +def test_recipe_to_dict() -> None: + """Test converting recipe to dictionary.""" + recipe = Recipe.from_yaml(SIMPLE_RECIPE_YAML) + + recipe_dict = recipe.to_dict() + assert isinstance(recipe_dict, dict) + assert "package" in recipe_dict + + +def test_recipe_to_dict_snapshot() -> None: + """Test recipe serialization with snapshot.""" + recipe = Recipe.from_yaml(SIMPLE_RECIPE_YAML) + + recipe_dict = recipe.to_dict() + # The snapshot will capture the exact structure + assert recipe_dict == snapshot( + { + "package": {"name": "test-package", "version": "1.0.0"}, + "build": { + "number": 0, + "string": None, + "script": {}, + "noarch": None, + "python": { + "entry_points": [], + "skip_pyc_compilation": [], + "use_python_app_entrypoint": False, + "version_independent": False, + "site_packages_path": None, + }, + "skip": [], + "always_copy_files": [], + "always_include_files": [], + "merge_build_and_host_envs": False, + "files": [], + "dynamic_linking": { + "rpaths": [], + "binary_relocation": True, + "missing_dso_allowlist": [], + "rpath_allowlist": [], + "overdepending_behavior": None, + "overlinking_behavior": None, + }, + "variant": { + "use_keys": [], + "ignore_keys": [], + "down_prioritize_variant": None, + }, + "prefix_detection": { + "force_file_type": {"text": [], "binary": []}, + "ignore": False, + "ignore_binary_files": False, + }, + "post_process": [], + }, + "requirements": {"host": ["python"], "run": ["python"]}, + "about": { + "homepage": None, + "license": "MIT", + "license_family": None, + "summary": "A test package", + "description": None, + "documentation": None, + "repository": None, + }, + "extra": {}, + } + ) + + +def test_single_output_recipe_package() -> None: + """Test accessing package info from single-output recipe.""" + recipe = Recipe.from_yaml(SIMPLE_RECIPE_YAML) + + assert isinstance(recipe, SingleOutputRecipe) + assert recipe.package is not None + assert recipe.build is not None + assert recipe.requirements is not None + assert recipe.about is not None + + # TODO: decide if we want to keep context a dictionary + context = recipe.context + assert isinstance(context, dict) + + +def test_single_output_recipe_to_dict() -> None: + """Test converting single-output recipe to dict.""" + recipe = Recipe.from_yaml(SIMPLE_RECIPE_YAML) + recipe_dict = recipe.to_dict() + assert isinstance(recipe_dict, dict) + assert "package" in recipe_dict + + +def test_single_output_recipe_to_dict_snapshot() -> None: + """Test single-output recipe serialization with snapshot.""" + recipe = Recipe.from_yaml(SIMPLE_RECIPE_YAML) + recipe_dict = recipe.to_dict() + + # Snapshot the full structure + assert recipe_dict == snapshot( + { + "package": {"name": "test-package", "version": "1.0.0"}, + "build": { + "number": 0, + "string": None, + "script": {}, + "noarch": None, + "python": { + "entry_points": [], + "skip_pyc_compilation": [], + "use_python_app_entrypoint": False, + "version_independent": False, + "site_packages_path": None, + }, + "skip": [], + "always_copy_files": [], + "always_include_files": [], + "merge_build_and_host_envs": False, + "files": [], + "dynamic_linking": { + "rpaths": [], + "binary_relocation": True, + "missing_dso_allowlist": [], + "rpath_allowlist": [], + "overdepending_behavior": None, + "overlinking_behavior": None, + }, + "variant": { + "use_keys": [], + "ignore_keys": [], + "down_prioritize_variant": None, + }, + "prefix_detection": { + "force_file_type": {"text": [], "binary": []}, + "ignore": False, + "ignore_binary_files": False, + }, + "post_process": [], + }, + "requirements": {"host": ["python"], "run": ["python"]}, + "about": { + "homepage": None, + "license": "MIT", + "license_family": None, + "summary": "A test package", + "description": None, + "documentation": None, + "repository": None, + }, + "extra": {}, + } + ) + + +def test_multi_output_recipe_about() -> None: + """Test accessing about from multi-output recipe.""" + recipe = Recipe.from_yaml(MULTI_OUTPUT_RECIPE_YAML) + + assert isinstance(recipe, MultiOutputRecipe) + assert isinstance(recipe.outputs, list) + assert len(recipe.outputs) == 2 + + assert recipe.recipe is not None + assert recipe.about is not None + assert recipe.build is not None + + context = recipe.context + assert isinstance(context, dict) + + +def test_multi_output_recipe_to_dict() -> None: + """Test converting multi-output recipe to dict.""" + recipe = Recipe.from_yaml(MULTI_OUTPUT_RECIPE_YAML) + recipe_dict = recipe.to_dict() + assert isinstance(recipe_dict, dict) + assert "recipe" in recipe_dict or "outputs" in recipe_dict + + +def test_multi_output_recipe_to_dict_snapshot() -> None: + """Test multi-output recipe serialization with snapshot.""" + recipe = Recipe.from_yaml(MULTI_OUTPUT_RECIPE_YAML) + + # Snapshot the full structure including all outputs + assert recipe.to_dict() == snapshot( + { + "recipe": {"name": "test-multi", "version": "1.0.0"}, + "build": { + "number": 0, + "string": None, + "script": {}, + "noarch": None, + "python": { + "entry_points": [], + "skip_pyc_compilation": [], + "use_python_app_entrypoint": False, + "version_independent": False, + "site_packages_path": None, + }, + "skip": [], + "always_copy_files": [], + "always_include_files": [], + "merge_build_and_host_envs": False, + "files": [], + "dynamic_linking": { + "rpaths": [], + "binary_relocation": True, + "missing_dso_allowlist": [], + "rpath_allowlist": [], + "overdepending_behavior": None, + "overlinking_behavior": None, + }, + "variant": { + "use_keys": [], + "ignore_keys": [], + "down_prioritize_variant": None, + }, + "prefix_detection": { + "force_file_type": {"text": [], "binary": []}, + "ignore": False, + "ignore_binary_files": False, + }, + "post_process": [], + }, + "about": { + "homepage": None, + "license": "MIT", + "license_family": None, + "summary": "A multi-output test package", + "description": None, + "documentation": None, + "repository": None, + }, + "extra": {}, + "outputs": [ + { + "package": {"name": "test-multi-lib"}, + "inherit": None, + "requirements": {"run": ["libtest"]}, + "build": { + "number": 0, + "string": None, + "script": {}, + "noarch": None, + "python": { + "entry_points": [], + "skip_pyc_compilation": [], + "use_python_app_entrypoint": False, + "version_independent": False, + "site_packages_path": None, + }, + "skip": [], + "always_copy_files": [], + "always_include_files": [], + "merge_build_and_host_envs": False, + "files": [], + "dynamic_linking": { + "rpaths": [], + "binary_relocation": True, + "missing_dso_allowlist": [], + "rpath_allowlist": [], + "overdepending_behavior": None, + "overlinking_behavior": None, + }, + "variant": { + "use_keys": [], + "ignore_keys": [], + "down_prioritize_variant": None, + }, + "prefix_detection": { + "force_file_type": {"text": [], "binary": []}, + "ignore": False, + "ignore_binary_files": False, + }, + "post_process": [], + }, + "about": { + "homepage": None, + "license": None, + "license_family": None, + "summary": None, + "description": None, + "documentation": None, + "repository": None, + }, + }, + { + "package": {"name": "test-multi-dev"}, + "inherit": None, + "requirements": {"run": ["test-multi-lib"]}, + "build": { + "number": 0, + "string": None, + "script": {}, + "noarch": None, + "python": { + "entry_points": [], + "skip_pyc_compilation": [], + "use_python_app_entrypoint": False, + "version_independent": False, + "site_packages_path": None, + }, + "skip": [], + "always_copy_files": [], + "always_include_files": [], + "merge_build_and_host_envs": False, + "files": [], + "dynamic_linking": { + "rpaths": [], + "binary_relocation": True, + "missing_dso_allowlist": [], + "rpath_allowlist": [], + "overdepending_behavior": None, + "overlinking_behavior": None, + }, + "variant": { + "use_keys": [], + "ignore_keys": [], + "down_prioritize_variant": None, + }, + "prefix_detection": { + "force_file_type": {"text": [], "binary": []}, + "ignore": False, + "ignore_binary_files": False, + }, + "post_process": [], + }, + "about": { + "homepage": None, + "license": None, + "license_family": None, + "summary": None, + "description": None, + "documentation": None, + "repository": None, + }, + }, + ], + } + ) + + +def test_package_to_dict() -> None: + """Test converting package to dict.""" + recipe = Recipe.from_yaml(SIMPLE_RECIPE_YAML) + assert isinstance(recipe, SingleOutputRecipe) + package = recipe.package + package_dict = package.to_dict() + assert isinstance(package_dict, dict) + + +def test_package_to_dict_snapshot() -> None: + """Test package serialization with snapshot.""" + recipe = Recipe.from_yaml(SIMPLE_RECIPE_YAML) + assert isinstance(recipe, SingleOutputRecipe) + + package = recipe.package + package_dict = package.to_dict() + + # Snapshot should capture name and version + assert package_dict == snapshot({"name": "test-package", "version": "1.0.0"}) + + +def test_build_to_dict_snapshot() -> None: + """Test build serialization with snapshot.""" + recipe = Recipe.from_yaml(SIMPLE_RECIPE_YAML) + assert isinstance(recipe, SingleOutputRecipe) + + build = recipe.build + build_dict = build.to_dict() + + # Snapshot should capture build number and other build settings + assert build_dict == snapshot( + { + "number": 0, + "string": None, + "script": {}, + "noarch": None, + "python": { + "entry_points": [], + "skip_pyc_compilation": [], + "use_python_app_entrypoint": False, + "version_independent": False, + "site_packages_path": None, + }, + "skip": [], + "always_copy_files": [], + "always_include_files": [], + "merge_build_and_host_envs": False, + "files": [], + "dynamic_linking": { + "rpaths": [], + "binary_relocation": True, + "missing_dso_allowlist": [], + "rpath_allowlist": [], + "overdepending_behavior": None, + "overlinking_behavior": None, + }, + "variant": { + "use_keys": [], + "ignore_keys": [], + "down_prioritize_variant": None, + }, + "prefix_detection": { + "force_file_type": {"text": [], "binary": []}, + "ignore": False, + "ignore_binary_files": False, + }, + "post_process": [], + } + ) + + +def test_requirements_to_dict_snapshot() -> None: + """Test requirements serialization with snapshot.""" + recipe = Recipe.from_yaml(SIMPLE_RECIPE_YAML) + assert isinstance(recipe, SingleOutputRecipe) + + requirements = recipe.requirements + requirements_dict = requirements.to_dict() + + # Snapshot should capture host and run dependencies + assert requirements_dict == snapshot({"host": ["python"], "run": ["python"]}) + + +def test_about_to_dict_snapshot() -> None: + """Test about serialization with snapshot.""" + recipe = Recipe.from_yaml(SIMPLE_RECIPE_YAML) + assert isinstance(recipe, SingleOutputRecipe) + + about = recipe.about + about_dict = about.to_dict() + + # Snapshot should capture summary and license + assert about_dict == snapshot( + { + "homepage": None, + "license": "MIT", + "license_family": None, + "summary": "A test package", + "description": None, + "documentation": None, + "repository": None, + } + ) + + +def test_recipe_with_context() -> None: + """Test recipe with Jinja context variables.""" + yaml_with_context = """ +context: + version: 1.2.3 + +package: + name: test-package + version: ${{ version }} + +build: + number: 0 +""" + + recipe = Recipe.from_yaml(yaml_with_context) + context = recipe.context + assert len(context) > 0 + + +def test_invalid_yaml() -> None: + """Test that invalid YAML raises an error.""" + invalid_yaml = """ + this is not: [valid yaml + """ + + with pytest.raises(Exception): # Should raise some parsing error + Recipe.from_yaml(invalid_yaml) + + +def test_recipe_repr() -> None: + """Test recipe string representation.""" + recipe = Recipe.from_yaml(SIMPLE_RECIPE_YAML) + + repr_str = repr(recipe) + assert "Stage0Recipe" in repr_str or "Recipe" in repr_str + + +def test_multi_output_outputs_snapshot() -> None: + """Test multi-output recipe outputs structure with snapshot.""" + recipe = Recipe.from_yaml(MULTI_OUTPUT_RECIPE_YAML) + assert isinstance(recipe, MultiOutputRecipe) + outputs = recipe.outputs + + # Convert outputs to serializable format for snapshot + outputs_data = [] + for output in outputs: + outputs_data.append(output.to_dict()) + + # Snapshot should capture all output configurations + assert outputs_data == snapshot( + [ + { + "package": {"name": "test-multi-lib"}, + "inherit": None, + "requirements": {"run": ["libtest"]}, + "build": { + "number": 0, + "string": None, + "script": {}, + "noarch": None, + "python": { + "entry_points": [], + "skip_pyc_compilation": [], + "use_python_app_entrypoint": False, + "version_independent": False, + "site_packages_path": None, + }, + "skip": [], + "always_copy_files": [], + "always_include_files": [], + "merge_build_and_host_envs": False, + "files": [], + "dynamic_linking": { + "rpaths": [], + "binary_relocation": True, + "missing_dso_allowlist": [], + "rpath_allowlist": [], + "overdepending_behavior": None, + "overlinking_behavior": None, + }, + "variant": { + "use_keys": [], + "ignore_keys": [], + "down_prioritize_variant": None, + }, + "prefix_detection": { + "force_file_type": {"text": [], "binary": []}, + "ignore": False, + "ignore_binary_files": False, + }, + "post_process": [], + }, + "about": { + "homepage": None, + "license": None, + "license_family": None, + "summary": None, + "description": None, + "documentation": None, + "repository": None, + }, + }, + { + "package": {"name": "test-multi-dev"}, + "inherit": None, + "requirements": {"run": ["test-multi-lib"]}, + "build": { + "number": 0, + "string": None, + "script": {}, + "noarch": None, + "python": { + "entry_points": [], + "skip_pyc_compilation": [], + "use_python_app_entrypoint": False, + "version_independent": False, + "site_packages_path": None, + }, + "skip": [], + "always_copy_files": [], + "always_include_files": [], + "merge_build_and_host_envs": False, + "files": [], + "dynamic_linking": { + "rpaths": [], + "binary_relocation": True, + "missing_dso_allowlist": [], + "rpath_allowlist": [], + "overdepending_behavior": None, + "overlinking_behavior": None, + }, + "variant": { + "use_keys": [], + "ignore_keys": [], + "down_prioritize_variant": None, + }, + "prefix_detection": { + "force_file_type": {"text": [], "binary": []}, + "ignore": False, + "ignore_binary_files": False, + }, + "post_process": [], + }, + "about": { + "homepage": None, + "license": None, + "license_family": None, + "summary": None, + "description": None, + "documentation": None, + "repository": None, + }, + }, + ] + ) + + +def test_context_with_jinja_snapshot() -> None: + """Test recipe context with Jinja variables using snapshot.""" + yaml_with_context = """ +context: + version: 1.2.3 + name: my-package + +package: + name: ${{ name }} + version: ${{ version }} + +requirements: + build: + - if: win + then: + - vc2019 + else: + - gcc + - ${{ compiler('cxx' )}} + run: + - ${{ "pywin32" if win }} + +build: + number: 0 +""" + + recipe = Recipe.from_yaml(yaml_with_context) + assert isinstance(recipe, SingleOutputRecipe) + + # Snapshot the context structure + assert recipe.context == snapshot({"version": "1.2.3", "name": "my-package"}) + # Jinja should stay jinja in the stage0 recipe + assert recipe.package.to_dict() == snapshot({"name": "${{ name }}", "version": "${{ version }}"}) + + assert recipe.requirements.to_dict() == snapshot( + { + "build": [ + {"if": "win", "then": "vc2019", "else": "gcc"}, + "${{ compiler('cxx' )}}", + ], + "run": ['${{ "pywin32" if win }}'], + } + ) diff --git a/py-rattler-build/tests/unit/test_stage1_bindings.py b/py-rattler-build/tests/unit/test_stage1_bindings.py new file mode 100644 index 000000000..cd1f2191b --- /dev/null +++ b/py-rattler-build/tests/unit/test_stage1_bindings.py @@ -0,0 +1,268 @@ +"""Tests for Stage1 Python bindings. + +Note: Stage1 recipes are fully evaluated recipes. These tests focus on the API surface +since creating Stage1 recipes requires the full evaluation pipeline which is tested +elsewhere. +""" + +from inline_snapshot import snapshot +from rattler_build import stage1 + + +def test_stage1_module_exports() -> None: + """Test that stage1 module exports expected classes.""" + assert hasattr(stage1, "Recipe") + assert hasattr(stage1, "Package") + assert hasattr(stage1, "Build") + assert hasattr(stage1, "Requirements") + assert hasattr(stage1, "About") + assert hasattr(stage1, "Source") + assert hasattr(stage1, "StagingCache") + + +def test_stage1_recipe_type_annotations() -> None: + """Test that Recipe class has proper type annotations.""" + + # Check that the class can be used for type annotations + def accepts_recipe(recipe: stage1.Recipe) -> None: + pass + + # This should not raise a type error + assert callable(accepts_recipe) + + +def test_stage1_package_type_annotations() -> None: + """Test that Package class has proper type annotations.""" + + def accepts_package(package: stage1.Package) -> None: + pass + + assert callable(accepts_package) + + +def test_stage1_build_type_annotations() -> None: + """Test that Build class has proper type annotations.""" + + def accepts_build(build: stage1.Build) -> None: + pass + + assert callable(accepts_build) + + +def test_stage1_requirements_type_annotations() -> None: + """Test that Requirements class has proper type annotations.""" + + def accepts_requirements(requirements: stage1.Requirements) -> None: + pass + + assert callable(accepts_requirements) + + +def test_stage1_about_type_annotations() -> None: + """Test that About class has proper type annotations.""" + + def accepts_about(about: stage1.About) -> None: + pass + + assert callable(accepts_about) + + +def test_stage1_source_type_annotations() -> None: + """Test that Source class has proper type annotations.""" + + def accepts_source(source: stage1.Source) -> None: + pass + + assert callable(accepts_source) + + +def test_stage1_staging_cache_type_annotations() -> None: + """Test that StagingCache class has proper type annotations.""" + + def accepts_staging_cache(cache: stage1.StagingCache) -> None: + pass + + assert callable(accepts_staging_cache) + + +def test_stage1_recipe_wrapper_structure() -> None: + """Test that Recipe wrapper has expected methods.""" + # The Recipe class should have these methods based on the implementation + expected_methods = [ + "package", + "build", + "requirements", + "about", + "context", + "used_variant", + "sources", + "staging_caches", + "inherits_from", + "to_dict", + ] + + for method in expected_methods: + assert hasattr(stage1.Recipe, method), f"Recipe should have {method} method/property" + + +def test_stage1_package_wrapper_structure() -> None: + """Test that Package wrapper has expected methods.""" + expected_methods = ["name", "version", "to_dict"] + + for method in expected_methods: + assert hasattr(stage1.Package, method), f"Package should have {method} method/property" + + +def test_stage1_build_wrapper_structure() -> None: + """Test that Build wrapper has expected methods.""" + expected_methods = ["number", "string", "script", "noarch", "to_dict"] + + for method in expected_methods: + assert hasattr(stage1.Build, method), f"Build should have {method} method/property" + + +def test_stage1_requirements_wrapper_structure() -> None: + """Test that Requirements wrapper has expected methods.""" + expected_methods = ["build", "host", "run", "to_dict"] + + for method in expected_methods: + assert hasattr(stage1.Requirements, method), f"Requirements should have {method} method/property" + + +def test_stage1_about_wrapper_structure() -> None: + """Test that About wrapper has expected methods.""" + expected_methods = ["homepage", "repository", "documentation", "license", "summary", "description", "to_dict"] + + for method in expected_methods: + assert hasattr(stage1.About, method), f"About should have {method} method/property" + + +def test_stage1_source_wrapper_structure() -> None: + """Test that Source wrapper has expected methods.""" + expected_methods = ["to_dict"] + + for method in expected_methods: + assert hasattr(stage1.Source, method), f"Source should have {method} method/property" + + +def test_stage1_staging_cache_wrapper_structure() -> None: + """Test that StagingCache wrapper has expected methods.""" + expected_methods = ["name", "build", "requirements", "to_dict"] + + for method in expected_methods: + assert hasattr(stage1.StagingCache, method), f"StagingCache should have {method} method/property" + + +def test_stage1_wrapper_classes_exist() -> None: + """Test that wrapper classes are properly defined.""" + # Try to access the classes - this will fail if they're not properly imported + assert stage1.Recipe is not None + assert stage1.Package is not None + assert stage1.Build is not None + assert stage1.Requirements is not None + assert stage1.About is not None + assert stage1.Source is not None + assert stage1.StagingCache is not None + + +# Integration tests would require a full build pipeline +# These would test actual Stage1 recipe creation and manipulation +# For now, we focus on the API surface and type safety + + +def test_stage1_module_structure_snapshot() -> None: + """Test the stage1 module structure with snapshot.""" + # Get all public exports from stage1 module + exports = [name for name in dir(stage1) if not name.startswith("_")] + + # Snapshot the module structure to catch any API changes + assert sorted(exports) == snapshot( + [ + "About", + "Any", + "Build", + "Dict", + "List", + "Optional", + "Package", + "Recipe", + "Requirements", + "Source", + "StagingCache", + "TYPE_CHECKING", + ] + ) + + +def test_stage1_recipe_properties_snapshot() -> None: + """Test Recipe class properties with snapshot.""" + # Get all public properties and methods + recipe_attrs = [name for name in dir(stage1.Recipe) if not name.startswith("_")] + + # Snapshot to ensure API stability + assert sorted(recipe_attrs) == snapshot( + [ + "about", + "build", + "context", + "inherits_from", + "package", + "requirements", + "sources", + "staging_caches", + "to_dict", + "used_variant", + ] + ) + + +def test_stage1_package_properties_snapshot() -> None: + """Test Package class properties with snapshot.""" + package_attrs = [name for name in dir(stage1.Package) if not name.startswith("_")] + + assert sorted(package_attrs) == snapshot(["name", "to_dict", "version"]) + + +def test_stage1_build_properties_snapshot() -> None: + """Test Build class properties with snapshot.""" + build_attrs = [name for name in dir(stage1.Build) if not name.startswith("_")] + + assert sorted(build_attrs) == snapshot(["noarch", "number", "script", "string", "to_dict"]) + + +def test_stage1_requirements_properties_snapshot() -> None: + """Test Requirements class properties with snapshot.""" + requirements_attrs = [name for name in dir(stage1.Requirements) if not name.startswith("_")] + + assert sorted(requirements_attrs) == snapshot(["build", "host", "run", "to_dict"]) + + +def test_stage1_about_properties_snapshot() -> None: + """Test About class properties with snapshot.""" + about_attrs = [name for name in dir(stage1.About) if not name.startswith("_")] + + assert sorted(about_attrs) == snapshot( + [ + "description", + "documentation", + "homepage", + "license", + "repository", + "summary", + "to_dict", + ] + ) + + +def test_stage1_source_properties_snapshot() -> None: + """Test Source class properties with snapshot.""" + source_attrs = [name for name in dir(stage1.Source) if not name.startswith("_")] + + assert sorted(source_attrs) == snapshot(["to_dict"]) + + +def test_stage1_staging_cache_properties_snapshot() -> None: + """Test StagingCache class properties with snapshot.""" + cache_attrs = [name for name in dir(stage1.StagingCache) if not name.startswith("_")] + + assert sorted(cache_attrs) == snapshot(["build", "name", "requirements", "to_dict"]) diff --git a/py-rattler-build/tests/unit/test_upload.py b/py-rattler-build/tests/unit/test_upload.py new file mode 100644 index 000000000..e62b0e59c --- /dev/null +++ b/py-rattler-build/tests/unit/test_upload.py @@ -0,0 +1,49 @@ +import os + +import pytest + +import rattler_build + + +@pytest.mark.skipif(not os.getenv("CI"), reason="Only run on CI") +def test_upload_to_quetz_no_token() -> None: + url = "https://quetz.io" + channel = "some_channel" + with pytest.raises(rattler_build.RattlerBuildError, match="No quetz api key was given"): + rattler_build.upload_package_to_quetz([], url, channel) + + +@pytest.mark.skipif(not os.getenv("CI"), reason="Only run on CI") +def test_upload_to_artifactory_no_token() -> None: + url = "https://artifactory.io" + channel = "some_channel" + with pytest.raises(rattler_build.RattlerBuildError, match="No bearer token was given"): + rattler_build.upload_package_to_artifactory([], url, channel) + + +@pytest.mark.skipif(not os.getenv("CI"), reason="Only run on CI") +def test_upload_to_prefix_no_token() -> None: + url = "https://prefix.dev" + channel = "some_channel" + with pytest.raises(rattler_build.RattlerBuildError, match="No prefix.dev api key was given"): + rattler_build.upload_package_to_prefix([], url, channel) + + +@pytest.mark.skipif(not os.getenv("CI"), reason="Only run on CI") +def test_upload_to_anaconda_no_token() -> None: + url = "https://anaconda.org" + with pytest.raises(rattler_build.RattlerBuildError, match="No anaconda.org api key was given"): + rattler_build.upload_package_to_anaconda([], url) + + +@pytest.mark.skipif(not os.getenv("CI"), reason="Only run on CI") +def test_upload_packages_to_conda_forge_invalid_url() -> None: + staging_token = "xxx" + feedstock = "some_feedstock" + feedstock_token = "xxx" + anaconda_url = "invalid-url" + + with pytest.raises(rattler_build.RattlerBuildError, match="relative URL without a base"): + rattler_build.upload_packages_to_conda_forge( + [], staging_token, feedstock, feedstock_token, anaconda_url=anaconda_url + ) diff --git a/py-rattler-build/tests/unit/test_variant_config.py b/py-rattler-build/tests/unit/test_variant_config.py new file mode 100644 index 000000000..4b93a8ebc --- /dev/null +++ b/py-rattler-build/tests/unit/test_variant_config.py @@ -0,0 +1,410 @@ +"""Tests for VariantConfig Python bindings.""" + +import tempfile +from pathlib import Path +from rattler_build.variant_config import VariantConfig + + +def test_variant_config_creation() -> None: + """Test creating an empty VariantConfig.""" + config = VariantConfig() + assert len(config) == 0 + + +def test_variant_config_set_values() -> None: + """Test setting variant values.""" + config = VariantConfig() + + # Set some variant values + config.set_values("python", ["3.8", "3.9", "3.10"]) + config.set_values("numpy", ["1.21", "1.22"]) + + # Check that keys were added + keys = config.keys() + assert "python" in keys + assert "numpy" in keys + assert len(config) == 2 + + +def test_variant_config_get_values() -> None: + """Test getting variant values.""" + config = VariantConfig() + + config.set_values("python", ["3.9", "3.10", "3.11"]) + + values = config.get_values("python") + assert values is not None + assert len(values) == 3 + + +def test_variant_config_get_nonexistent_key() -> None: + """Test getting values for a key that doesn't exist.""" + config = VariantConfig() + + values = config.get_values("nonexistent") + assert values is None + + +def test_variant_config_to_dict() -> None: + """Test converting VariantConfig to dictionary.""" + config = VariantConfig() + + config.set_values("python", ["3.10", "3.11"]) + config.set_values("rust", ["1.70", "1.71"]) + + config_dict = config.to_dict() + assert isinstance(config_dict, dict) + assert "python" in config_dict + assert "rust" in config_dict + + +def test_variant_config_merge() -> None: + """Test merging two VariantConfigs.""" + config1 = VariantConfig() + config1.set_values("python", ["3.9", "3.10"]) + + config2 = VariantConfig() + config2.set_values("numpy", ["1.21", "1.22"]) + config2.set_values("cuda", ["11.8", "12.0"]) + + # Merge config2 into config1 + config1.merge(config2) + + # Check that config1 now has all keys + keys = config1.keys() + assert "python" in keys + assert "numpy" in keys + assert "cuda" in keys + + +def test_variant_config_combinations() -> None: + """Test generating variant combinations.""" + config = VariantConfig() + + config.set_values("python", ["3.9", "3.10"]) + config.set_values("numpy", ["1.21", "1.22"]) + + combinations = config.combinations() + + # Should have 2 * 2 = 4 combinations + assert len(combinations) == 4 + + # Each combination should be a dict + for combo in combinations: + assert isinstance(combo, dict) + assert "python" in combo + assert "numpy" in combo + + +def test_variant_config_from_yaml() -> None: + """Test loading VariantConfig from YAML string.""" + yaml_content = """ +python: + - "3.9" + - "3.10" + - "3.11" +numpy: + - "1.21" + - "1.22" +""" + + config = VariantConfig.from_yaml(yaml_content) + + keys = config.keys() + assert "python" in keys + assert "numpy" in keys + + python_values = config.get_values("python") + assert python_values is not None + assert len(python_values) == 3 + + +def test_variant_config_from_file() -> None: + """Test loading VariantConfig from a file.""" + yaml_content = """ +python: + - "3.10" + - "3.11" +rust: + - "1.70" +""" + + # Create a temporary file + with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: + f.write(yaml_content) + temp_path = Path(f.name) + + try: + config = VariantConfig.from_file(temp_path) + + keys = config.keys() + assert "python" in keys + assert "rust" in keys + finally: + # Clean up + temp_path.unlink() + + +def test_variant_config_with_different_types() -> None: + """Test setting variant values with different types.""" + config = VariantConfig() + + # Strings + config.set_values("version", ["1.0", "2.0"]) + + # The values should be stored + values = config.get_values("version") + assert values is not None + assert len(values) == 2 + + +def test_variant_config_len() -> None: + """Test the __len__ method.""" + config = VariantConfig() + assert len(config) == 0 + + config.set_values("python", ["3.9"]) + assert len(config) == 1 + + config.set_values("numpy", ["1.21"]) + assert len(config) == 2 + + +def test_variant_config_repr() -> None: + """Test the string representation.""" + config = VariantConfig() + config.set_values("python", ["3.10"]) + + repr_str = repr(config) + assert "VariantConfig" in repr_str + assert "keys=1" in repr_str + + +def test_variant_config_empty_combinations() -> None: + """Test combinations on empty config.""" + config = VariantConfig() + + combinations = config.combinations() + + # Empty config should give one empty combination + assert len(combinations) >= 0 + + +def test_variant_config_zip_keys() -> None: + """Test zip_keys functionality.""" + config = VariantConfig() + + # Initially, zip_keys should be None + assert config.zip_keys is None + + # Set variant values + config.set_values("python", ["3.9", "3.10", "3.11"]) + config.set_values("numpy", ["1.20", "1.21", "1.22"]) + + # Without zip_keys, we get all combinations (3 * 3 = 9) + combinations = config.combinations() + assert len(combinations) == 9 + + # Set zip_keys to synchronize python and numpy + config.zip_keys = [["python", "numpy"]] + assert config.zip_keys == [["python", "numpy"]] + + # With zip_keys, we get only synchronized combinations (3) + combinations = config.combinations() + assert len(combinations) == 3 + + # Verify the combinations are synchronized + for i, combo in enumerate(combinations): + assert combo["python"] == ["3.9", "3.10", "3.11"][i] + assert combo["numpy"] == ["1.20", "1.21", "1.22"][i] + + +def test_variant_config_from_yaml_with_zip_keys() -> None: + """Test loading VariantConfig from YAML with zip_keys.""" + yaml_content = """ +python: + - "3.9" + - "3.10" +numpy: + - "1.20" + - "1.21" +zip_keys: + - [python, numpy] +""" + + config = VariantConfig.from_yaml(yaml_content) + + # Check that zip_keys were parsed correctly + assert config.zip_keys is not None + assert len(config.zip_keys) == 1 + assert config.zip_keys[0] == ["python", "numpy"] + + # Check that combinations are synchronized + combinations = config.combinations() + assert len(combinations) == 2 # Not 4 + + +def test_variant_config_from_file_with_context() -> None: + """Test loading VariantConfig with JinjaConfig context.""" + from rattler_build import JinjaConfig + + yaml_content = """ +c_compiler: + - if: unix + then: gcc + - if: win + then: msvc +""" + + # Create a temporary file + with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: + f.write(yaml_content) + temp_path = Path(f.name) + + try: + # Load with Linux context + jinja_config_linux = JinjaConfig(target_platform="linux-64") + config_linux = VariantConfig.from_file_with_context(temp_path, jinja_config_linux) + + values_linux = config_linux.get_values("c_compiler") + assert values_linux is not None + assert "gcc" in values_linux + assert "msvc" not in values_linux + + # Load with Windows context + jinja_config_win = JinjaConfig(target_platform="win-64") + config_win = VariantConfig.from_file_with_context(temp_path, jinja_config_win) + + values_win = config_win.get_values("c_compiler") + assert values_win is not None + assert "msvc" in values_win + assert "gcc" not in values_win + finally: + # Clean up + temp_path.unlink() + + +def test_variant_config_from_yaml_with_context() -> None: + """Test loading VariantConfig from YAML string with JinjaConfig context.""" + from rattler_build import JinjaConfig + + yaml_content = """ +c_compiler: + - if: unix + then: gcc + - if: win + then: msvc +cxx_compiler: + - if: unix + then: gxx + - if: win + then: msvc +""" + + # Load with Linux context + jinja_config_linux = JinjaConfig(target_platform="linux-64") + config_linux = VariantConfig.from_yaml_with_context(yaml_content, jinja_config_linux) + + c_values = config_linux.get_values("c_compiler") + assert c_values is not None + assert "gcc" in c_values + + cxx_values = config_linux.get_values("cxx_compiler") + assert cxx_values is not None + assert "gxx" in cxx_values + + +def test_variant_config_from_conda_build_config() -> None: + """Test loading conda_build_config.yaml format with selectors.""" + from rattler_build import JinjaConfig + + yaml_content = """ +python: + - 3.9 + - 3.10 # [unix] + - 3.11 # [osx] +c_compiler: + - gcc # [linux] + - clang # [osx] + - vs2019 # [win] +""" + + # Create a temporary file + with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: + f.write(yaml_content) + temp_path = Path(f.name) + + try: + # Load with Linux context + jinja_config_linux = JinjaConfig(target_platform="linux-64") + config_linux = VariantConfig.from_conda_build_config(temp_path, jinja_config_linux) + + python_values = config_linux.get_values("python") + assert python_values is not None + assert len(python_values) == 2 # 3.9 and 3.10 (unix selector) + + c_compiler_values = config_linux.get_values("c_compiler") + assert c_compiler_values is not None + assert "gcc" in c_compiler_values + assert "clang" not in c_compiler_values + assert "vs2019" not in c_compiler_values + + # Load with macOS context + jinja_config_osx = JinjaConfig(target_platform="osx-64") + config_osx = VariantConfig.from_conda_build_config(temp_path, jinja_config_osx) + + python_values_osx = config_osx.get_values("python") + assert python_values_osx is not None + assert len(python_values_osx) == 3 # 3.9, 3.10 (unix), and 3.11 (osx) + + c_compiler_values_osx = config_osx.get_values("c_compiler") + assert c_compiler_values_osx is not None + assert "clang" in c_compiler_values_osx + assert "gcc" not in c_compiler_values_osx + + # Load with Windows context + jinja_config_win = JinjaConfig(target_platform="win-64") + config_win = VariantConfig.from_conda_build_config(temp_path, jinja_config_win) + + python_values_win = config_win.get_values("python") + assert python_values_win is not None + assert len(python_values_win) == 1 # Only 3.9 (no unix/osx selectors match) + + c_compiler_values_win = config_win.get_values("c_compiler") + assert c_compiler_values_win is not None + assert "vs2019" in c_compiler_values_win + finally: + # Clean up + temp_path.unlink() + + +def test_variant_config_multiple_zip_key_groups() -> None: + """Test multiple zip_key groups.""" + config = VariantConfig() + + # Set variant values + config.set_values("python", ["3.9", "3.10"]) + config.set_values("numpy", ["1.20", "1.21"]) + config.set_values("c_compiler", ["gcc", "clang"]) + config.set_values("cxx_compiler", ["gxx", "clangxx"]) + + # Set multiple zip_key groups + config.zip_keys = [["python", "numpy"], ["c_compiler", "cxx_compiler"]] + + # Should get 2 * 2 = 4 combinations (not 2 * 2 * 2 * 2 = 16) + combinations = config.combinations() + assert len(combinations) == 4 + + # Check that synchronization is preserved + for combo in combinations: + # python-numpy should be synchronized + if combo["python"] == "3.9": + assert combo["numpy"] == "1.20" + else: + assert combo["numpy"] == "1.21" + + # c_compiler-cxx_compiler should be synchronized + if combo["c_compiler"] == "gcc": + assert combo["cxx_compiler"] == "gxx" + else: + assert combo["cxx_compiler"] == "clangxx"