From d3180a5672817c396906f9cf8f604a9b64d77f44 Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Sun, 24 May 2026 10:54:06 -0700 Subject: [PATCH 01/44] feat: scaffold hm-dsl-engine crate with DslEngine trait --- Cargo.lock | 1498 ++++++++++++++++++++++++++++++- Cargo.toml | 5 + crates/hm-dsl-engine/Cargo.toml | 43 + crates/hm-dsl-engine/src/lib.rs | 85 ++ crates/hm/Cargo.toml | 3 + 5 files changed, 1622 insertions(+), 12 deletions(-) create mode 100644 crates/hm-dsl-engine/Cargo.toml create mode 100644 crates/hm-dsl-engine/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index cb921358..4acbf275 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + [[package]] name = "adler2" version = "2.0.1" @@ -17,6 +26,18 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "ambient-authority" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -41,7 +62,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0a8acea8c2f1c60f0a92a8cd26bf96ca97db56f10bbcab238bbe0cceba659ee" dependencies = [ - "nom", + "nom 7.1.3", "vte", ] @@ -101,6 +122,21 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "ar_archive_writer" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" +dependencies = [ + "object 0.37.3", +] + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + [[package]] name = "arrayvec" version = "0.7.6" @@ -272,6 +308,26 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + [[package]] name = "bitflags" version = "2.11.1" @@ -344,9 +400,12 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +dependencies = [ + "allocator-api2", +] [[package]] name = "bytes" @@ -354,6 +413,93 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "cap-fs-ext" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5528f85b1e134ae811704e41ef80930f56e795923f866813255bc342cc20654" +dependencies = [ + "cap-primitives", + "cap-std", + "io-lifetimes", + "windows-sys 0.59.0", +] + +[[package]] +name = "cap-net-ext" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20a158160765c6a7d0d8c072a53d772e4cb243f38b04bfcf6b4939cfbe7482e7" +dependencies = [ + "cap-primitives", + "cap-std", + "rustix 1.1.4", + "smallvec", +] + +[[package]] +name = "cap-primitives" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cf3aea8a5081171859ef57bc1606b1df6999df4f1110f8eef68b30098d1d3a" +dependencies = [ + "ambient-authority", + "fs-set-times", + "io-extras", + "io-lifetimes", + "ipnet", + "maybe-owned", + "rustix 1.1.4", + "rustix-linux-procfs", + "windows-sys 0.59.0", + "winx", +] + +[[package]] +name = "cap-rand" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8144c22e24bbcf26ade86cb6501a0916c46b7e4787abdb0045a467eb1645a1d" +dependencies = [ + "ambient-authority", + "rand 0.8.6", +] + +[[package]] +name = "cap-std" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6dc3090992a735d23219de5c204927163d922f42f575a0189b005c62d37549a" +dependencies = [ + "cap-primitives", + "io-extras", + "io-lifetimes", + "rustix 1.1.4", +] + +[[package]] +name = "cap-time-ext" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "def102506ce40c11710a9b16e614af0cde8e76ae51b1f48c04b8d79f4b671a80" +dependencies = [ + "ambient-authority", + "cap-primitives", + "iana-time-zone", + "once_cell", + "rustix 1.1.4", + "winx", +] + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.2.60" @@ -442,6 +588,15 @@ dependencies = [ "cc", ] +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.18", +] + [[package]] name = "colorchoice" version = "1.0.5" @@ -471,6 +626,20 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "compact_str" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + [[package]] name = "compression-codecs" version = "0.4.37" @@ -538,6 +707,12 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cow-utils" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "417bef24afe1460300965a25ff4a24b8b45ad011948302ec221e8a0a81eb2c79" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -547,6 +722,142 @@ dependencies = [ "libc", ] +[[package]] +name = "cranelift-assembler-x64" +version = "0.120.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5023e06632d8f351c2891793ccccfe4aef957954904392434038745fb6f1f68" +dependencies = [ + "cranelift-assembler-x64-meta", +] + +[[package]] +name = "cranelift-assembler-x64-meta" +version = "0.120.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c4012b4c8c1f6eb05c0a0a540e3e1ee992631af51aa2bbb3e712903ce4fd65" +dependencies = [ + "cranelift-srcgen", +] + +[[package]] +name = "cranelift-bforest" +version = "0.120.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d6d883b4942ef3a7104096b8bc6f2d1a41393f159ac8de12aed27b25d67f895" +dependencies = [ + "cranelift-entity", +] + +[[package]] +name = "cranelift-bitset" +version = "0.120.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db7b2ee9eec6ca8a716d900d5264d678fb2c290c58c46c8da7f94ee268175d17" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "cranelift-codegen" +version = "0.120.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeda0892577afdce1ac2e9a983a55f8c5b87a59334e1f79d8f735a2d7ba4f4b4" +dependencies = [ + "bumpalo", + "cranelift-assembler-x64", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", + "hashbrown 0.15.5", + "log", + "pulley-interpreter", + "regalloc2", + "rustc-hash", + "serde", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.120.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e461480d87f920c2787422463313326f67664e68108c14788ba1676f5edfcd15" +dependencies = [ + "cranelift-assembler-x64-meta", + "cranelift-codegen-shared", + "cranelift-srcgen", + "pulley-interpreter", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.120.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976584d09f200c6c84c4b9ff7af64fc9ad0cb64dffa5780991edd3fe143a30a1" + +[[package]] +name = "cranelift-control" +version = "0.120.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46d43d70f4e17c545aa88dbf4c84d4200755d27c6e3272ebe4de65802fa6a955" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.120.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75418674520cb400c8772bfd6e11a62736c78fc1b6e418195696841d1bf91f1" +dependencies = [ + "cranelift-bitset", + "serde", + "serde_derive", +] + +[[package]] +name = "cranelift-frontend" +version = "0.120.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8b1a91c86687a344f3c52dd6dfb6e50db0dfa7f2e9c7711b060b3623e1fdeb" +dependencies = [ + "cranelift-codegen", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.120.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711baa4e3432d4129295b39ec2b4040cc1b558874ba0a37d08e832e857db7285" + +[[package]] +name = "cranelift-native" +version = "0.120.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41c83e8666e3bcc5ffeaf6f01f356f0e1f9dcd69ce5511a1efd7ca5722001a3f" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + +[[package]] +name = "cranelift-srcgen" +version = "0.120.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e3f4d783a55c64266d17dc67d2708852235732a100fc40dd9f1051adc64d7b" + [[package]] name = "crc32fast" version = "1.5.0" @@ -704,6 +1015,16 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + [[package]] name = "dirs" version = "6.0.0" @@ -721,10 +1042,21 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users", + "redox_users 0.5.2", "windows-sys 0.61.2", ] +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users 0.4.6", + "winapi", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -751,6 +1083,12 @@ dependencies = [ "litrs", ] +[[package]] +name = "dragonbox_ecma" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d742b56656e8b14d63e7ea9806597b1849ae25412584c8adf78c0f67bd985e66" + [[package]] name = "dunce" version = "1.0.5" @@ -769,12 +1107,33 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "encode_unicode" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -791,12 +1150,29 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + [[package]] name = "fastrand" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +[[package]] +name = "fd-lock" +version = "4.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" +dependencies = [ + "cfg-if", + "rustix 1.1.4", + "windows-sys 0.59.0", +] + [[package]] name = "filetime" version = "0.2.27" @@ -860,6 +1236,17 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs-set-times" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" +dependencies = [ + "io-lifetimes", + "rustix 1.1.4", + "windows-sys 0.59.0", +] + [[package]] name = "fs2" version = "0.4.3" @@ -1023,6 +1410,17 @@ dependencies = [ "wasip3", ] +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +dependencies = [ + "fallible-iterator", + "indexmap 2.14.0", + "stable_deref_trait", +] + [[package]] name = "globset" version = "0.4.18" @@ -1101,6 +1499,7 @@ dependencies = [ "futures", "futures-util", "hex", + "hm-dsl-engine", "hm-pipeline-ir", "hm-plugin-cloud", "hm-plugin-protocol", @@ -1149,6 +1548,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "foldhash", + "serde", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", ] [[package]] @@ -1175,6 +1584,28 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hm-dsl-engine" +version = "0.0.0-dev" +dependencies = [ + "anyhow", + "async-trait", + "dirs", + "oxc_allocator", + "oxc_parser", + "oxc_span", + "oxc_transformer", + "reqwest", + "serde", + "serde_json", + "sha2", + "tempfile", + "tokio", + "tracing", + "wasmtime", + "wasmtime-wasi", +] + [[package]] name = "hm-pipeline-ir" version = "0.0.0-dev" @@ -1573,6 +2004,22 @@ dependencies = [ "tempfile", ] +[[package]] +name = "io-extras" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2285ddfe3054097ef4b2fe909ef8c3bcd1ea52a8f0d274416caebeef39f04a65" +dependencies = [ + "io-lifetimes", + "windows-sys 0.59.0", +] + +[[package]] +name = "io-lifetimes" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" + [[package]] name = "ipnet" version = "2.12.0" @@ -1612,6 +2059,15 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -1707,6 +2163,12 @@ version = "0.2.185" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libredox" version = "0.1.16" @@ -1764,6 +2226,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + [[package]] name = "matchers" version = "0.2.0" @@ -1779,12 +2250,27 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +[[package]] +name = "maybe-owned" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" + [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memfd" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" +dependencies = [ + "rustix 1.1.4", +] + [[package]] name = "mime" version = "0.3.17" @@ -1856,6 +2342,21 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "nonmax" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "610a5acd306ec67f907abe5567859a3c693fb9886eb1f012ab8f2a47bef3db51" + [[package]] name = "normalize-line-endings" version = "0.3.0" @@ -1871,12 +2372,31 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-conv" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1927,6 +2447,27 @@ dependencies = [ "objc2", ] +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "crc32fast", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "memchr", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -1961,6 +2502,306 @@ dependencies = [ "supports-color 3.0.2", ] +[[package]] +name = "oxc-browserslist" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd843fdc95833f2faf4251b701a812d488ef35958144af91b9ff540d9fe8ceaa" +dependencies = [ + "bincode", + "flate2", + "nom 8.0.0", + "rustc-hash", + "serde", + "serde_json", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "oxc-miette" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4356a61f2ed4c9b3610245215fbf48970eb277126919f87db9d0efa93a74245c" +dependencies = [ + "cfg-if", + "owo-colors", + "oxc-miette-derive", + "textwrap", + "thiserror 2.0.18", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "oxc-miette-derive" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b237422b014f8f8fff75bb9379e697d13f8d57551a22c88bebb39f073c1bf696" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "oxc_allocator" +version = "0.100.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4f1190f7827f25e629c6482be1899c0f0dd533bba2e8f4f19ee092b066c70a" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.16.1", + "oxc_data_structures", + "rustc-hash", +] + +[[package]] +name = "oxc_ast" +version = "0.100.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00897f7e7adfe2a96964ef0f2b96e9abb4122efd32a1d7245534265611c1dc29" +dependencies = [ + "bitflags", + "oxc_allocator", + "oxc_ast_macros", + "oxc_data_structures", + "oxc_diagnostics", + "oxc_estree", + "oxc_regular_expression", + "oxc_span", + "oxc_syntax", +] + +[[package]] +name = "oxc_ast_macros" +version = "0.100.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7534491e8e2967628667105ce85c51a0fbec0eb9285b9519c5a8c40ec950fcda" +dependencies = [ + "phf", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "oxc_ast_visit" +version = "0.100.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be2ccba62dfa57a9cb4150a92259a35895042e110a8ae2d9c9e756ce1213457f" +dependencies = [ + "oxc_allocator", + "oxc_ast", + "oxc_span", + "oxc_syntax", +] + +[[package]] +name = "oxc_compat" +version = "0.100.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ec5f6a584c21e5a0d772d2c7de90545e42fc0a5171e6fea8feba579bcd58eae" +dependencies = [ + "cow-utils", + "oxc-browserslist", + "oxc_syntax", + "rustc-hash", + "serde", +] + +[[package]] +name = "oxc_data_structures" +version = "0.100.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "265cc1ca3fe1b037736fac2b480e09b7cbc6e707dcf613d174a273819443e34e" +dependencies = [ + "ropey", +] + +[[package]] +name = "oxc_diagnostics" +version = "0.100.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2348244a22cab56e28c7ddda4f1c2944fa22b146f905859b20b969291702de1e" +dependencies = [ + "cow-utils", + "oxc-miette", + "percent-encoding", +] + +[[package]] +name = "oxc_ecmascript" +version = "0.100.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa9634e4cae82b2ea110ce53b3706a3ff3688efa9bcdfde404fee6b7046b398" +dependencies = [ + "cow-utils", + "num-bigint", + "num-traits", + "oxc_allocator", + "oxc_ast", + "oxc_span", + "oxc_syntax", +] + +[[package]] +name = "oxc_estree" +version = "0.100.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45a4a164ff3428c355a657274242e21b9c442ea211af3f473c34b7d4950a02a3" + +[[package]] +name = "oxc_index" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3e6120999627ec9703025eab7c9f410ebb7e95557632a8902ca48210416c2b" +dependencies = [ + "nonmax", + "serde", +] + +[[package]] +name = "oxc_parser" +version = "0.100.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f7b80e7ccac6dce38f24a167e4f340cde84e5f88203fab66b9d1dca250c77f" +dependencies = [ + "bitflags", + "cow-utils", + "memchr", + "num-bigint", + "num-traits", + "oxc_allocator", + "oxc_ast", + "oxc_data_structures", + "oxc_diagnostics", + "oxc_ecmascript", + "oxc_regular_expression", + "oxc_span", + "oxc_syntax", + "rustc-hash", + "seq-macro", +] + +[[package]] +name = "oxc_regular_expression" +version = "0.100.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efe446c68b7ddfb8f547b4216723db8923262916aa3745285c4a89740e9f99b2" +dependencies = [ + "bitflags", + "oxc_allocator", + "oxc_ast_macros", + "oxc_diagnostics", + "oxc_span", + "phf", + "rustc-hash", + "unicode-id-start", +] + +[[package]] +name = "oxc_semantic" +version = "0.100.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "176a90472a6095c26ba93e5bd0d066eb238722fc408415f59994306627161761" +dependencies = [ + "itertools", + "oxc_allocator", + "oxc_ast", + "oxc_ast_visit", + "oxc_data_structures", + "oxc_diagnostics", + "oxc_ecmascript", + "oxc_index", + "oxc_span", + "oxc_syntax", + "phf", + "rustc-hash", + "self_cell", +] + +[[package]] +name = "oxc_span" +version = "0.100.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81d505df37f3383fb3e64c846f4a4be208ac398a7df1f294e88f581ea367c2bb" +dependencies = [ + "compact_str", + "oxc-miette", + "oxc_allocator", + "oxc_ast_macros", + "oxc_estree", +] + +[[package]] +name = "oxc_syntax" +version = "0.100.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "550af62244a879aa5bb48524df380cc16da012ee0e04b6d381336db5e02afc0b" +dependencies = [ + "bitflags", + "cow-utils", + "dragonbox_ecma", + "nonmax", + "oxc_allocator", + "oxc_ast_macros", + "oxc_data_structures", + "oxc_estree", + "oxc_index", + "oxc_span", + "phf", + "unicode-id-start", +] + +[[package]] +name = "oxc_transformer" +version = "0.100.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34d1a230c3796c820c3eb234ea74e1543a6c2fe234908f5289fa52877c549bcb" +dependencies = [ + "base64", + "compact_str", + "indexmap 2.14.0", + "itoa", + "memchr", + "oxc_allocator", + "oxc_ast", + "oxc_ast_visit", + "oxc_compat", + "oxc_data_structures", + "oxc_diagnostics", + "oxc_ecmascript", + "oxc_regular_expression", + "oxc_semantic", + "oxc_span", + "oxc_syntax", + "oxc_traverse", + "rustc-hash", + "serde", + "serde_json", + "sha1", +] + +[[package]] +name = "oxc_traverse" +version = "0.100.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fef8dd8934b5f2eb0cb14b01039a475661e9083f52bbb407bfc45fe963aa238" +dependencies = [ + "itoa", + "oxc_allocator", + "oxc_ast", + "oxc_ast_visit", + "oxc_data_structures", + "oxc_ecmascript", + "oxc_semantic", + "oxc_span", + "oxc_syntax", + "rustc-hash", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -2003,12 +2844,61 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + [[package]] name = "plain" version = "0.2.3" @@ -2021,6 +2911,18 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + [[package]] name = "potential_utf" version = "0.1.5" @@ -2094,6 +2996,27 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "psm" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645dbe486e346d9b5de3ef16ede18c26e6c70ad97418f4874b8b1889d6e761ea" +dependencies = [ + "ar_archive_writer", + "cc", +] + +[[package]] +name = "pulley-interpreter" +version = "33.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "986beaef947a51d17b42b0ea18ceaa88450d35b6994737065ed505c39172db71" +dependencies = [ + "cranelift-bitset", + "log", + "wasmtime-math", +] + [[package]] name = "quinn" version = "0.11.9" @@ -2248,6 +3171,17 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + [[package]] name = "redox_users" version = "0.5.2" @@ -2279,6 +3213,20 @@ dependencies = [ "syn", ] +[[package]] +name = "regalloc2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5216b1837de2149f8bc8e6d5f88a9326b63b8c836ed58ce4a0a29ec736a59734" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.15.5", + "log", + "rustc-hash", + "smallvec", +] + [[package]] name = "regex" version = "1.12.3" @@ -2364,6 +3312,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "ropey" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93411e420bcd1a75ddd1dc3caf18c23155eda2c090631a85af21ba19e97093b5" +dependencies = [ + "smallvec", + "str_indices", +] + [[package]] name = "rustc-hash" version = "2.1.2" @@ -2405,6 +3363,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustix-linux-procfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056" +dependencies = [ + "once_cell", + "rustix 1.1.4", +] + [[package]] name = "rustls" version = "0.23.38" @@ -2593,6 +3561,12 @@ dependencies = [ "libc", ] +[[package]] +name = "self_cell" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" + [[package]] name = "semver" version = "1.0.28" @@ -2603,6 +3577,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "seq-macro" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc" + [[package]] name = "serde" version = "1.0.228" @@ -2788,6 +3768,12 @@ version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + [[package]] name = "slab" version = "0.4.12" @@ -2799,6 +3785,9 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] [[package]] name = "smart-default" @@ -2811,6 +3800,12 @@ dependencies = [ "syn", ] +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + [[package]] name = "socket2" version = "0.6.3" @@ -2821,12 +3816,30 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "sptr" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a" + [[package]] name = "stable_deref_trait" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "str_indices" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d08889ec5408683408db66ad89e0e1f93dff55c73a4ccc71c427d5b277ee47e6" + [[package]] name = "strsim" version = "0.11.1" @@ -2889,6 +3902,22 @@ dependencies = [ "syn", ] +[[package]] +name = "system-interface" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4592f674ce18521c2a81483873a49596655b179f71c5e05d10c1fe66c78745" +dependencies = [ + "bitflags", + "cap-fs-ext", + "cap-std", + "fd-lock", + "io-lifetimes", + "rustix 0.38.44", + "windows-sys 0.59.0", + "winx", +] + [[package]] name = "tar" version = "0.4.45" @@ -2900,6 +3929,12 @@ dependencies = [ "xattr", ] +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + [[package]] name = "tempfile" version = "3.27.0" @@ -2913,6 +3948,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "terminal_size" version = "0.4.4" @@ -2929,6 +3973,17 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -3238,6 +4293,17 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "trait-variant" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -3256,12 +4322,24 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" +[[package]] +name = "unicode-id-start" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81b79ad29b5e19de4260020f8919b443b2ef0277d242ce532ec7b7a2cc8b6007" + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + [[package]] name = "unicode-segmentation" version = "1.13.2" @@ -3286,6 +4364,12 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + [[package]] name = "ureq" version = "2.12.1" @@ -3349,6 +4433,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + [[package]] name = "vte" version = "0.14.1" @@ -3466,6 +4556,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.229.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38ba1d491ecacb085a2552025c10a675a6fddcbd03b1fc9b36c536010ce265d2" +dependencies = [ + "leb128fmt", + "wasmparser 0.229.0", +] + [[package]] name = "wasm-encoder" version = "0.244.0" @@ -3473,7 +4573,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" dependencies = [ "leb128fmt", - "wasmparser", + "wasmparser 0.244.0", ] [[package]] @@ -3484,8 +4584,8 @@ checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", "indexmap 2.14.0", - "wasm-encoder", - "wasmparser", + "wasm-encoder 0.244.0", + "wasmparser 0.244.0", ] [[package]] @@ -3501,6 +4601,19 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasmparser" +version = "0.229.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc3b1f053f5d41aa55640a1fa9b6d1b8a9e4418d118ce308d20e24ff3575a8c" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "semver", + "serde", +] + [[package]] name = "wasmparser" version = "0.244.0" @@ -3513,6 +4626,292 @@ dependencies = [ "semver", ] +[[package]] +name = "wasmprinter" +version = "0.229.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25dac01892684a99b8fbfaf670eb6b56edea8a096438c75392daeb83156ae2e" +dependencies = [ + "anyhow", + "termcolor", + "wasmparser 0.229.0", +] + +[[package]] +name = "wasmtime" +version = "33.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57373e1d8699662fb791270ac5dfac9da5c14f618ecf940cdb29dc3ad9472a3c" +dependencies = [ + "addr2line", + "anyhow", + "async-trait", + "bitflags", + "bumpalo", + "cc", + "cfg-if", + "encoding_rs", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "libc", + "log", + "mach2", + "memfd", + "object 0.36.7", + "once_cell", + "postcard", + "psm", + "pulley-interpreter", + "rustix 1.1.4", + "semver", + "serde", + "serde_derive", + "smallvec", + "sptr", + "target-lexicon", + "trait-variant", + "wasmparser 0.229.0", + "wasmtime-asm-macros", + "wasmtime-cache", + "wasmtime-component-macro", + "wasmtime-component-util", + "wasmtime-cranelift", + "wasmtime-environ", + "wasmtime-fiber", + "wasmtime-jit-icache-coherence", + "wasmtime-math", + "wasmtime-slab", + "wasmtime-versioned-export-macros", + "wasmtime-winch", + "windows-sys 0.59.0", +] + +[[package]] +name = "wasmtime-asm-macros" +version = "33.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0fc91372865167a695dc98d0d6771799a388a7541d3f34e939d0539d6583de" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "wasmtime-cache" +version = "33.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c90a5ce3e570f1d2bfd037d0b57d06460ee980eab6ffe138bcb734bb72b312" +dependencies = [ + "anyhow", + "base64", + "directories-next", + "log", + "postcard", + "rustix 1.1.4", + "serde", + "serde_derive", + "sha2", + "toml", + "windows-sys 0.59.0", + "zstd", +] + +[[package]] +name = "wasmtime-component-macro" +version = "33.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25c9c7526675ff9a9794b115023c4af5128e3eb21389bfc3dc1fd344d549258f" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn", + "wasmtime-component-util", + "wasmtime-wit-bindgen", + "wit-parser 0.229.0", +] + +[[package]] +name = "wasmtime-component-util" +version = "33.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc42ec8b078875804908d797cb4950fec781d9add9684c9026487fd8eb3f6291" + +[[package]] +name = "wasmtime-cranelift" +version = "33.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2bd72f0a6a0ffcc6a184ec86ac35c174e48ea0e97bbae277c8f15f8bf77a566" +dependencies = [ + "anyhow", + "cfg-if", + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "gimli", + "itertools", + "log", + "object 0.36.7", + "pulley-interpreter", + "smallvec", + "target-lexicon", + "thiserror 2.0.18", + "wasmparser 0.229.0", + "wasmtime-environ", + "wasmtime-versioned-export-macros", +] + +[[package]] +name = "wasmtime-environ" +version = "33.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6187bb108a23eb25d2a92aa65d6c89fb5ed53433a319038a2558567f3011ff2" +dependencies = [ + "anyhow", + "cranelift-bitset", + "cranelift-entity", + "gimli", + "indexmap 2.14.0", + "log", + "object 0.36.7", + "postcard", + "semver", + "serde", + "serde_derive", + "smallvec", + "target-lexicon", + "wasm-encoder 0.229.0", + "wasmparser 0.229.0", + "wasmprinter", + "wasmtime-component-util", +] + +[[package]] +name = "wasmtime-fiber" +version = "33.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc8965d2128c012329f390e24b8b2758dd93d01bf67e1a1a0dd3d8fd72f56873" +dependencies = [ + "anyhow", + "cc", + "cfg-if", + "rustix 1.1.4", + "wasmtime-asm-macros", + "wasmtime-versioned-export-macros", + "windows-sys 0.59.0", +] + +[[package]] +name = "wasmtime-jit-icache-coherence" +version = "33.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7af0e940cb062a45c0b3f01a926f77da5947149e99beb4e3dd9846d5b8f11619" +dependencies = [ + "anyhow", + "cfg-if", + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "wasmtime-math" +version = "33.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acfca360e719dda9a27e26944f2754ff2fd5bad88e21919c42c5a5f38ddd93cb" +dependencies = [ + "libm", +] + +[[package]] +name = "wasmtime-slab" +version = "33.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e240559cada55c4b24af979d5f6c95e0029f5772f32027ec3c62b258aaff65" + +[[package]] +name = "wasmtime-versioned-export-macros" +version = "33.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0963c1438357a3d8c0efe152b4ef5259846c1cf8b864340270744fe5b3bae5e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "wasmtime-wasi" +version = "33.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae951b72c7c6749a1c15dcdfb6d940a2614c932b4a54f474636e78e2c744b4c" +dependencies = [ + "anyhow", + "async-trait", + "bitflags", + "bytes", + "cap-fs-ext", + "cap-net-ext", + "cap-rand", + "cap-std", + "cap-time-ext", + "fs-set-times", + "futures", + "io-extras", + "io-lifetimes", + "rustix 1.1.4", + "system-interface", + "thiserror 2.0.18", + "tokio", + "tracing", + "url", + "wasmtime", + "wasmtime-wasi-io", + "windows-sys 0.59.0", +] + +[[package]] +name = "wasmtime-wasi-io" +version = "33.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a835790dcecc3d7051ec67da52ba9e04af25e1bc204275b9391e3f0042b10797" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "futures", + "wasmtime", +] + +[[package]] +name = "wasmtime-winch" +version = "33.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc3b117d03d6eeabfa005a880c5c22c06503bb8820f3aa2e30f0e8d87b6752f" +dependencies = [ + "anyhow", + "cranelift-codegen", + "gimli", + "object 0.36.7", + "target-lexicon", + "wasmparser 0.229.0", + "wasmtime-cranelift", + "wasmtime-environ", + "winch-codegen", +] + +[[package]] +name = "wasmtime-wit-bindgen" +version = "33.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1382f4f09390eab0d75d4994d0c3b0f6279f86a571807ec67a8253c87cf6a145" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.14.0", + "wit-parser 0.229.0", +] + [[package]] name = "web-sys" version = "0.3.95" @@ -3619,6 +5018,25 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "winch-codegen" +version = "33.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7914c296fbcef59d1b89a15e82384d34dc9669bc09763f2ef068a28dd3a64ebf" +dependencies = [ + "anyhow", + "cranelift-assembler-x64", + "cranelift-codegen", + "gimli", + "regalloc2", + "smallvec", + "target-lexicon", + "thiserror 2.0.18", + "wasmparser 0.229.0", + "wasmtime-cranelift", + "wasmtime-environ", +] + [[package]] name = "windows" version = "0.62.2" @@ -3909,6 +5327,16 @@ version = "0.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" +[[package]] +name = "winx" +version = "0.36.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" +dependencies = [ + "bitflags", + "windows-sys 0.59.0", +] + [[package]] name = "wiremock" version = "0.6.5" @@ -3955,7 +5383,7 @@ checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ "anyhow", "heck", - "wit-parser", + "wit-parser 0.244.0", ] [[package]] @@ -4002,10 +5430,28 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "wasm-encoder", + "wasm-encoder 0.244.0", "wasm-metadata", - "wasmparser", - "wit-parser", + "wasmparser 0.244.0", + "wit-parser 0.244.0", +] + +[[package]] +name = "wit-parser" +version = "0.229.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459c6ba62bf511d6b5f2a845a2a736822e38059c1cfa0b644b467bbbfae4efa6" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.14.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.229.0", ] [[package]] @@ -4023,7 +5469,7 @@ dependencies = [ "serde_derive", "serde_json", "unicode-xid", - "wasmparser", + "wasmparser 0.244.0", ] [[package]] @@ -4150,3 +5596,31 @@ name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index 71a66fc5..0fcbc9d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "crates/hm-pipeline-ir", "crates/hm-util", "crates/hm-plugin-cloud", + "crates/hm-dsl-engine", ] default-members = [ "crates/hm", @@ -13,6 +14,7 @@ default-members = [ "crates/hm-pipeline-ir", "crates/hm-util", "crates/hm-plugin-cloud", + "crates/hm-dsl-engine", ] [workspace.package] @@ -24,7 +26,10 @@ repository = "https://github.com/harmont-dev/harmont-cli" hm-plugin-protocol = { path = "crates/hm-plugin-protocol", version = "0.0.0-dev" } hm-pipeline-ir = { path = "crates/hm-pipeline-ir", version = "0.0.0-dev" } hm-util = { path = "crates/hm-util", version = "0.0.0-dev" } +hm-dsl-engine = { path = "crates/hm-dsl-engine", version = "0.0.0-dev" } anyhow = "1" +async-trait = "0.1" +dirs = "6" daggy = { version = "0.9", features = ["serde-1"] } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/crates/hm-dsl-engine/Cargo.toml b/crates/hm-dsl-engine/Cargo.toml new file mode 100644 index 00000000..fac5b834 --- /dev/null +++ b/crates/hm-dsl-engine/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "hm-dsl-engine" +version = "0.0.0-dev" +edition.workspace = true +license.workspace = true +repository.workspace = true +description = "Embedded DSL engine: run Python/TypeScript pipelines via Wasmtime." +publish = false + +[lib] +path = "src/lib.rs" + +[features] +default = ["embedded-python", "embedded-typescript"] +embedded-python = ["dep:wasmtime", "dep:wasmtime-wasi"] +embedded-typescript = ["dep:wasmtime", "dep:wasmtime-wasi", "dep:oxc_transformer", "dep:oxc_allocator", "dep:oxc_parser", "dep:oxc_span"] + +[dependencies] +# Always-on +anyhow = { workspace = true } +async-trait = { workspace = true } +dirs = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +sha2 = "0.10" +tempfile = "3" +tokio = { version = "1", features = ["process", "fs"] } +tracing = "0.1" +reqwest = { version = "0.13", default-features = false, features = ["stream", "rustls"] } + +# Feature-gated +wasmtime = { version = "33", default-features = false, features = ["cranelift", "cache"], optional = true } +wasmtime-wasi = { version = "33", default-features = false, optional = true } +oxc_transformer = { version = "0.100", optional = true } +oxc_allocator = { version = "0.100", optional = true } +oxc_parser = { version = "0.100", optional = true } +oxc_span = { version = "0.100", optional = true } + +[dev-dependencies] +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } + +[lints] +workspace = true diff --git a/crates/hm-dsl-engine/src/lib.rs b/crates/hm-dsl-engine/src/lib.rs new file mode 100644 index 00000000..710c2e2f --- /dev/null +++ b/crates/hm-dsl-engine/src/lib.rs @@ -0,0 +1,85 @@ +//! Embedded DSL engine: run Python/TypeScript pipeline definitions via Wasmtime. + +use std::path::Path; + +use async_trait::async_trait; +use serde::Deserialize; + +// Feature-gated modules — added as engines are implemented: +// mod detect; (embedded-python | embedded-typescript) +// mod wasm_runtime; (embedded-python | embedded-typescript) +// mod runtime_cache; (embedded-python | embedded-typescript) +// mod python_engine; (embedded-python) +// mod js_engine; (embedded-typescript) +// mod ts_preprocess; (embedded-typescript) + +/// The language a DSL pipeline is written in. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum DslLanguage { + /// `CPython` compiled to WASI. + Python, + /// TypeScript transpiled to JS, run via `QuickJS`-WASI. + TypeScript, +} + +/// Minimal metadata extracted from a DSL pipeline module. +#[derive(Debug, Clone, Deserialize)] +pub struct PipelineMeta { + /// URL-safe identifier (e.g. `"deploy-api"`). + pub slug: String, + /// Human-readable display name. + pub name: String, +} + +/// Trait implemented by each language-specific engine. +#[async_trait] +pub trait DslEngine: Send + Sync { + /// Discover all pipeline definitions under `project_dir`. + async fn list_pipelines(&self, project_dir: &Path) -> anyhow::Result>; + + /// Render a single pipeline to its JSON IR. + async fn render_pipeline_json( + &self, + project_dir: &Path, + slug: &str, + ) -> anyhow::Result; +} + +/// Return an appropriate [`DslEngine`] for the given language. +/// +/// # Errors +/// +/// Returns an error if the requested language feature was not compiled in. +#[allow(clippy::unused_async)] // Will await engine constructors once implemented. +pub async fn engine_for(lang: DslLanguage) -> anyhow::Result> { + match lang { + DslLanguage::Python => { + #[cfg(feature = "embedded-python")] + { + let _ = lang; // silence unused-variable warning in stub + anyhow::bail!("Python engine not yet implemented"); + } + #[cfg(not(feature = "embedded-python"))] + { + anyhow::bail!( + "embedded-python feature is not enabled; \ + rebuild with `--features embedded-python`" + ); + } + } + DslLanguage::TypeScript => { + #[cfg(feature = "embedded-typescript")] + { + let _ = lang; + anyhow::bail!("TypeScript engine not yet implemented"); + } + #[cfg(not(feature = "embedded-typescript"))] + { + anyhow::bail!( + "embedded-typescript feature is not enabled; \ + rebuild with `--features embedded-typescript`" + ); + } + } + } +} diff --git a/crates/hm/Cargo.toml b/crates/hm/Cargo.toml index 49b0842b..b9b73a4b 100644 --- a/crates/hm/Cargo.toml +++ b/crates/hm/Cargo.toml @@ -63,6 +63,7 @@ hm-pipeline-ir = { workspace = true } hm-plugin-protocol = { workspace = true } hm-util = { workspace = true } hm-plugin-cloud = { path = "../hm-plugin-cloud" } +hm-dsl-engine = { workspace = true } daggy = { workspace = true } schemars = { workspace = true } semver = { workspace = true } @@ -74,6 +75,8 @@ hex = "0.4" default = [] py-env = [] docker-integration = [] +embedded-python = ["hm-dsl-engine/embedded-python"] +embedded-typescript = ["hm-dsl-engine/embedded-typescript"] [dev-dependencies] wiremock = "0.6" From c70e21220618479765c4822a41c1275b0ca093c6 Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Sun, 24 May 2026 10:56:47 -0700 Subject: [PATCH 02/44] feat(dsl-engine): detect DSL language from .harmont/ contents --- crates/hm-dsl-engine/src/detect.rs | 119 +++++++++++++++++++++++++++++ crates/hm-dsl-engine/src/lib.rs | 3 +- 2 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 crates/hm-dsl-engine/src/detect.rs diff --git a/crates/hm-dsl-engine/src/detect.rs b/crates/hm-dsl-engine/src/detect.rs new file mode 100644 index 00000000..6d42ff92 --- /dev/null +++ b/crates/hm-dsl-engine/src/detect.rs @@ -0,0 +1,119 @@ +use std::path::Path; + +use anyhow::{Context, bail}; + +use crate::DslLanguage; + +/// Detect the DSL language used in a project by scanning `.harmont/` for file +/// extensions. +/// +/// # Errors +/// +/// - The `.harmont/` directory does not exist. +/// - No `.py` or `.ts` files are found inside `.harmont/`. +/// - Both `.py` and `.ts` files are present (mixed languages). +pub fn detect_language(repo_root: &Path) -> anyhow::Result { + let harmont_dir = repo_root.join(".harmont"); + + if !harmont_dir.is_dir() { + bail!( + "no .harmont/ directory found in {}", + repo_root.display() + ); + } + + let entries = std::fs::read_dir(&harmont_dir) + .with_context(|| format!("failed to read {}", harmont_dir.display()))?; + + let mut has_py = false; + let mut has_ts = false; + + for entry in entries { + let entry = entry?; + let path = entry.path(); + + match path.extension().and_then(|e| e.to_str()) { + Some("py") => has_py = true, + Some("ts") => has_ts = true, + _ => {} + } + } + + match (has_py, has_ts) { + (true, true) => bail!("use one language per project: .harmont/ contains both .py and .ts files"), + (true, false) => Ok(DslLanguage::Python), + (false, true) => Ok(DslLanguage::TypeScript), + (false, false) => bail!( + "no .py or .ts files found in {}", + harmont_dir.display() + ), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + use std::fs; + + /// Helper: create a temp dir with `.harmont/` and the given filenames inside + /// it. + fn setup(files: &[&str]) -> TempDir { + let tmp = TempDir::new().unwrap(); + let harmont = tmp.path().join(".harmont"); + fs::create_dir(&harmont).unwrap(); + for name in files { + fs::write(harmont.join(name), "").unwrap(); + } + tmp + } + + #[test] + fn python_file_detected() { + let tmp = setup(&["ci.py"]); + let lang = detect_language(tmp.path()).unwrap(); + assert_eq!(lang, DslLanguage::Python); + } + + #[test] + fn typescript_file_detected() { + let tmp = setup(&["ci.ts"]); + let lang = detect_language(tmp.path()).unwrap(); + assert_eq!(lang, DslLanguage::TypeScript); + } + + #[test] + fn mixed_languages_is_error() { + let tmp = setup(&["ci.py", "deploy.ts"]); + let err = detect_language(tmp.path()).unwrap_err(); + let msg = err.to_string(); + assert!( + msg.contains("use one language per project"), + "unexpected error: {msg}" + ); + } + + #[test] + fn no_harmont_dir_is_error() { + let tmp = TempDir::new().unwrap(); + // Do NOT create .harmont/ + let err = detect_language(tmp.path()).unwrap_err(); + let msg = err.to_string(); + assert!( + msg.contains("no .harmont/ directory"), + "unexpected error: {msg}" + ); + } + + #[test] + fn empty_harmont_dir_is_error() { + let tmp = TempDir::new().unwrap(); + fs::create_dir(tmp.path().join(".harmont")).unwrap(); + let err = detect_language(tmp.path()).unwrap_err(); + let msg = err.to_string(); + assert!( + msg.contains("no .py or .ts files"), + "unexpected error: {msg}" + ); + } +} diff --git a/crates/hm-dsl-engine/src/lib.rs b/crates/hm-dsl-engine/src/lib.rs index 710c2e2f..461a0ba4 100644 --- a/crates/hm-dsl-engine/src/lib.rs +++ b/crates/hm-dsl-engine/src/lib.rs @@ -5,8 +5,9 @@ use std::path::Path; use async_trait::async_trait; use serde::Deserialize; +pub mod detect; + // Feature-gated modules — added as engines are implemented: -// mod detect; (embedded-python | embedded-typescript) // mod wasm_runtime; (embedded-python | embedded-typescript) // mod runtime_cache; (embedded-python | embedded-typescript) // mod python_engine; (embedded-python) From 837c8d9ffd09b1842389a2df8839162eba56040c Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Sun, 24 May 2026 10:58:49 -0700 Subject: [PATCH 03/44] feat(dsl-engine): WASM runtime download + SHA-256 cache --- Cargo.lock | 1 + crates/hm-dsl-engine/Cargo.toml | 1 + crates/hm-dsl-engine/src/lib.rs | 4 +- crates/hm-dsl-engine/src/runtime_cache.rs | 229 ++++++++++++++++++++++ 4 files changed, 234 insertions(+), 1 deletion(-) create mode 100644 crates/hm-dsl-engine/src/runtime_cache.rs diff --git a/Cargo.lock b/Cargo.lock index 4acbf275..a83f76bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1591,6 +1591,7 @@ dependencies = [ "anyhow", "async-trait", "dirs", + "hex", "oxc_allocator", "oxc_parser", "oxc_span", diff --git a/crates/hm-dsl-engine/Cargo.toml b/crates/hm-dsl-engine/Cargo.toml index fac5b834..3d6c0135 100644 --- a/crates/hm-dsl-engine/Cargo.toml +++ b/crates/hm-dsl-engine/Cargo.toml @@ -22,6 +22,7 @@ async-trait = { workspace = true } dirs = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +hex = "0.4" sha2 = "0.10" tempfile = "3" tokio = { version = "1", features = ["process", "fs"] } diff --git a/crates/hm-dsl-engine/src/lib.rs b/crates/hm-dsl-engine/src/lib.rs index 461a0ba4..9e4eb313 100644 --- a/crates/hm-dsl-engine/src/lib.rs +++ b/crates/hm-dsl-engine/src/lib.rs @@ -9,11 +9,13 @@ pub mod detect; // Feature-gated modules — added as engines are implemented: // mod wasm_runtime; (embedded-python | embedded-typescript) -// mod runtime_cache; (embedded-python | embedded-typescript) // mod python_engine; (embedded-python) // mod js_engine; (embedded-typescript) // mod ts_preprocess; (embedded-typescript) +#[cfg(any(feature = "embedded-python", feature = "embedded-typescript"))] +pub mod runtime_cache; + /// The language a DSL pipeline is written in. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum DslLanguage { diff --git a/crates/hm-dsl-engine/src/runtime_cache.rs b/crates/hm-dsl-engine/src/runtime_cache.rs new file mode 100644 index 00000000..6f2b1a8b --- /dev/null +++ b/crates/hm-dsl-engine/src/runtime_cache.rs @@ -0,0 +1,229 @@ +//! WASM runtime download and SHA-256 verified cache. +//! +//! Downloads WASM interpreter modules (e.g. CPython-WASI), verifies their +//! SHA-256 digest, and caches them under `~/.harmont/runtimes/`. + +use std::path::{Path, PathBuf}; + +use anyhow::{Context, Result, bail}; +use sha2::{Digest, Sha256}; +use tracing::info; + +/// A local cache directory for WASM runtime modules. +#[derive(Debug)] +pub struct RuntimeCache { + base: PathBuf, +} + +/// Describes a WASM runtime module to fetch and verify. +#[derive(Debug, Clone, Copy)] +pub struct RuntimeSpec { + /// Short name, e.g. `"cpython"`. + pub name: &'static str, + /// SemVer version string, e.g. `"3.12.0"`. + pub version: &'static str, + /// HTTPS URL to download the `.wasm` module from. + pub url: &'static str, + /// Expected lowercase hex-encoded SHA-256 of the downloaded file. + pub sha256: &'static str, +} + +/// Placeholder — will be updated with a real hash once we test against the +/// actual cpython-wasi `.wasm` artifact. +pub const CPYTHON_WASI: RuntimeSpec = RuntimeSpec { + name: "cpython", + version: "3.12.0", + url: "https://github.com/nicktimko/nicktimko.github.io/releases/download/cpython-3.12.0-wasi/python-3.12.0-wasi.wasm", + sha256: "0000000000000000000000000000000000000000000000000000000000000000", +}; + +impl RuntimeCache { + /// Create a cache rooted at `base`. The directory is created on first + /// [`ensure`](Self::ensure) call if it does not already exist. + pub fn new(base: PathBuf) -> Self { + Self { base } + } + + /// Default cache location: `~/.harmont/runtimes/`. + /// + /// # Errors + /// + /// Returns an error if the home directory cannot be determined. + pub fn default_path() -> Result { + let home = dirs::home_dir().context("could not determine home directory")?; + Ok(Self { + base: home.join(".harmont").join("runtimes"), + }) + } + + /// Deterministic path where a runtime module would be cached. + pub fn module_path(&self, name: &str, version: &str) -> PathBuf { + self.base.join(format!("{name}-{version}.wasm")) + } + + /// Ensure the runtime described by `spec` is present on disk. + /// + /// 1. If the file already exists and passes SHA-256 verification, return + /// its path immediately. + /// 2. Otherwise download from `spec.url` into a temporary file, verify the + /// digest, and atomically rename it into the cache directory. + /// + /// # Errors + /// + /// - Network / IO failures during download. + /// - SHA-256 mismatch after download. + pub async fn ensure(&self, spec: &RuntimeSpec) -> Result { + let target = self.module_path(spec.name, spec.version); + + // 1. Already cached? + if target.exists() { + verify_sha256(&target, spec.sha256)?; + info!( + runtime = spec.name, + version = spec.version, + "using cached WASM module" + ); + return Ok(target); + } + + // Make sure the cache directory exists. + tokio::fs::create_dir_all(&self.base) + .await + .with_context(|| format!("failed to create cache dir {}", self.base.display()))?; + + // 2. Download to a temp file in the same directory (same filesystem → + // rename is atomic). + let tmp = tempfile::NamedTempFile::new_in(&self.base) + .context("failed to create temp file for WASM download")?; + + info!( + runtime = spec.name, + version = spec.version, + url = spec.url, + "downloading WASM runtime" + ); + + let response = reqwest::get(spec.url) + .await + .with_context(|| format!("failed to GET {}", spec.url))?; + + if !response.status().is_success() { + bail!( + "download failed: {} returned HTTP {}", + spec.url, + response.status() + ); + } + + let bytes = response + .bytes() + .await + .with_context(|| format!("failed to read response body from {}", spec.url))?; + + tokio::fs::write(tmp.path(), &bytes) + .await + .context("failed to write downloaded WASM module to temp file")?; + + // 3. Verify SHA-256. + verify_sha256(tmp.path(), spec.sha256)?; + + // 4. Rename into place. + tmp.persist(&target) + .with_context(|| format!("failed to persist WASM module to {}", target.display()))?; + + info!( + runtime = spec.name, + version = spec.version, + path = %target.display(), + "cached WASM module" + ); + + Ok(target) + } +} + +/// Verify that the SHA-256 digest of `path` matches `expected` (lowercase hex). +/// +/// # Errors +/// +/// - IO failure reading the file. +/// - Digest mismatch. +pub fn verify_sha256(path: &Path, expected: &str) -> Result<()> { + let bytes = std::fs::read(path) + .with_context(|| format!("failed to read {} for SHA-256 check", path.display()))?; + + let digest = Sha256::digest(&bytes); + let actual = hex::encode(digest); + + if actual != expected { + bail!( + "SHA-256 mismatch for {}: expected {expected}, got {actual}", + path.display() + ); + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use tempfile::TempDir; + + #[test] + fn cache_dir_path_is_correct() { + let dir = PathBuf::from("/tmp/test-runtimes"); + let cache = RuntimeCache::new(dir); + let path = cache.module_path("cpython", "3.12.0"); + assert_eq!(path, PathBuf::from("/tmp/test-runtimes/cpython-3.12.0.wasm")); + } + + #[test] + fn sha256_verify_accepts_correct_hash() { + let tmp = TempDir::new().unwrap(); + let file = tmp.path().join("test.wasm"); + let content = b"hello, wasm runtime"; + fs::write(&file, content).unwrap(); + + // Pre-computed SHA-256 of "hello, wasm runtime". + let expected = hex::encode(Sha256::digest(content)); + verify_sha256(&file, &expected).unwrap(); + } + + #[test] + fn sha256_verify_rejects_bad_hash() { + let tmp = TempDir::new().unwrap(); + let file = tmp.path().join("test.wasm"); + fs::write(&file, b"hello, wasm runtime").unwrap(); + + let bad_hash = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + let err = verify_sha256(&file, bad_hash).unwrap_err(); + let msg = err.to_string(); + assert!(msg.contains("SHA-256 mismatch"), "unexpected error: {msg}"); + } + + #[tokio::test] + async fn ensure_returns_cached_path_if_exists() { + let tmp = TempDir::new().unwrap(); + let cache = RuntimeCache::new(tmp.path().to_path_buf()); + + let content = b"fake wasm module bytes"; + let sha = hex::encode(Sha256::digest(content)); + + // Pre-create the cached file. + let expected_path = cache.module_path("cpython", "3.12.0"); + fs::write(&expected_path, content).unwrap(); + + // Build a spec whose sha256 matches. + let spec = RuntimeSpec { + name: "cpython", + version: "3.12.0", + url: "https://example.invalid/should-not-be-fetched", + sha256: Box::leak(sha.into_boxed_str()), + }; + + let result = cache.ensure(&spec).await.unwrap(); + assert_eq!(result, expected_path); + } +} From 893ef6edaff763f4c0a7a289965df910cb27beac Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Sun, 24 May 2026 11:02:32 -0700 Subject: [PATCH 04/44] feat(dsl-engine): wasmtime engine factory with compilation cache --- crates/hm-dsl-engine/src/lib.rs | 4 +- crates/hm-dsl-engine/src/wasm_runtime.rs | 53 ++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 crates/hm-dsl-engine/src/wasm_runtime.rs diff --git a/crates/hm-dsl-engine/src/lib.rs b/crates/hm-dsl-engine/src/lib.rs index 9e4eb313..1c5df775 100644 --- a/crates/hm-dsl-engine/src/lib.rs +++ b/crates/hm-dsl-engine/src/lib.rs @@ -8,11 +8,13 @@ use serde::Deserialize; pub mod detect; // Feature-gated modules — added as engines are implemented: -// mod wasm_runtime; (embedded-python | embedded-typescript) // mod python_engine; (embedded-python) // mod js_engine; (embedded-typescript) // mod ts_preprocess; (embedded-typescript) +#[cfg(any(feature = "embedded-python", feature = "embedded-typescript"))] +pub mod wasm_runtime; + #[cfg(any(feature = "embedded-python", feature = "embedded-typescript"))] pub mod runtime_cache; diff --git a/crates/hm-dsl-engine/src/wasm_runtime.rs b/crates/hm-dsl-engine/src/wasm_runtime.rs new file mode 100644 index 00000000..dd7db016 --- /dev/null +++ b/crates/hm-dsl-engine/src/wasm_runtime.rs @@ -0,0 +1,53 @@ +//! Shared [`wasmtime::Engine`] factory with Cranelift compilation and disk +//! caching. +//! +//! Every embedded DSL engine (Python-WASI, QuickJS-WASI) shares a single +//! `Engine` configuration so compilation artefacts are cached once and reused +//! across runtimes. + +use anyhow::{Context, Result}; +use wasmtime::{Cache, CacheConfig, Config, Engine, OptLevel}; + +/// Create a wasmtime [`Engine`] configured for Cranelift compilation with +/// on-disk caching under `~/.harmont/runtimes/cache/`. +/// +/// # Errors +/// +/// Returns an error if the engine configuration is invalid or if wasmtime +/// cannot be initialised on this platform. +pub fn create_engine() -> Result { + let mut config = Config::new(); + + // Explicitly enable proposals used by WASI runtimes (both are on by + // default in wasmtime 33 but being explicit avoids breakage if defaults + // change). + config.wasm_bulk_memory(true); + config.wasm_multi_value(true); + config.cranelift_opt_level(OptLevel::Speed); + + // Enable compilation caching for fast subsequent runs. + if let Some(home) = dirs::home_dir() { + let cache_dir = home.join(".harmont").join("runtimes").join("cache"); + let _ = std::fs::create_dir_all(&cache_dir); + + let mut cache_config = CacheConfig::new(); + cache_config.with_directory(&cache_dir); + + if let Ok(cache) = Cache::new(cache_config) { + config.cache(Some(cache)); + } + } + + Engine::new(&config).context("creating wasmtime engine") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn engine_creates_successfully() { + let engine = create_engine().expect("engine should be created successfully"); + drop(engine); + } +} From 405795b9542e87393605673556f6f74c3f839c4a Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Sun, 24 May 2026 11:05:56 -0700 Subject: [PATCH 05/44] feat(dsl-engine): embed harmont-py + croniter sources in binary Add compile-time embedding of the harmont Python DSL package and its vendored dependencies (croniter, python-dateutil, six) using include_dir. The WASI Python engine can extract these to a scratch directory without needing filesystem access to the source tree. --- Cargo.lock | 20 + crates/hm-dsl-engine/Cargo.toml | 3 +- crates/hm-dsl-engine/src/embedded_sources.rs | 76 + crates/hm-dsl-engine/src/lib.rs | 3 + .../hm-dsl-engine/vendor/croniter/__init__.py | 41 + .../hm-dsl-engine/vendor/croniter/croniter.py | 1492 +++++++++++++ .../hm-dsl-engine/vendor/dateutil/__init__.py | 24 + .../hm-dsl-engine/vendor/dateutil/_common.py | 43 + .../hm-dsl-engine/vendor/dateutil/_version.py | 4 + .../hm-dsl-engine/vendor/dateutil/easter.py | 89 + .../vendor/dateutil/parser/__init__.py | 61 + .../vendor/dateutil/parser/_parser.py | 1613 ++++++++++++++ .../vendor/dateutil/parser/isoparser.py | 416 ++++ .../vendor/dateutil/relativedelta.py | 599 ++++++ crates/hm-dsl-engine/vendor/dateutil/rrule.py | 1737 ++++++++++++++++ .../vendor/dateutil/tz/__init__.py | 12 + .../vendor/dateutil/tz/_common.py | 419 ++++ .../vendor/dateutil/tz/_factories.py | 80 + crates/hm-dsl-engine/vendor/dateutil/tz/tz.py | 1849 +++++++++++++++++ .../hm-dsl-engine/vendor/dateutil/tz/win.py | 370 ++++ crates/hm-dsl-engine/vendor/dateutil/tzwin.py | 2 + crates/hm-dsl-engine/vendor/dateutil/utils.py | 71 + .../vendor/dateutil/zoneinfo/__init__.py | 167 ++ .../zoneinfo/dateutil-zoneinfo.tar.gz | Bin 0 -> 156400 bytes .../vendor/dateutil/zoneinfo/rebuild.py | 75 + crates/hm-dsl-engine/vendor/six.py | 1003 +++++++++ 26 files changed, 10268 insertions(+), 1 deletion(-) create mode 100644 crates/hm-dsl-engine/src/embedded_sources.rs create mode 100644 crates/hm-dsl-engine/vendor/croniter/__init__.py create mode 100644 crates/hm-dsl-engine/vendor/croniter/croniter.py create mode 100644 crates/hm-dsl-engine/vendor/dateutil/__init__.py create mode 100644 crates/hm-dsl-engine/vendor/dateutil/_common.py create mode 100644 crates/hm-dsl-engine/vendor/dateutil/_version.py create mode 100644 crates/hm-dsl-engine/vendor/dateutil/easter.py create mode 100644 crates/hm-dsl-engine/vendor/dateutil/parser/__init__.py create mode 100644 crates/hm-dsl-engine/vendor/dateutil/parser/_parser.py create mode 100644 crates/hm-dsl-engine/vendor/dateutil/parser/isoparser.py create mode 100644 crates/hm-dsl-engine/vendor/dateutil/relativedelta.py create mode 100644 crates/hm-dsl-engine/vendor/dateutil/rrule.py create mode 100644 crates/hm-dsl-engine/vendor/dateutil/tz/__init__.py create mode 100644 crates/hm-dsl-engine/vendor/dateutil/tz/_common.py create mode 100644 crates/hm-dsl-engine/vendor/dateutil/tz/_factories.py create mode 100644 crates/hm-dsl-engine/vendor/dateutil/tz/tz.py create mode 100644 crates/hm-dsl-engine/vendor/dateutil/tz/win.py create mode 100644 crates/hm-dsl-engine/vendor/dateutil/tzwin.py create mode 100644 crates/hm-dsl-engine/vendor/dateutil/utils.py create mode 100644 crates/hm-dsl-engine/vendor/dateutil/zoneinfo/__init__.py create mode 100644 crates/hm-dsl-engine/vendor/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz create mode 100644 crates/hm-dsl-engine/vendor/dateutil/zoneinfo/rebuild.py create mode 100644 crates/hm-dsl-engine/vendor/six.py diff --git a/Cargo.lock b/Cargo.lock index a83f76bc..4ad47c5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1592,6 +1592,7 @@ dependencies = [ "async-trait", "dirs", "hex", + "include_dir", "oxc_allocator", "oxc_parser", "oxc_span", @@ -1956,6 +1957,25 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "include_dir" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" +dependencies = [ + "include_dir_macros", +] + +[[package]] +name = "include_dir_macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "indexmap" version = "1.9.3" diff --git a/crates/hm-dsl-engine/Cargo.toml b/crates/hm-dsl-engine/Cargo.toml index 3d6c0135..fe198640 100644 --- a/crates/hm-dsl-engine/Cargo.toml +++ b/crates/hm-dsl-engine/Cargo.toml @@ -12,7 +12,7 @@ path = "src/lib.rs" [features] default = ["embedded-python", "embedded-typescript"] -embedded-python = ["dep:wasmtime", "dep:wasmtime-wasi"] +embedded-python = ["dep:wasmtime", "dep:wasmtime-wasi", "dep:include_dir"] embedded-typescript = ["dep:wasmtime", "dep:wasmtime-wasi", "dep:oxc_transformer", "dep:oxc_allocator", "dep:oxc_parser", "dep:oxc_span"] [dependencies] @@ -36,6 +36,7 @@ oxc_transformer = { version = "0.100", optional = true } oxc_allocator = { version = "0.100", optional = true } oxc_parser = { version = "0.100", optional = true } oxc_span = { version = "0.100", optional = true } +include_dir = { version = "0.7", optional = true } [dev-dependencies] tokio = { version = "1", features = ["macros", "rt-multi-thread"] } diff --git a/crates/hm-dsl-engine/src/embedded_sources.rs b/crates/hm-dsl-engine/src/embedded_sources.rs new file mode 100644 index 00000000..61ea71a9 --- /dev/null +++ b/crates/hm-dsl-engine/src/embedded_sources.rs @@ -0,0 +1,76 @@ +//! Compile-time embedded Python source trees. +//! +//! The [`include_dir`] proc-macro bakes directory contents into the binary so +//! the WASI Python engine can materialise them into a scratch directory without +//! requiring filesystem access to the original source tree. + +use std::path::Path; + +use include_dir::{include_dir, Dir}; + +/// The `harmont` Python package source tree +/// (`dsls/harmont-py/harmont/`). +pub static HARMONT_PY: Dir<'_> = + include_dir!("$CARGO_MANIFEST_DIR/../../dsls/harmont-py/harmont"); + +/// Vendored third-party Python packages required by `harmont`. +/// +/// Contains `croniter`, `dateutil` (python-dateutil), and `six` — everything +/// the DSL needs at runtime. +pub static VENDOR_PACKAGES: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/vendor"); + +/// Extract an embedded directory tree to disk. +/// +/// Creates `target` (and its parents) if it does not already exist, then +/// writes every file from `dir` under it. +/// +/// # Errors +/// +/// Returns an error if any file or directory cannot be created under `target`. +pub fn extract_to(dir: &Dir<'_>, target: &Path) -> anyhow::Result<()> { + std::fs::create_dir_all(target).map_err(|e| { + anyhow::anyhow!("creating target directory {}: {e}", target.display()) + })?; + dir.extract(target) + .map_err(|e| anyhow::anyhow!("extracting embedded sources to {}: {e}", target.display())) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn harmont_py_contains_init() { + assert!(HARMONT_PY.get_file("__init__.py").is_some()); + } + + #[test] + fn harmont_py_contains_pipeline() { + assert!(HARMONT_PY.get_file("pipeline.py").is_some()); + } + + #[test] + fn vendor_contains_croniter() { + assert!( + VENDOR_PACKAGES.get_file("croniter/__init__.py").is_some(), + "vendor must contain croniter package with __init__.py", + ); + } + + #[test] + fn extract_harmont_py_creates_files() { + let tmp = tempfile::tempdir().expect("tempdir"); + let target = tmp.path().join("harmont"); + extract_to(&HARMONT_PY, &target).expect("extract"); + assert!(target.join("__init__.py").exists()); + assert!(target.join("pipeline.py").exists()); + } + + #[test] + fn extract_vendor_creates_croniter() { + let tmp = tempfile::tempdir().expect("tempdir"); + let target = tmp.path().join("vendor"); + extract_to(&VENDOR_PACKAGES, &target).expect("extract"); + assert!(target.join("croniter").join("__init__.py").exists()); + } +} diff --git a/crates/hm-dsl-engine/src/lib.rs b/crates/hm-dsl-engine/src/lib.rs index 1c5df775..bb2fe56f 100644 --- a/crates/hm-dsl-engine/src/lib.rs +++ b/crates/hm-dsl-engine/src/lib.rs @@ -12,6 +12,9 @@ pub mod detect; // mod js_engine; (embedded-typescript) // mod ts_preprocess; (embedded-typescript) +#[cfg(feature = "embedded-python")] +pub mod embedded_sources; + #[cfg(any(feature = "embedded-python", feature = "embedded-typescript"))] pub mod wasm_runtime; diff --git a/crates/hm-dsl-engine/vendor/croniter/__init__.py b/crates/hm-dsl-engine/vendor/croniter/__init__.py new file mode 100644 index 00000000..52e41dcd --- /dev/null +++ b/crates/hm-dsl-engine/vendor/croniter/__init__.py @@ -0,0 +1,41 @@ +from . import croniter as cron_m +from .croniter import ( + DAY_FIELD, + HOUR_FIELD, + MINUTE_FIELD, + MONTH_FIELD, + OVERFLOW32B_MODE, + SECOND_FIELD, + UTC_DT, + YEAR_FIELD, + CroniterBadCronError, + CroniterBadDateError, + CroniterBadTypeRangeError, + CroniterError, + CroniterNotAlphaError, + CroniterUnsupportedSyntaxError, + croniter, + croniter_range, + datetime_to_timestamp, +) + +__all__ = [ + "DAY_FIELD", + "HOUR_FIELD", + "MINUTE_FIELD", + "MONTH_FIELD", + "OVERFLOW32B_MODE", + "SECOND_FIELD", + "UTC_DT", + "YEAR_FIELD", + "CroniterBadCronError", + "CroniterBadDateError", + "CroniterBadTypeRangeError", + "CroniterError", + "CroniterNotAlphaError", + "CroniterUnsupportedSyntaxError", + "cron_m", + "croniter", + "croniter_range", + "datetime_to_timestamp", +] diff --git a/crates/hm-dsl-engine/vendor/croniter/croniter.py b/crates/hm-dsl-engine/vendor/croniter/croniter.py new file mode 100644 index 00000000..4f0fa625 --- /dev/null +++ b/crates/hm-dsl-engine/vendor/croniter/croniter.py @@ -0,0 +1,1492 @@ +#!/usr/bin/env python +import binascii +import calendar +import copy +import datetime +import math +import platform +import random +import re +import struct +import sys +import traceback as _traceback +from time import time +from typing import Any, Literal, Optional, Union + +from dateutil.relativedelta import relativedelta +from dateutil.tz import datetime_exists, tzutc + +ExpandedExpression = list[Union[int, Literal["*", "l"]]] + + +def is_32bit() -> bool: + """ + Detect if Python is running in 32-bit mode. + Returns True if running on 32-bit Python, False for 64-bit. + """ + # Method 1: Check pointer size + bits = struct.calcsize("P") * 8 + + # Method 2: Check platform architecture string + try: + architecture = platform.architecture()[0] + except RuntimeError: + architecture = None + + # Method 3: Check maxsize + is_small_maxsize = sys.maxsize <= 2**32 + + # Evaluate all available methods + is_32 = False + + if bits == 32: + is_32 = True + elif architecture and "32" in architecture: + is_32 = True + elif is_small_maxsize: + is_32 = True + + return is_32 + + +try: + # https://github.com/python/cpython/issues/101069 detection + if is_32bit(): + datetime.datetime.fromtimestamp(3999999999) + OVERFLOW32B_MODE = False +except OverflowError: + OVERFLOW32B_MODE = True + + +UTC_DT = datetime.timezone.utc +EPOCH = datetime.datetime.fromtimestamp(0, UTC_DT) + +M_ALPHAS: dict[str, Union[int, str]] = { + "jan": 1, + "feb": 2, + "mar": 3, + "apr": 4, + "may": 5, + "jun": 6, + "jul": 7, + "aug": 8, + "sep": 9, + "oct": 10, + "nov": 11, + "dec": 12, +} +DOW_ALPHAS: dict[str, Union[int, str]] = { + "sun": 0, + "mon": 1, + "tue": 2, + "wed": 3, + "thu": 4, + "fri": 5, + "sat": 6, +} + +MINUTE_FIELD = 0 +HOUR_FIELD = 1 +DAY_FIELD = 2 +MONTH_FIELD = 3 +DOW_FIELD = 4 +SECOND_FIELD = 5 +YEAR_FIELD = 6 + +UNIX_FIELDS = (MINUTE_FIELD, HOUR_FIELD, DAY_FIELD, MONTH_FIELD, DOW_FIELD) +SECOND_FIELDS = (MINUTE_FIELD, HOUR_FIELD, DAY_FIELD, MONTH_FIELD, DOW_FIELD, SECOND_FIELD) +YEAR_FIELDS = ( + MINUTE_FIELD, + HOUR_FIELD, + DAY_FIELD, + MONTH_FIELD, + DOW_FIELD, + SECOND_FIELD, + YEAR_FIELD, +) + +step_search_re = re.compile(r"^([^-]+)-([^-/]+)(/(\d+))?$") +only_int_re = re.compile(r"^\d+$") + +DAYS = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) +WEEKDAYS = "|".join(DOW_ALPHAS.keys()) +MONTHS = "|".join(M_ALPHAS.keys()) +star_or_int_re = re.compile(r"^(\d+|\*)$") +special_dow_re = re.compile( + rf"^(?P
((?P(({WEEKDAYS})(-({WEEKDAYS}))?)"
+    rf"|(({MONTHS})(-({MONTHS}))?)|\w+)#)|l)(?P\d+)$"
+)
+nearest_weekday_re = re.compile(r"^(?:(\d+)w|w(\d+))$")
+re_star = re.compile("[*]")
+hash_expression_re = re.compile(
+    r"^(?Ph|r)(\((?P\d+)-(?P\d+)\))?(\/(?P\d+))?$"
+)
+
+CRON_FIELDS = {
+    "unix": UNIX_FIELDS,
+    "second": SECOND_FIELDS,
+    "year": YEAR_FIELDS,
+    len(UNIX_FIELDS): UNIX_FIELDS,
+    len(SECOND_FIELDS): SECOND_FIELDS,
+    len(YEAR_FIELDS): YEAR_FIELDS,
+}
+UNIX_CRON_LEN = len(UNIX_FIELDS)
+SECOND_CRON_LEN = len(SECOND_FIELDS)
+YEAR_CRON_LEN = len(YEAR_FIELDS)
+# retrocompat
+VALID_LEN_EXPRESSION = {a for a in CRON_FIELDS if isinstance(a, int)}
+
+MARKER = object()
+
+
+def datetime_to_timestamp(d):
+    if d.tzinfo is not None:
+        d = d.replace(tzinfo=None) - d.utcoffset()
+
+    return (d - datetime.datetime(1970, 1, 1)).total_seconds()
+
+
+def _is_leap(year: int) -> bool:
+    return year % 400 == 0 or (year % 4 == 0 and year % 100 != 0)
+
+
+def _last_day_of_month(year: int, month: int) -> int:
+    """Calculate the last day of the given month (honor leap years)."""
+    last_day = DAYS[month - 1]
+    if month == 2 and _is_leap(year):
+        last_day += 1
+    return last_day
+
+
+def _is_successor(
+    date: datetime.datetime, previous_date: datetime.datetime, is_prev: bool
+) -> bool:
+    """Check if the given date is a successor (after/before) of the previous date."""
+    if is_prev:
+        return date.astimezone(UTC_DT) < previous_date.astimezone(UTC_DT)
+    return date.astimezone(UTC_DT) > previous_date.astimezone(UTC_DT)
+
+
+def _timezone_delta(date1: datetime.datetime, date2: datetime.datetime) -> datetime.timedelta:
+    """Calculate the timezone difference of the given dates."""
+    offset1 = date1.utcoffset()
+    offset2 = date2.utcoffset()
+    assert offset1 is not None
+    assert offset2 is not None
+    return offset2 - offset1
+
+
+def _add_tzinfo(
+    date: datetime.datetime, previous_date: datetime.datetime, is_prev: bool
+) -> tuple[datetime.datetime, bool]:
+    """Add the tzinfo from the previous date to the given date.
+
+    In case the new date is ambiguous, determine the correct date
+    based on it being closer to the previous date but still a successor
+    (after/before based on `is_prev`).
+
+    In case the date does not exist, jump forward to the next existing date.
+    """
+    localize = getattr(previous_date.tzinfo, "localize", None)
+    if localize is not None:
+        # pylint: disable-next=import-outside-toplevel
+        import pytz
+
+        try:
+            result = localize(date, is_dst=None)
+        except pytz.NonExistentTimeError:
+            while True:
+                date += datetime.timedelta(minutes=1)
+                try:
+                    result = localize(date, is_dst=None)
+                except pytz.NonExistentTimeError:
+                    continue
+                break
+            return result, False
+        except pytz.AmbiguousTimeError:
+            closer = localize(date, is_dst=not is_prev)
+            farther = localize(date, is_dst=is_prev)
+            # TODO: Check negative DST
+            assert (closer.astimezone(UTC_DT) > farther.astimezone(UTC_DT)) == is_prev
+            if _is_successor(closer, previous_date, is_prev):
+                result = closer
+            else:
+                assert _is_successor(farther, previous_date, is_prev)
+                result = farther
+        return result, True
+
+    result = date.replace(fold=1 if is_prev else 0, tzinfo=previous_date.tzinfo)
+    if not datetime_exists(result):
+        while not datetime_exists(result):
+            result += datetime.timedelta(minutes=1)
+        return result, False
+
+    # result is closer to the previous date
+    farther = date.replace(fold=0 if is_prev else 1, tzinfo=previous_date.tzinfo)
+    # Comparing the UTC offsets in the check for the date being ambiguous.
+    if result.utcoffset() != farther.utcoffset():
+        # TODO: Check negative DST
+        assert (result.astimezone(UTC_DT) > farther.astimezone(UTC_DT)) == is_prev
+        if not _is_successor(result, previous_date, is_prev):
+            assert _is_successor(farther, previous_date, is_prev)
+            result = farther
+    return result, True
+
+
+class CroniterError(ValueError):
+    """General top-level Croniter base exception"""
+
+
+class CroniterBadTypeRangeError(TypeError):
+    """."""
+
+
+class CroniterBadCronError(CroniterError):
+    """Syntax, unknown value, or range error within a cron expression"""
+
+
+class CroniterUnsupportedSyntaxError(CroniterBadCronError):
+    """Valid cron syntax, but likely to produce inaccurate results"""
+
+    # Extending CroniterBadCronError, which may be contridatory, but this allows
+    # catching both errors with a single exception.  From a user perspective
+    # these will likely be handled the same way.
+
+
+class CroniterBadDateError(CroniterError):
+    """Unable to find next/prev timestamp match"""
+
+
+class CroniterNotAlphaError(CroniterBadCronError):
+    """Cron syntax contains an invalid day or month abbreviation"""
+
+
+class croniter:
+    MONTHS_IN_YEAR = 12
+
+    # This helps with expanding `*` fields into `lower-upper` ranges. Each item
+    # in this tuple maps to the corresponding field index
+    RANGES = ((0, 59), (0, 23), (1, 31), (1, 12), (0, 6), (0, 59), (1970, 2099))
+
+    ALPHACONV: tuple[dict[str, Union[int, str]], ...] = (
+        {},  # 0: min
+        {},  # 1: hour
+        {"l": "l"},  # 2: dom
+        # 3: mon
+        copy.deepcopy(M_ALPHAS),
+        # 4: dow
+        copy.deepcopy(DOW_ALPHAS),
+        # 5: second
+        {},
+        # 6: year
+        {},
+    )
+
+    LOWMAP: tuple[dict[int, int], ...] = ({}, {}, {0: 1}, {0: 1}, {7: 0}, {}, {})
+
+    LEN_MEANS_ALL = (60, 24, 31, 12, 7, 60, 130)
+
+    def __init__(
+        self,
+        expr_format: str,
+        start_time: Optional[Union[datetime.datetime, float]] = None,
+        ret_type: type = float,
+        day_or: bool = True,
+        max_years_between_matches: Optional[int] = None,
+        is_prev: bool = False,
+        hash_id: Optional[Union[bytes, str]] = None,
+        implement_cron_bug: bool = False,
+        second_at_beginning: bool = False,
+        expand_from_start_time: bool = False,
+    ) -> None:
+        self._ret_type = ret_type
+        self._day_or = day_or
+        self._implement_cron_bug = implement_cron_bug
+        self.second_at_beginning = bool(second_at_beginning)
+        self._expand_from_start_time = expand_from_start_time
+
+        if hash_id is not None:
+            if not isinstance(hash_id, (bytes, str)):
+                raise TypeError("hash_id must be bytes or UTF-8 string")
+            if not isinstance(hash_id, bytes):
+                hash_id = hash_id.encode("UTF-8")
+
+        self._max_years_btw_matches_explicitly_set = max_years_between_matches is not None
+        if max_years_between_matches is None:
+            max_years_between_matches = 50
+        self._max_years_between_matches = max(int(max_years_between_matches), 1)
+
+        if start_time is None:
+            start_time = time()
+
+        self.tzinfo: Optional[datetime.tzinfo] = None
+
+        self.start_time = 0.0
+        self.dst_start_time = 0.0
+        self.cur = 0.0
+        self.set_current(start_time, force=True)
+
+        self.expanded, self.nth_weekday_of_month, self.expressions, self.nearest_weekday = self._expand(
+            expr_format,
+            hash_id=hash_id,
+            from_timestamp=self.dst_start_time if self._expand_from_start_time else None,
+            second_at_beginning=second_at_beginning,
+        )
+        self.fields = CRON_FIELDS[len(self.expanded)]
+        self._is_prev = is_prev
+
+    @classmethod
+    def _alphaconv(cls, index, key, expressions):
+        try:
+            return cls.ALPHACONV[index][key]
+        except KeyError:
+            raise CroniterNotAlphaError(f"[{' '.join(expressions)}] is not acceptable")
+
+    def get_next(self, ret_type=None, start_time=None, update_current=True):
+        if start_time and self._expand_from_start_time:
+            raise ValueError(
+                "start_time is not supported when using expand_from_start_time = True."
+            )
+        return self._get_next(
+            ret_type=ret_type, start_time=start_time, is_prev=False, update_current=update_current
+        )
+
+    def get_prev(self, ret_type=None, start_time=None, update_current=True):
+        return self._get_next(
+            ret_type=ret_type, start_time=start_time, is_prev=True, update_current=update_current
+        )
+
+    def get_current(self, ret_type=None):
+        ret_type = ret_type or self._ret_type
+        if issubclass(ret_type, datetime.datetime):
+            return self.timestamp_to_datetime(self.cur)
+        return self.cur
+
+    def set_current(
+        self, start_time: Optional[Union[datetime.datetime, float]], force: bool = True
+    ) -> float:
+        if (force or (self.cur is None)) and start_time is not None:
+            if isinstance(start_time, datetime.datetime):
+                self.tzinfo = start_time.tzinfo
+                start_time = self.datetime_to_timestamp(start_time)
+
+            self.start_time = start_time
+            self.dst_start_time = start_time
+            self.cur = start_time
+        return self.cur
+
+    @staticmethod
+    def datetime_to_timestamp(d: datetime.datetime) -> float:
+        """
+        Converts a `datetime` object `d` into a UNIX timestamp.
+        """
+        return datetime_to_timestamp(d)
+
+    _datetime_to_timestamp = datetime_to_timestamp  # retrocompat
+
+    def timestamp_to_datetime(self, timestamp: float, tzinfo: Any = MARKER) -> datetime.datetime:
+        """
+        Converts a UNIX `timestamp` into a `datetime` object.
+        """
+        if tzinfo is MARKER:  # allow to give tzinfo=None even if self.tzinfo is set
+            tzinfo = self.tzinfo
+        if OVERFLOW32B_MODE:
+            # degraded mode to workaround Y2038
+            # see https://github.com/python/cpython/issues/101069
+            result = EPOCH.replace(tzinfo=None) + datetime.timedelta(seconds=timestamp)
+        else:
+            result = datetime.datetime.fromtimestamp(timestamp, tz=tzutc()).replace(tzinfo=None)
+        if tzinfo:
+            result = result.replace(tzinfo=UTC_DT).astimezone(tzinfo)
+        return result
+
+    _timestamp_to_datetime = timestamp_to_datetime  # retrocompat
+
+    def _get_next(self, ret_type=None, start_time=None, is_prev=None, update_current=None):
+        if update_current is None:
+            update_current = True
+        self.set_current(start_time, force=True)
+        if is_prev is None:
+            is_prev = self._is_prev
+        self._is_prev = is_prev
+
+        ret_type = ret_type or self._ret_type
+
+        if not issubclass(ret_type, (float, datetime.datetime)):
+            raise TypeError("Invalid ret_type, only 'float' or 'datetime' is acceptable.")
+
+        result = self._calc_next(is_prev)
+        timestamp = self.datetime_to_timestamp(result)
+        if update_current:
+            self.cur = timestamp
+        if issubclass(ret_type, datetime.datetime):
+            return result
+        return timestamp
+
+    # iterator protocol, to enable direct use of croniter
+    # objects in a loop, like "for dt in croniter("5 0 * * *'): ..."
+    # or for combining multiple croniters into single
+    # dates feed using 'itertools' module
+    def all_next(self, ret_type=None, start_time=None, update_current=None):
+        """
+        Returns a generator yielding consecutive dates.
+
+        May be used instead of an implicit call to __iter__ whenever a
+        non-default `ret_type` needs to be specified.
+        """
+        # In a Python 3.7+ world:  contextlib.suppress and contextlib.nullcontext could
+        # be used instead
+        try:
+            while True:
+                self._is_prev = False
+                yield self._get_next(
+                    ret_type=ret_type, start_time=start_time, update_current=update_current
+                )
+                start_time = None
+        except CroniterBadDateError:
+            if self._max_years_btw_matches_explicitly_set:
+                return
+            raise
+
+    def all_prev(self, ret_type=None, start_time=None, update_current=None):
+        """
+        Returns a generator yielding previous dates.
+        """
+        try:
+            while True:
+                self._is_prev = True
+                yield self._get_next(
+                    ret_type=ret_type, start_time=start_time, update_current=update_current
+                )
+                start_time = None
+        except CroniterBadDateError:
+            if self._max_years_btw_matches_explicitly_set:
+                return
+            raise
+
+    def iter(self, *args, **kwargs):
+        return self.all_prev if self._is_prev else self.all_next
+
+    def __iter__(self):
+        return self
+
+    __next__ = next = _get_next
+
+    def _calc_next(self, is_prev: bool) -> datetime.datetime:
+        current = self.timestamp_to_datetime(self.cur)
+        expanded = self.expanded[:]
+        nth_weekday_of_month = self.nth_weekday_of_month.copy()
+
+        # exception to support day of month and day of week as defined in cron
+        if (expanded[DAY_FIELD][0] != "*" and expanded[DOW_FIELD][0] != "*") and self._day_or:
+            # If requested, handle a bug in vixie cron/ISC cron where day_of_month and
+            # day_of_week form an intersection (AND) instead of a union (OR) if either
+            # field is an asterisk or starts with an asterisk (https://crontab.guru/cron-bug.html)
+            if self._implement_cron_bug and (
+                re_star.match(self.expressions[DAY_FIELD])
+                or re_star.match(self.expressions[DOW_FIELD])
+            ):
+                # To produce a schedule identical to the cron bug, we'll bypass the code
+                # that makes a union of DOM and DOW, and instead skip to the code that
+                # does an intersect instead
+                pass
+            else:
+                bak = expanded[DOW_FIELD]
+                expanded[DOW_FIELD] = ["*"]
+                t1 = self._calc(current, expanded, nth_weekday_of_month, is_prev)
+                expanded[DOW_FIELD] = bak
+                expanded[DAY_FIELD] = ["*"]
+
+                t2 = self._calc(current, expanded, nth_weekday_of_month, is_prev)
+                if is_prev:
+                    return t1 if t1 > t2 else t2
+                return t1 if t1 < t2 else t2
+
+        return self._calc(current, expanded, nth_weekday_of_month, is_prev)
+
+    def _calc(
+        self,
+        now: datetime.datetime,
+        expanded: list[ExpandedExpression],
+        nth_weekday_of_month: dict[int, set[int]],
+        is_prev: bool,
+    ) -> datetime.datetime:
+        if is_prev:
+            nearest_diff_method = self._get_prev_nearest_diff
+            offset = relativedelta(microseconds=-1)
+        else:
+            nearest_diff_method = self._get_next_nearest_diff
+            if len(expanded) > UNIX_CRON_LEN:
+                offset = relativedelta(seconds=1)
+            else:
+                offset = relativedelta(minutes=1)
+        # Calculate the next cron time in local time a.k.a. timezone unaware time.
+        unaware_time = now.replace(tzinfo=None) + offset
+        if len(expanded) > UNIX_CRON_LEN:
+            unaware_time = unaware_time.replace(microsecond=0)
+        else:
+            unaware_time = unaware_time.replace(second=0, microsecond=0)
+
+        month = unaware_time.month
+        year = current_year = unaware_time.year
+
+        def proc_year(d):
+            if len(expanded) == YEAR_CRON_LEN:
+                try:
+                    expanded[YEAR_FIELD].index("*")
+                except ValueError:
+                    # use None as range_val to indicate no loop
+                    diff_year = nearest_diff_method(d.year, expanded[YEAR_FIELD], None)
+                    if diff_year is None:
+                        return None, d
+                    if diff_year != 0:
+                        if is_prev:
+                            d += relativedelta(
+                                years=diff_year, month=12, day=31, hour=23, minute=59, second=59
+                            )
+                        else:
+                            d += relativedelta(
+                                years=diff_year, month=1, day=1, hour=0, minute=0, second=0
+                            )
+                        return True, d
+            return False, d
+
+        def proc_month(d):
+            try:
+                expanded[MONTH_FIELD].index("*")
+            except ValueError:
+                diff_month = nearest_diff_method(
+                    d.month, expanded[MONTH_FIELD], self.MONTHS_IN_YEAR
+                )
+                reset_day = 1
+
+                if diff_month is not None and diff_month != 0:
+                    if is_prev:
+                        d += relativedelta(months=diff_month)
+                        reset_day = _last_day_of_month(d.year, d.month)
+                        d += relativedelta(day=reset_day, hour=23, minute=59, second=59)
+                    else:
+                        d += relativedelta(
+                            months=diff_month, day=reset_day, hour=0, minute=0, second=0
+                        )
+                    return True, d
+            return False, d
+
+        def proc_day_of_month(d):
+            try:
+                expanded[DAY_FIELD].index("*")
+            except ValueError:
+                days = _last_day_of_month(year, month)
+                if "l" in expanded[DAY_FIELD] and days == d.day:
+                    return False, d
+
+                if is_prev:
+                    prev_month = (month - 2) % self.MONTHS_IN_YEAR + 1
+                    prev_year = year - 1 if month == 1 else year
+                    days_in_prev_month = _last_day_of_month(prev_year, prev_month)
+                    diff_day = nearest_diff_method(d.day, expanded[DAY_FIELD], days_in_prev_month)
+                else:
+                    diff_day = nearest_diff_method(d.day, expanded[DAY_FIELD], days)
+
+                if diff_day is not None and diff_day != 0:
+                    if is_prev:
+                        d += relativedelta(days=diff_day, hour=23, minute=59, second=59)
+                    else:
+                        d += relativedelta(days=diff_day, hour=0, minute=0, second=0)
+                    return True, d
+            return False, d
+
+        def proc_day_of_week(d):
+            try:
+                expanded[DOW_FIELD].index("*")
+            except ValueError:
+                diff_day_of_week = nearest_diff_method(d.isoweekday() % 7, expanded[DOW_FIELD], 7)
+                if diff_day_of_week is not None and diff_day_of_week != 0:
+                    if is_prev:
+                        d += relativedelta(days=diff_day_of_week, hour=23, minute=59, second=59)
+                    else:
+                        d += relativedelta(days=diff_day_of_week, hour=0, minute=0, second=0)
+                    return True, d
+            return False, d
+
+        def proc_day_of_week_nth(d):
+            if "*" in nth_weekday_of_month:
+                s = nth_weekday_of_month["*"]
+                for i in range(0, 7):
+                    if i in nth_weekday_of_month:
+                        nth_weekday_of_month[i].update(s)
+                    else:
+                        nth_weekday_of_month[i] = s
+                del nth_weekday_of_month["*"]
+
+            candidates = []
+            for wday, nth in nth_weekday_of_month.items():
+                c = self._get_nth_weekday_of_month(d.year, d.month, wday)
+                for n in nth:
+                    if n == "l":
+                        candidate = c[-1]
+                    elif len(c) < n:
+                        continue
+                    else:
+                        candidate = c[n - 1]
+                    if (is_prev and candidate <= d.day) or (not is_prev and d.day <= candidate):
+                        candidates.append(candidate)
+
+            if not candidates:
+                if is_prev:
+                    d += relativedelta(days=-d.day, hour=23, minute=59, second=59)
+                else:
+                    days = _last_day_of_month(year, month)
+                    d += relativedelta(days=(days - d.day + 1), hour=0, minute=0, second=0)
+                return True, d
+
+            candidates.sort()
+            diff_day = (candidates[-1] if is_prev else candidates[0]) - d.day
+            if diff_day != 0:
+                if is_prev:
+                    d += relativedelta(days=diff_day, hour=23, minute=59, second=59)
+                else:
+                    d += relativedelta(days=diff_day, hour=0, minute=0, second=0)
+                return True, d
+            return False, d
+
+        def proc_nearest_weekday(d):
+            """Process W (nearest weekday) day-of-month entries."""
+            candidates = []
+            for w_day in self.nearest_weekday:
+                candidate = self._get_nearest_weekday(d.year, d.month, w_day)
+                if (is_prev and candidate <= d.day) or (not is_prev and d.day <= candidate):
+                    candidates.append(candidate)
+
+            if not candidates:
+                if is_prev:
+                    d += relativedelta(days=-d.day, hour=23, minute=59, second=59)
+                else:
+                    days = _last_day_of_month(year, month)
+                    d += relativedelta(days=(days - d.day + 1), hour=0, minute=0, second=0)
+                return True, d
+
+            candidates.sort()
+            diff_day = (candidates[-1] if is_prev else candidates[0]) - d.day
+            if diff_day != 0:
+                if is_prev:
+                    d += relativedelta(days=diff_day, hour=23, minute=59, second=59)
+                else:
+                    d += relativedelta(days=diff_day, hour=0, minute=0, second=0)
+                return True, d
+            return False, d
+
+        def proc_hour(d):
+            try:
+                expanded[HOUR_FIELD].index("*")
+            except ValueError:
+                diff_hour = nearest_diff_method(d.hour, expanded[HOUR_FIELD], 24)
+                if diff_hour is not None and diff_hour != 0:
+                    if is_prev:
+                        d += relativedelta(hours=diff_hour, minute=59, second=59)
+                    else:
+                        d += relativedelta(hours=diff_hour, minute=0, second=0)
+                    return True, d
+            return False, d
+
+        def proc_minute(d):
+            try:
+                expanded[MINUTE_FIELD].index("*")
+            except ValueError:
+                diff_min = nearest_diff_method(d.minute, expanded[MINUTE_FIELD], 60)
+                if diff_min is not None and diff_min != 0:
+                    if is_prev:
+                        d += relativedelta(minutes=diff_min, second=59)
+                    else:
+                        d += relativedelta(minutes=diff_min, second=0)
+                    return True, d
+            return False, d
+
+        def proc_second(d):
+            if len(expanded) > UNIX_CRON_LEN:
+                try:
+                    expanded[SECOND_FIELD].index("*")
+                except ValueError:
+                    diff_sec = nearest_diff_method(d.second, expanded[SECOND_FIELD], 60)
+                    if diff_sec is not None and diff_sec != 0:
+                        d += relativedelta(seconds=diff_sec)
+                        return True, d
+            else:
+                d += relativedelta(second=0)
+            return False, d
+
+        procs = [
+            proc_year,
+            proc_month,
+            (proc_nearest_weekday if self.nearest_weekday else proc_day_of_month),
+            (proc_day_of_week_nth if nth_weekday_of_month else proc_day_of_week),
+            proc_hour,
+            proc_minute,
+            proc_second,
+        ]
+
+        while abs(year - current_year) <= self._max_years_between_matches:
+            next = False
+            stop = False
+            for proc in procs:
+                (changed, unaware_time) = proc(unaware_time)
+                # `None` can be set mostly for year processing
+                # so please see proc_year / _get_prev_nearest_diff / _get_next_nearest_diff
+                if changed is None:
+                    stop = True
+                    break
+                if changed:
+                    month, year = unaware_time.month, unaware_time.year
+                    next = True
+                    break
+            if stop:
+                break
+            if next:
+                continue
+
+            unaware_time = unaware_time.replace(microsecond=0)
+            if now.tzinfo is None:
+                return unaware_time
+
+            # Add timezone information back and handle DST changes
+            aware_time, exists = _add_tzinfo(unaware_time, now, is_prev)
+
+            if not exists and (
+                not _is_successor(aware_time, now, is_prev) or "*" in expanded[HOUR_FIELD]
+            ):
+                # The calculated local date does not exist and moving the time forward
+                # to the next valid time isn't the correct solution. Search for the
+                # next matching cron time that exists.
+                while not exists:
+                    unaware_time = self._calc(
+                        unaware_time, expanded, nth_weekday_of_month, is_prev
+                    )
+                    aware_time, exists = _add_tzinfo(unaware_time, now, is_prev)
+
+            offset_delta = _timezone_delta(now, aware_time)
+            if not offset_delta:
+                # There was no DST change.
+                return aware_time
+
+            # There was a DST change. So check if there is a alternative cron time
+            # for the other UTC offset.
+            alternative_unaware_time = now.replace(tzinfo=None) + offset_delta
+            alternative_unaware_time = self._calc(
+                alternative_unaware_time, expanded, nth_weekday_of_month, is_prev
+            )
+            alternative_aware_time, exists = _add_tzinfo(alternative_unaware_time, now, is_prev)
+
+            if not _is_successor(alternative_aware_time, now, is_prev):
+                # The alternative time is an ancestor of now. Thus it is not an alternative.
+                return aware_time
+
+            if _is_successor(aware_time, alternative_aware_time, is_prev):
+                return alternative_aware_time
+
+            return aware_time
+
+        if is_prev:
+            raise CroniterBadDateError("failed to find prev date")
+        raise CroniterBadDateError("failed to find next date")
+
+    @staticmethod
+    def _get_next_nearest_diff(x, to_check, range_val):
+        """
+        `range_val` is the range of a field.
+        If no available time, we can move to next loop(like next month).
+        `range_val` can also be set to `None` to indicate that there is no loop.
+        ( Currently, should only used for `year` field )
+        """
+        for i, d in enumerate(to_check):
+            if range_val is not None:
+                if d == "l":
+                    # if 'l' then it is the last day of month
+                    # => its value of range_val
+                    d = range_val
+                elif d > range_val:
+                    continue
+            if d >= x:
+                return d - x
+        # When range_val is None and x not exists in to_check,
+        # `None` will be returned to suggest no more available time
+        if range_val is None:
+            return None
+        return to_check[0] - x + range_val
+
+    @staticmethod
+    def _get_prev_nearest_diff(x, to_check, range_val):
+        """
+        `range_val` is the range of a field.
+        If no available time, we can move to previous loop(like previous month).
+        Range_val can also be set to `None` to indicate that there is no loop.
+        ( Currently should only used for `year` field )
+        """
+        candidates = to_check[:]
+        candidates.reverse()
+        for d in candidates:
+            if d != "l" and d <= x:
+                return d - x
+        if "l" in candidates:
+            return -x
+        # When range_val is None and x not exists in to_check,
+        # `None` will be returned to suggest no more available time
+        if range_val is None:
+            return None
+        candidate = candidates[0]
+        for c in candidates:
+            # fixed: c < range_val
+            # this code will reject all 31 day of month, 12 month, 59 second,
+            # 23 hour and so on.
+            # if candidates has just a element, this will not harmful.
+            # but candidates have multiple elements, then values equal to
+            # range_val will rejected.
+            if c <= range_val:
+                candidate = c
+                break
+        # fix crontab "0 6 30 3 *" condidates only a element, then get_prev error
+        # return 2021-03-02 06:00:00
+        if candidate > range_val:
+            return -range_val
+        return candidate - x - range_val
+
+    @staticmethod
+    def _get_nth_weekday_of_month(year: int, month: int, day_of_week: int) -> tuple[int, ...]:
+        """For a given year/month return a list of days in nth-day-of-month order.
+        The last weekday of the month is always [-1].
+        """
+        w = (day_of_week + 6) % 7
+        c = calendar.Calendar(w).monthdayscalendar(year, month)
+        if c[0][0] == 0:
+            c.pop(0)
+        return tuple(i[0] for i in c)
+
+    @staticmethod
+    def _get_nearest_weekday(year, month, day):
+        """Get the nearest weekday (Mon-Fri) to the given day in the given month.
+
+        Rules:
+        - If the day is a weekday, return it.
+        - If Saturday, return Friday (day-1), unless that crosses into previous month,
+          then return Monday (day+2).
+        - If Sunday, return Monday (day+1), unless that crosses into next month,
+          then return Friday (day-2).
+        """
+        last_day = _last_day_of_month(year, month)
+        day = min(day, last_day)
+        weekday = calendar.weekday(year, month, day)  # 0=Mon, 6=Sun
+        if weekday < 5:  # Mon-Fri
+            return day
+        if weekday == 5:  # Saturday
+            if day > 1:
+                return day - 1  # Friday
+            else:
+                return day + 2  # Monday (1st is Sat, so 3rd is Mon)
+        # Sunday
+        if day < last_day:
+            return day + 1  # Monday
+        else:
+            return day - 2  # Friday (last day is Sun, go back to Fri)
+
+    @classmethod
+    def value_alias(cls, val, field_index, len_expressions=UNIX_CRON_LEN):
+        if isinstance(len_expressions, (list, dict, tuple, set)):
+            len_expressions = len(len_expressions)
+        if val in cls.LOWMAP[field_index] and not (
+            # do not support 0 as a month either for classical 5 fields cron,
+            # 6fields second repeat form or 7 fields year form
+            # but still let conversion happen if day field is shifted
+            (field_index in [DAY_FIELD, MONTH_FIELD] and len_expressions == UNIX_CRON_LEN)
+            or (field_index in [MONTH_FIELD, DOW_FIELD] and len_expressions == SECOND_CRON_LEN)
+            or (
+                field_index in [DAY_FIELD, MONTH_FIELD, DOW_FIELD]
+                and len_expressions == YEAR_CRON_LEN
+            )
+        ):
+            val = cls.LOWMAP[field_index][val]
+        return val
+
+    # Maximum days in each month (non-leap year for Feb)
+    DAYS_IN_MONTH = {1: 31, 2: 28, 3: 31, 4: 30, 5: 31, 6: 30, 7: 31, 8: 31, 9: 30, 10: 31, 11: 30, 12: 31}
+
+    @classmethod
+    def _expand(cls, expr_format, hash_id=None, second_at_beginning=False, from_timestamp=None, strict=False, strict_year=None):
+        # Split the expression in components, and normalize L -> l, MON -> mon,
+        # etc. Keep expr_format untouched so we can use it in the exception
+        # messages.
+        expr_aliases = {
+            "@midnight": ("0 0 * * *", "h h(0-2) * * * h"),
+            "@hourly": ("0 * * * *", "h * * * * h"),
+            "@daily": ("0 0 * * *", "h h * * * h"),
+            "@weekly": ("0 0 * * 0", "h h * * h h"),
+            "@monthly": ("0 0 1 * *", "h h h * * h"),
+            "@yearly": ("0 0 1 1 *", "h h h h * h"),
+            "@annually": ("0 0 1 1 *", "h h h h * h"),
+        }
+
+        efl = expr_format.lower()
+        hash_id_expr = 1 if hash_id is not None else 0
+        try:
+            efl = expr_aliases[efl][hash_id_expr]
+        except KeyError:
+            pass
+
+        expressions = efl.split()
+
+        if len(expressions) not in VALID_LEN_EXPRESSION:
+            raise CroniterBadCronError(
+                "Exactly 5, 6 or 7 columns has to be specified for iterator expression."
+            )
+
+        if len(expressions) > UNIX_CRON_LEN and second_at_beginning:
+            # move second to it's own(6th) field to process by same logical
+            expressions.insert(SECOND_FIELD, expressions.pop(0))
+
+        expanded = []
+        nth_weekday_of_month = {}
+        nearest_weekday = set()
+
+        for field_index, expr in enumerate(expressions):
+            for expanderid, expander in EXPANDERS.items():
+                expr = expander(cls).expand(
+                    efl, field_index, expr, hash_id=hash_id, from_timestamp=from_timestamp
+                )
+
+            if "?" in expr:
+                if expr != "?":
+                    raise CroniterBadCronError(
+                        f"[{expr_format}] is not acceptable."
+                        f" Question mark can not used with other characters"
+                    )
+                if field_index not in [DAY_FIELD, DOW_FIELD]:
+                    raise CroniterBadCronError(
+                        f"[{expr_format}] is not acceptable. "
+                        f"Question mark can only used in day_of_month or day_of_week"
+                    )
+                # currently just trade `?` as `*`
+                expr = "*"
+
+            e_list = expr.split(",")
+            res = []
+
+            while len(e_list) > 0:
+                e = e_list.pop()
+                nth = None
+
+                if field_index == DOW_FIELD:
+                    # Handle special case in the dow expression: 2#3, l3
+                    special_dow_rem = special_dow_re.match(str(e))
+                    if special_dow_rem:
+                        g = special_dow_rem.groupdict()
+                        he, last = g.get("he", ""), g.get("last", "")
+                        if he:
+                            e = he
+                            try:
+                                nth = int(last)
+                                assert 5 >= nth >= 1
+                            except (KeyError, ValueError, AssertionError):
+                                raise CroniterBadCronError(
+                                    f"[{expr_format}] is not acceptable."
+                                    f" Invalid day_of_week value: '{nth}'"
+                                )
+                        elif last:
+                            e = last
+                            nth = g["pre"]  # 'l'
+
+                if field_index == DAY_FIELD:
+                    # Handle W (nearest weekday) in day-of-month: 15w, w15
+                    w_match = nearest_weekday_re.match(str(e))
+                    if w_match:
+                        w_day = int(w_match.group(1) or w_match.group(2))
+                        if w_day < 1 or w_day > 31:
+                            raise CroniterBadCronError(
+                                f"[{expr_format}] is not acceptable,"
+                                f" nearest weekday day value '{w_day}' out of range"
+                            )
+                        if len(e_list) > 0 or len(res) > 0:
+                            raise CroniterBadCronError(
+                                f"[{expr_format}] is not acceptable."
+                                f" 'W' can only be used with a single day value,"
+                                f" not in a list or range"
+                            )
+                        nearest_weekday.add(w_day)
+                        res.append(w_day)
+                        continue
+
+                # Before matching step_search_re, normalize "*" to "{min}-{max}".
+                # Example: in the minute field, "*/5" normalizes to "0-59/5"
+                t = re.sub(
+                    r"^\*(\/.+)$",
+                    r"%d-%d\1" % (cls.RANGES[field_index][0], cls.RANGES[field_index][1]),
+                    str(e),
+                )
+                m = step_search_re.search(t)
+
+                if not m:
+                    # Before matching step_search_re,
+                    # normalize "{start}/{step}" to "{start}-{max}/{step}".
+                    # Example: in the minute field, "10/5" normalizes to "10-59/5"
+                    t = re.sub(r"^(.+)\/(.+)$", r"\1-%d/\2" % (cls.RANGES[field_index][1]), str(e))
+                    m = step_search_re.search(t)
+
+                if m:
+                    # early abort if low/high are out of bounds
+                    (low, high, step) = m.group(1), m.group(2), m.group(4) or 1
+                    if field_index == DAY_FIELD and high == "l":
+                        high = "31"
+
+                    if not only_int_re.search(low):
+                        low = str(cls._alphaconv(field_index, low, expressions))
+
+                    if not only_int_re.search(high):
+                        high = str(cls._alphaconv(field_index, high, expressions))
+
+                    # normally, it's already guarded by the RE that should not accept
+                    # not-int values.
+                    if not only_int_re.search(str(step)):
+                        raise CroniterBadCronError(
+                            f"[{expr_format}] step '{step}'"
+                            f" in field {field_index} is not acceptable"
+                        )
+                    step = int(step)
+
+                    for band in low, high:
+                        if not only_int_re.search(str(band)):
+                            raise CroniterBadCronError(
+                                f"[{expr_format}] bands '{low}-{high}'"
+                                f" in field {field_index} are not acceptable"
+                            )
+
+                    low, high = (
+                        cls.value_alias(int(_val), field_index, expressions)
+                        for _val in (low, high)
+                    )
+
+                    if max(low, high) > max(
+                        cls.RANGES[field_index][0], cls.RANGES[field_index][1]
+                    ):
+                        raise CroniterBadCronError(f"{expr_format} is out of bands")
+
+                    if from_timestamp:
+                        low = cls._get_low_from_current_date_number(
+                            field_index, int(step), int(from_timestamp)
+                        )
+
+                    # Handle when the second bound of the range is in backtracking order:
+                    # eg: X-Sun or X-7 (Sat-Sun) in DOW, or X-Jan (Apr-Jan) in MONTH
+                    if low > high:
+                        whole_field_range = list(
+                            range(cls.RANGES[field_index][0], cls.RANGES[field_index][1] + 1, 1)
+                        )
+                        # Add FirstBound -> ENDRANGE, respecting step
+                        rng = list(range(low, cls.RANGES[field_index][1] + 1, step))
+                        # Then 0 -> SecondBound, but skipping n first occurences according to step
+                        # EG to respect such expressions : Apr-Jan/3
+                        to_skip = 0
+                        if rng:
+                            already_skipped = list(reversed(whole_field_range)).index(rng[-1])
+                            curpos = whole_field_range.index(rng[-1])
+                            if ((curpos + step) > len(whole_field_range)) and (
+                                already_skipped < step
+                            ):
+                                to_skip = step - already_skipped
+                        rng += list(range(cls.RANGES[field_index][0] + to_skip, high + 1, step))
+                    # if we include a range type: Jan-Jan, or Sun-Sun,
+                    #  it means the whole cycle (all days of week, # all monthes of year, etc)
+                    elif low == high:
+                        rng = list(
+                            range(cls.RANGES[field_index][0], cls.RANGES[field_index][1] + 1, step)
+                        )
+                    else:
+                        try:
+                            rng = list(range(low, high + 1, step))
+                        except ValueError as exc:
+                            raise CroniterBadCronError(f"invalid range: {exc}")
+
+                    if field_index == DOW_FIELD and nth and nth != "l":
+                        rng = [f"{item}#{nth}" for item in rng]
+                    e_list += [a for a in rng if a not in e_list]
+                else:
+                    if t.startswith("-"):
+                        raise CroniterBadCronError(
+                            f"[{expr_format}] is not acceptable, negative numbers not allowed"
+                        )
+                    if not star_or_int_re.search(t):
+                        t = cls._alphaconv(field_index, t, expressions)
+
+                    try:
+                        t = int(t)
+                    except ValueError:
+                        pass
+
+                    t = cls.value_alias(t, field_index, expressions)
+
+                    if t not in ["*", "l"] and (
+                        int(t) < cls.RANGES[field_index][0] or int(t) > cls.RANGES[field_index][1]
+                    ):
+                        raise CroniterBadCronError(
+                            f"[{expr_format}] is not acceptable, out of range"
+                        )
+
+                    res.append(t)
+
+                    if field_index == DOW_FIELD and nth:
+                        if t not in nth_weekday_of_month:
+                            nth_weekday_of_month[t] = set()
+                        nth_weekday_of_month[t].add(nth)
+
+            res = set(res)
+            res = sorted(res, key=lambda i: f"{i:02}" if isinstance(i, int) else i)
+            if len(res) == cls.LEN_MEANS_ALL[field_index]:
+                # Make sure the wildcard is used in the correct way (avoid over-optimization)
+                if (field_index == DAY_FIELD and "*" not in expressions[DOW_FIELD]) or (
+                    field_index == DOW_FIELD and "*" not in expressions[DAY_FIELD]
+                ):
+                    pass
+                else:
+                    res = ["*"]
+
+            expanded.append(["*"] if (len(res) == 1 and res[0] == "*") else res)
+
+        # Check to make sure the dow combo in use is supported
+        if nth_weekday_of_month:
+            dow_expanded_set = set(expanded[DOW_FIELD])
+            dow_expanded_set = dow_expanded_set.difference(nth_weekday_of_month.keys())
+            dow_expanded_set.discard("*")
+            # Skip: if it's all weeks instead of wildcard
+            if dow_expanded_set and len(set(expanded[DOW_FIELD])) != cls.LEN_MEANS_ALL[DOW_FIELD]:
+                raise CroniterUnsupportedSyntaxError(
+                    f"day-of-week field does not support mixing literal values and nth"
+                    f" day of week syntax.  Cron: '{expr_format}'"
+                    f"    dow={dow_expanded_set} vs nth={nth_weekday_of_month}"
+                )
+
+        if strict:
+            # Cross-validate day-of-month against month (and optionally year)
+            # to reject impossible combinations like "0 0 31 2 *" (Feb 31st).
+            days = expanded[DAY_FIELD]
+            months = expanded[MONTH_FIELD]
+            if days != ["*"] and days != ["l"] and months != ["*"]:
+                int_days = [d for d in days if isinstance(d, int)]
+                int_months = [m for m in months if isinstance(m, int)]
+                if int_days and int_months:
+                    # Determine max days per month, accounting for leap years
+                    days_in_month = dict(cls.DAYS_IN_MONTH)
+                    if 2 in int_months:
+                        has_leap_year = True  # assume possible by default
+                        if strict_year is not None:
+                            # Year explicitly provided as parameter
+                            if isinstance(strict_year, int):
+                                has_leap_year = calendar.isleap(strict_year)
+                            else:
+                                has_leap_year = any(calendar.isleap(y) for y in strict_year)
+                        elif len(expanded) > YEAR_FIELD:
+                            years = expanded[YEAR_FIELD]
+                            if years != ["*"]:
+                                int_years = [y for y in years if isinstance(y, int)]
+                                if int_years:
+                                    has_leap_year = any(calendar.isleap(y) for y in int_years)
+                        if has_leap_year:
+                            days_in_month[2] = 29
+                    min_day = min(int_days)
+                    max_possible = max(days_in_month[m] for m in int_months)
+                    if min_day > max_possible:
+                        raise CroniterBadCronError(
+                            f"[{expr_format}] is not acceptable. Day(s) {int_days}"
+                            f" can never occur in month(s) {int_months}"
+                        )
+
+        return expanded, nth_weekday_of_month, expressions, nearest_weekday
+
+    @classmethod
+    def expand(
+        cls,
+        expr_format: str,
+        hash_id: Optional[Union[bytes, str]] = None,
+        second_at_beginning: bool = False,
+        from_timestamp: Optional[float] = None,
+        strict: bool = False,
+        strict_year: Optional[Union[int, list[int]]] = None,
+    ) -> tuple[list[ExpandedExpression], dict[int, set[int]]]:
+        """
+        Expand a cron expression format into a noramlized format of
+        list[list[int | 'l' | '*']]. The first list representing each element
+        of the epxression, and each sub-list representing the allowed values
+        for that expression component.
+
+        A tuple is returned, the first value being the expanded epxression
+        list, and the second being a `nth_weekday_of_month` mapping.
+
+        Examples:
+
+        # Every minute
+        >>> croniter.expand('* * * * *')
+        ([['*'], ['*'], ['*'], ['*'], ['*']], {})
+
+        # On the hour
+        >>> croniter.expand('0 0 * * *')
+        ([[0], [0], ['*'], ['*'], ['*']], {})
+
+        # Hours 0-5 and 10 monday through friday
+        >>> croniter.expand('0-5,10 * * * mon-fri')
+        ([[0, 1, 2, 3, 4, 5, 10], ['*'], ['*'], ['*'], [1, 2, 3, 4, 5]], {})
+
+        Note that some special values such as nth day of week are expanded to a
+        special mapping format for later processing:
+
+        # Every minute on the 3rd tuesday of the month
+        >>> croniter.expand('* * * * 2#3')
+        ([['*'], ['*'], ['*'], ['*'], [2]], {2: {3}})
+
+        # Every hour on the last day of the month
+        >>> croniter.expand('0 * l * *')
+        ([[0], ['*'], ['l'], ['*'], ['*']], {})
+
+        # On the hour every 15 seconds
+        >>> croniter.expand('0 0 * * * */15')
+        ([[0], [0], ['*'], ['*'], ['*'], [0, 15, 30, 45]], {})
+        """
+        try:
+            expanded, nth_weekday_of_month, _expressions, _nearest_weekday = cls._expand(
+                expr_format,
+                hash_id=hash_id,
+                second_at_beginning=second_at_beginning,
+                from_timestamp=from_timestamp,
+                strict=strict,
+                strict_year=strict_year,
+            )
+            return expanded, nth_weekday_of_month
+        except (ValueError,) as exc:
+            if isinstance(exc, CroniterError):
+                raise
+            trace = _traceback.format_exc()
+            raise CroniterBadCronError(trace)
+
+    @classmethod
+    def _get_low_from_current_date_number(cls, field_index, step, from_timestamp):
+        dt = datetime.datetime.fromtimestamp(from_timestamp, tz=UTC_DT)
+        if field_index == MINUTE_FIELD:
+            return dt.minute % step
+        if field_index == HOUR_FIELD:
+            return dt.hour % step
+        if field_index == DAY_FIELD:
+            return ((dt.day - 1) % step) + 1
+        if field_index == MONTH_FIELD:
+            return dt.month % step
+        if field_index == DOW_FIELD:
+            return (dt.weekday() + 1) % step
+
+        raise ValueError("Can't get current date number for index larger than 4")
+
+    @classmethod
+    def is_valid(cls, expression, hash_id=None, encoding="UTF-8", second_at_beginning=False, strict=False, strict_year=None):
+        if hash_id:
+            if not isinstance(hash_id, (bytes, str)):
+                raise TypeError("hash_id must be bytes or UTF-8 string")
+            if not isinstance(hash_id, bytes):
+                hash_id = hash_id.encode(encoding)
+        try:
+            cls.expand(expression, hash_id=hash_id, second_at_beginning=second_at_beginning, strict=strict, strict_year=strict_year)
+        except CroniterError:
+            return False
+        return True
+
+    @classmethod
+    def match(
+        cls,
+        cron_expression,
+        testdate,
+        day_or=True,
+        second_at_beginning=False,
+        precision_in_seconds=None,
+    ):
+        return cls.match_range(
+            cron_expression, testdate, testdate, day_or, second_at_beginning, precision_in_seconds
+        )
+
+    @classmethod
+    def match_range(
+        cls,
+        cron_expression,
+        from_datetime,
+        to_datetime,
+        day_or=True,
+        second_at_beginning=False,
+        precision_in_seconds=None,
+    ):
+        cron = cls(
+            cron_expression,
+            to_datetime,
+            ret_type=datetime.datetime,
+            day_or=day_or,
+            second_at_beginning=second_at_beginning,
+        )
+        tdp = cron.get_current(datetime.datetime)
+        if not tdp.microsecond:
+            tdp += relativedelta(microseconds=1)
+        cron.set_current(tdp, force=True)
+        try:
+            tdt = cron.get_prev()
+        except CroniterBadDateError:
+            return False
+        if precision_in_seconds is None:
+            precision_in_seconds = 1 if len(cron.expanded) > UNIX_CRON_LEN else 60
+        duration_in_second = (to_datetime - from_datetime).total_seconds() + precision_in_seconds
+        return (max(tdp, tdt) - min(tdp, tdt)).total_seconds() < duration_in_second
+
+
+def croniter_range(
+    start,
+    stop,
+    expr_format,
+    ret_type=None,
+    day_or=True,
+    exclude_ends=False,
+    _croniter=None,
+    second_at_beginning=False,
+    expand_from_start_time=False,
+):
+    """
+    Generator that provides all times from start to stop matching the given cron expression.
+    If the cron expression matches either 'start' and/or 'stop', those times will be returned as
+    well unless 'exclude_ends=True' is passed.
+
+    You can think of this function as sibling to the builtin range function for datetime objects.
+    Like range(start,stop,step), except that here 'step' is a cron expression.
+    """
+    _croniter = _croniter or croniter
+    auto_rt = datetime.datetime
+    # type is used in first if branch for perfs reasons
+    if type(start) is not type(stop) and not (
+        isinstance(start, type(stop)) or isinstance(stop, type(start))
+    ):
+        raise CroniterBadTypeRangeError(
+            f"The start and stop must be same type.  {type(start)} != {type(stop)}"
+        )
+    if isinstance(start, (float, int)):
+        start, stop = (
+            datetime.datetime.fromtimestamp(t, tzutc()).replace(tzinfo=None) for t in (start, stop)
+        )
+        auto_rt = float
+    if ret_type is None:
+        ret_type = auto_rt
+    if not exclude_ends:
+        ms1 = relativedelta(microseconds=1)
+        if start < stop:  # Forward (normal) time order
+            start -= ms1
+            stop += ms1
+        else:  # Reverse time order
+            start += ms1
+            stop -= ms1
+    year_span = math.floor(abs(stop.year - start.year)) + 1
+    ic = _croniter(
+        expr_format,
+        start,
+        ret_type=datetime.datetime,
+        day_or=day_or,
+        max_years_between_matches=year_span,
+        second_at_beginning=second_at_beginning,
+        expand_from_start_time=expand_from_start_time,
+    )
+    # define a continue (cont) condition function and step function for the main while loop
+    if start < stop:  # Forward
+
+        def cont(v):
+            return v < stop
+
+        step = ic.get_next
+    else:  # Reverse
+
+        def cont(v):
+            return v > stop
+
+        step = ic.get_prev
+    try:
+        dt = step()
+        while cont(dt):
+            if ret_type is float:
+                yield ic.get_current(float)
+            else:
+                yield dt
+            dt = step()
+    except CroniterBadDateError:
+        # Stop iteration when this exception is raised; no match found within the given year range
+        return
+
+
+class HashExpander:
+    def __init__(self, cronit):
+        self.cron = cronit
+
+    def do(self, idx, hash_type="h", hash_id=None, range_end=None, range_begin=None):
+        """Return a hashed/random integer given range/hash information"""
+        if range_end is None:
+            range_end = self.cron.RANGES[idx][1]
+        if range_begin is None:
+            range_begin = self.cron.RANGES[idx][0]
+        if hash_type == "r":
+            crc = random.randint(0, 0xFFFFFFFF)
+        else:
+            crc = binascii.crc32(hash_id) & 0xFFFFFFFF
+        return ((crc >> idx) % (range_end - range_begin + 1)) + range_begin
+
+    def match(self, efl, idx, expr, hash_id=None, **kw):
+        return hash_expression_re.match(expr)
+
+    def expand(self, efl, idx, expr, hash_id=None, match="", **kw):
+        """Expand a hashed/random expression to its normal representation"""
+        if match == "":
+            match = self.match(efl, idx, expr, hash_id, **kw)
+        if not match:
+            return expr
+        m = match.groupdict()
+
+        if m["hash_type"] == "h" and hash_id is None:
+            raise CroniterBadCronError("Hashed definitions must include hash_id")
+
+        if m["range_begin"] and m["range_end"]:
+            if int(m["range_begin"]) >= int(m["range_end"]):
+                raise CroniterBadCronError("Range end must be greater than range begin")
+
+        if m["range_begin"] and m["range_end"] and m["divisor"]:
+            # Example: H(30-59)/10 -> 34-59/10 (i.e. 34,44,54)
+            if int(m["divisor"]) == 0:
+                raise CroniterBadCronError(f"Bad expression: {expr}")
+
+            x = self.do(
+                idx,
+                hash_type=m["hash_type"],
+                hash_id=hash_id,
+                range_begin=int(m["range_begin"]),
+                range_end=int(m["divisor"]) - 1 + int(m["range_begin"]),
+            )
+            return f"{x}-{int(m['range_end'])}/{int(m['divisor'])}"
+        if m["range_begin"] and m["range_end"]:
+            # Example: H(0-29) -> 12
+            return str(
+                self.do(
+                    idx,
+                    hash_type=m["hash_type"],
+                    hash_id=hash_id,
+                    range_end=int(m["range_end"]),
+                    range_begin=int(m["range_begin"]),
+                )
+            )
+        if m["divisor"]:
+            # Example: H/15 -> 7-59/15 (i.e. 7,22,37,52)
+            if int(m["divisor"]) == 0:
+                raise CroniterBadCronError(f"Bad expression: {expr}")
+
+            x = self.do(
+                idx,
+                hash_type=m["hash_type"],
+                hash_id=hash_id,
+                range_begin=self.cron.RANGES[idx][0],
+                range_end=int(m["divisor"]) - 1 + self.cron.RANGES[idx][0],
+            )
+            return f"{x}-{self.cron.RANGES[idx][1]}/{int(m['divisor'])}"
+
+        # Example: H -> 32
+        return str(self.do(idx, hash_type=m["hash_type"], hash_id=hash_id))
+
+
+EXPANDERS = {"hash": HashExpander}
diff --git a/crates/hm-dsl-engine/vendor/dateutil/__init__.py b/crates/hm-dsl-engine/vendor/dateutil/__init__.py
new file mode 100644
index 00000000..a2c19c06
--- /dev/null
+++ b/crates/hm-dsl-engine/vendor/dateutil/__init__.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+import sys
+
+try:
+    from ._version import version as __version__
+except ImportError:
+    __version__ = 'unknown'
+
+__all__ = ['easter', 'parser', 'relativedelta', 'rrule', 'tz',
+           'utils', 'zoneinfo']
+
+def __getattr__(name):
+    import importlib
+
+    if name in __all__:
+        return importlib.import_module("." + name, __name__)
+    raise AttributeError(
+        "module {!r} has not attribute {!r}".format(__name__, name)
+    )
+
+
+def __dir__():
+    # __dir__ should include all the lazy-importable modules as well.
+    return [x for x in globals() if x not in sys.modules] + __all__
diff --git a/crates/hm-dsl-engine/vendor/dateutil/_common.py b/crates/hm-dsl-engine/vendor/dateutil/_common.py
new file mode 100644
index 00000000..4eb2659b
--- /dev/null
+++ b/crates/hm-dsl-engine/vendor/dateutil/_common.py
@@ -0,0 +1,43 @@
+"""
+Common code used in multiple modules.
+"""
+
+
+class weekday(object):
+    __slots__ = ["weekday", "n"]
+
+    def __init__(self, weekday, n=None):
+        self.weekday = weekday
+        self.n = n
+
+    def __call__(self, n):
+        if n == self.n:
+            return self
+        else:
+            return self.__class__(self.weekday, n)
+
+    def __eq__(self, other):
+        try:
+            if self.weekday != other.weekday or self.n != other.n:
+                return False
+        except AttributeError:
+            return False
+        return True
+
+    def __hash__(self):
+        return hash((
+          self.weekday,
+          self.n,
+        ))
+
+    def __ne__(self, other):
+        return not (self == other)
+
+    def __repr__(self):
+        s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday]
+        if not self.n:
+            return s
+        else:
+            return "%s(%+d)" % (s, self.n)
+
+# vim:ts=4:sw=4:et
diff --git a/crates/hm-dsl-engine/vendor/dateutil/_version.py b/crates/hm-dsl-engine/vendor/dateutil/_version.py
new file mode 100644
index 00000000..ddda9809
--- /dev/null
+++ b/crates/hm-dsl-engine/vendor/dateutil/_version.py
@@ -0,0 +1,4 @@
+# file generated by setuptools_scm
+# don't change, don't track in version control
+__version__ = version = '2.9.0.post0'
+__version_tuple__ = version_tuple = (2, 9, 0)
diff --git a/crates/hm-dsl-engine/vendor/dateutil/easter.py b/crates/hm-dsl-engine/vendor/dateutil/easter.py
new file mode 100644
index 00000000..f74d1f74
--- /dev/null
+++ b/crates/hm-dsl-engine/vendor/dateutil/easter.py
@@ -0,0 +1,89 @@
+# -*- coding: utf-8 -*-
+"""
+This module offers a generic Easter computing method for any given year, using
+Western, Orthodox or Julian algorithms.
+"""
+
+import datetime
+
+__all__ = ["easter", "EASTER_JULIAN", "EASTER_ORTHODOX", "EASTER_WESTERN"]
+
+EASTER_JULIAN = 1
+EASTER_ORTHODOX = 2
+EASTER_WESTERN = 3
+
+
+def easter(year, method=EASTER_WESTERN):
+    """
+    This method was ported from the work done by GM Arts,
+    on top of the algorithm by Claus Tondering, which was
+    based in part on the algorithm of Ouding (1940), as
+    quoted in "Explanatory Supplement to the Astronomical
+    Almanac", P.  Kenneth Seidelmann, editor.
+
+    This algorithm implements three different Easter
+    calculation methods:
+
+    1. Original calculation in Julian calendar, valid in
+       dates after 326 AD
+    2. Original method, with date converted to Gregorian
+       calendar, valid in years 1583 to 4099
+    3. Revised method, in Gregorian calendar, valid in
+       years 1583 to 4099 as well
+
+    These methods are represented by the constants:
+
+    * ``EASTER_JULIAN   = 1``
+    * ``EASTER_ORTHODOX = 2``
+    * ``EASTER_WESTERN  = 3``
+
+    The default method is method 3.
+
+    More about the algorithm may be found at:
+
+    `GM Arts: Easter Algorithms `_
+
+    and
+
+    `The Calendar FAQ: Easter `_
+
+    """
+
+    if not (1 <= method <= 3):
+        raise ValueError("invalid method")
+
+    # g - Golden year - 1
+    # c - Century
+    # h - (23 - Epact) mod 30
+    # i - Number of days from March 21 to Paschal Full Moon
+    # j - Weekday for PFM (0=Sunday, etc)
+    # p - Number of days from March 21 to Sunday on or before PFM
+    #     (-6 to 28 methods 1 & 3, to 56 for method 2)
+    # e - Extra days to add for method 2 (converting Julian
+    #     date to Gregorian date)
+
+    y = year
+    g = y % 19
+    e = 0
+    if method < 3:
+        # Old method
+        i = (19*g + 15) % 30
+        j = (y + y//4 + i) % 7
+        if method == 2:
+            # Extra dates to convert Julian to Gregorian date
+            e = 10
+            if y > 1600:
+                e = e + y//100 - 16 - (y//100 - 16)//4
+    else:
+        # New method
+        c = y//100
+        h = (c - c//4 - (8*c + 13)//25 + 19*g + 15) % 30
+        i = h - (h//28)*(1 - (h//28)*(29//(h + 1))*((21 - g)//11))
+        j = (y + y//4 + i + 2 - c + c//4) % 7
+
+    # p can be from -6 to 56 corresponding to dates 22 March to 23 May
+    # (later dates apply to method 2, although 23 May never actually occurs)
+    p = i - j + e
+    d = 1 + (p + 27 + (p + 6)//40) % 31
+    m = 3 + (p + 26)//30
+    return datetime.date(int(y), int(m), int(d))
diff --git a/crates/hm-dsl-engine/vendor/dateutil/parser/__init__.py b/crates/hm-dsl-engine/vendor/dateutil/parser/__init__.py
new file mode 100644
index 00000000..d174b0e4
--- /dev/null
+++ b/crates/hm-dsl-engine/vendor/dateutil/parser/__init__.py
@@ -0,0 +1,61 @@
+# -*- coding: utf-8 -*-
+from ._parser import parse, parser, parserinfo, ParserError
+from ._parser import DEFAULTPARSER, DEFAULTTZPARSER
+from ._parser import UnknownTimezoneWarning
+
+from ._parser import __doc__
+
+from .isoparser import isoparser, isoparse
+
+__all__ = ['parse', 'parser', 'parserinfo',
+           'isoparse', 'isoparser',
+           'ParserError',
+           'UnknownTimezoneWarning']
+
+
+###
+# Deprecate portions of the private interface so that downstream code that
+# is improperly relying on it is given *some* notice.
+
+
+def __deprecated_private_func(f):
+    from functools import wraps
+    import warnings
+
+    msg = ('{name} is a private function and may break without warning, '
+           'it will be moved and or renamed in future versions.')
+    msg = msg.format(name=f.__name__)
+
+    @wraps(f)
+    def deprecated_func(*args, **kwargs):
+        warnings.warn(msg, DeprecationWarning)
+        return f(*args, **kwargs)
+
+    return deprecated_func
+
+def __deprecate_private_class(c):
+    import warnings
+
+    msg = ('{name} is a private class and may break without warning, '
+           'it will be moved and or renamed in future versions.')
+    msg = msg.format(name=c.__name__)
+
+    class private_class(c):
+        __doc__ = c.__doc__
+
+        def __init__(self, *args, **kwargs):
+            warnings.warn(msg, DeprecationWarning)
+            super(private_class, self).__init__(*args, **kwargs)
+
+    private_class.__name__ = c.__name__
+
+    return private_class
+
+
+from ._parser import _timelex, _resultbase
+from ._parser import _tzparser, _parsetz
+
+_timelex = __deprecate_private_class(_timelex)
+_tzparser = __deprecate_private_class(_tzparser)
+_resultbase = __deprecate_private_class(_resultbase)
+_parsetz = __deprecated_private_func(_parsetz)
diff --git a/crates/hm-dsl-engine/vendor/dateutil/parser/_parser.py b/crates/hm-dsl-engine/vendor/dateutil/parser/_parser.py
new file mode 100644
index 00000000..37d1663b
--- /dev/null
+++ b/crates/hm-dsl-engine/vendor/dateutil/parser/_parser.py
@@ -0,0 +1,1613 @@
+# -*- coding: utf-8 -*-
+"""
+This module offers a generic date/time string parser which is able to parse
+most known formats to represent a date and/or time.
+
+This module attempts to be forgiving with regards to unlikely input formats,
+returning a datetime object even for dates which are ambiguous. If an element
+of a date/time stamp is omitted, the following rules are applied:
+
+- If AM or PM is left unspecified, a 24-hour clock is assumed, however, an hour
+  on a 12-hour clock (``0 <= hour <= 12``) *must* be specified if AM or PM is
+  specified.
+- If a time zone is omitted, a timezone-naive datetime is returned.
+
+If any other elements are missing, they are taken from the
+:class:`datetime.datetime` object passed to the parameter ``default``. If this
+results in a day number exceeding the valid number of days per month, the
+value falls back to the end of the month.
+
+Additional resources about date/time string formats can be found below:
+
+- `A summary of the international standard date and time notation
+  `_
+- `W3C Date and Time Formats `_
+- `Time Formats (Planetary Rings Node) `_
+- `CPAN ParseDate module
+  `_
+- `Java SimpleDateFormat Class
+  `_
+"""
+from __future__ import unicode_literals
+
+import datetime
+import re
+import string
+import time
+import warnings
+
+from calendar import monthrange
+from io import StringIO
+
+import six
+from six import integer_types, text_type
+
+from decimal import Decimal
+
+from warnings import warn
+
+from .. import relativedelta
+from .. import tz
+
+__all__ = ["parse", "parserinfo", "ParserError"]
+
+
+# TODO: pandas.core.tools.datetimes imports this explicitly.  Might be worth
+# making public and/or figuring out if there is something we can
+# take off their plate.
+class _timelex(object):
+    # Fractional seconds are sometimes split by a comma
+    _split_decimal = re.compile("([.,])")
+
+    def __init__(self, instream):
+        if isinstance(instream, (bytes, bytearray)):
+            instream = instream.decode()
+
+        if isinstance(instream, text_type):
+            instream = StringIO(instream)
+        elif getattr(instream, 'read', None) is None:
+            raise TypeError('Parser must be a string or character stream, not '
+                            '{itype}'.format(itype=instream.__class__.__name__))
+
+        self.instream = instream
+        self.charstack = []
+        self.tokenstack = []
+        self.eof = False
+
+    def get_token(self):
+        """
+        This function breaks the time string into lexical units (tokens), which
+        can be parsed by the parser. Lexical units are demarcated by changes in
+        the character set, so any continuous string of letters is considered
+        one unit, any continuous string of numbers is considered one unit.
+
+        The main complication arises from the fact that dots ('.') can be used
+        both as separators (e.g. "Sep.20.2009") or decimal points (e.g.
+        "4:30:21.447"). As such, it is necessary to read the full context of
+        any dot-separated strings before breaking it into tokens; as such, this
+        function maintains a "token stack", for when the ambiguous context
+        demands that multiple tokens be parsed at once.
+        """
+        if self.tokenstack:
+            return self.tokenstack.pop(0)
+
+        seenletters = False
+        token = None
+        state = None
+
+        while not self.eof:
+            # We only realize that we've reached the end of a token when we
+            # find a character that's not part of the current token - since
+            # that character may be part of the next token, it's stored in the
+            # charstack.
+            if self.charstack:
+                nextchar = self.charstack.pop(0)
+            else:
+                nextchar = self.instream.read(1)
+                while nextchar == '\x00':
+                    nextchar = self.instream.read(1)
+
+            if not nextchar:
+                self.eof = True
+                break
+            elif not state:
+                # First character of the token - determines if we're starting
+                # to parse a word, a number or something else.
+                token = nextchar
+                if self.isword(nextchar):
+                    state = 'a'
+                elif self.isnum(nextchar):
+                    state = '0'
+                elif self.isspace(nextchar):
+                    token = ' '
+                    break  # emit token
+                else:
+                    break  # emit token
+            elif state == 'a':
+                # If we've already started reading a word, we keep reading
+                # letters until we find something that's not part of a word.
+                seenletters = True
+                if self.isword(nextchar):
+                    token += nextchar
+                elif nextchar == '.':
+                    token += nextchar
+                    state = 'a.'
+                else:
+                    self.charstack.append(nextchar)
+                    break  # emit token
+            elif state == '0':
+                # If we've already started reading a number, we keep reading
+                # numbers until we find something that doesn't fit.
+                if self.isnum(nextchar):
+                    token += nextchar
+                elif nextchar == '.' or (nextchar == ',' and len(token) >= 2):
+                    token += nextchar
+                    state = '0.'
+                else:
+                    self.charstack.append(nextchar)
+                    break  # emit token
+            elif state == 'a.':
+                # If we've seen some letters and a dot separator, continue
+                # parsing, and the tokens will be broken up later.
+                seenletters = True
+                if nextchar == '.' or self.isword(nextchar):
+                    token += nextchar
+                elif self.isnum(nextchar) and token[-1] == '.':
+                    token += nextchar
+                    state = '0.'
+                else:
+                    self.charstack.append(nextchar)
+                    break  # emit token
+            elif state == '0.':
+                # If we've seen at least one dot separator, keep going, we'll
+                # break up the tokens later.
+                if nextchar == '.' or self.isnum(nextchar):
+                    token += nextchar
+                elif self.isword(nextchar) and token[-1] == '.':
+                    token += nextchar
+                    state = 'a.'
+                else:
+                    self.charstack.append(nextchar)
+                    break  # emit token
+
+        if (state in ('a.', '0.') and (seenletters or token.count('.') > 1 or
+                                       token[-1] in '.,')):
+            l = self._split_decimal.split(token)
+            token = l[0]
+            for tok in l[1:]:
+                if tok:
+                    self.tokenstack.append(tok)
+
+        if state == '0.' and token.count('.') == 0:
+            token = token.replace(',', '.')
+
+        return token
+
+    def __iter__(self):
+        return self
+
+    def __next__(self):
+        token = self.get_token()
+        if token is None:
+            raise StopIteration
+
+        return token
+
+    def next(self):
+        return self.__next__()  # Python 2.x support
+
+    @classmethod
+    def split(cls, s):
+        return list(cls(s))
+
+    @classmethod
+    def isword(cls, nextchar):
+        """ Whether or not the next character is part of a word """
+        return nextchar.isalpha()
+
+    @classmethod
+    def isnum(cls, nextchar):
+        """ Whether the next character is part of a number """
+        return nextchar.isdigit()
+
+    @classmethod
+    def isspace(cls, nextchar):
+        """ Whether the next character is whitespace """
+        return nextchar.isspace()
+
+
+class _resultbase(object):
+
+    def __init__(self):
+        for attr in self.__slots__:
+            setattr(self, attr, None)
+
+    def _repr(self, classname):
+        l = []
+        for attr in self.__slots__:
+            value = getattr(self, attr)
+            if value is not None:
+                l.append("%s=%s" % (attr, repr(value)))
+        return "%s(%s)" % (classname, ", ".join(l))
+
+    def __len__(self):
+        return (sum(getattr(self, attr) is not None
+                    for attr in self.__slots__))
+
+    def __repr__(self):
+        return self._repr(self.__class__.__name__)
+
+
+class parserinfo(object):
+    """
+    Class which handles what inputs are accepted. Subclass this to customize
+    the language and acceptable values for each parameter.
+
+    :param dayfirst:
+        Whether to interpret the first value in an ambiguous 3-integer date
+        (e.g. 01/05/09) as the day (``True``) or month (``False``). If
+        ``yearfirst`` is set to ``True``, this distinguishes between YDM
+        and YMD. Default is ``False``.
+
+    :param yearfirst:
+        Whether to interpret the first value in an ambiguous 3-integer date
+        (e.g. 01/05/09) as the year. If ``True``, the first number is taken
+        to be the year, otherwise the last number is taken to be the year.
+        Default is ``False``.
+    """
+
+    # m from a.m/p.m, t from ISO T separator
+    JUMP = [" ", ".", ",", ";", "-", "/", "'",
+            "at", "on", "and", "ad", "m", "t", "of",
+            "st", "nd", "rd", "th"]
+
+    WEEKDAYS = [("Mon", "Monday"),
+                ("Tue", "Tuesday"),     # TODO: "Tues"
+                ("Wed", "Wednesday"),
+                ("Thu", "Thursday"),    # TODO: "Thurs"
+                ("Fri", "Friday"),
+                ("Sat", "Saturday"),
+                ("Sun", "Sunday")]
+    MONTHS = [("Jan", "January"),
+              ("Feb", "February"),      # TODO: "Febr"
+              ("Mar", "March"),
+              ("Apr", "April"),
+              ("May", "May"),
+              ("Jun", "June"),
+              ("Jul", "July"),
+              ("Aug", "August"),
+              ("Sep", "Sept", "September"),
+              ("Oct", "October"),
+              ("Nov", "November"),
+              ("Dec", "December")]
+    HMS = [("h", "hour", "hours"),
+           ("m", "minute", "minutes"),
+           ("s", "second", "seconds")]
+    AMPM = [("am", "a"),
+            ("pm", "p")]
+    UTCZONE = ["UTC", "GMT", "Z", "z"]
+    PERTAIN = ["of"]
+    TZOFFSET = {}
+    # TODO: ERA = ["AD", "BC", "CE", "BCE", "Stardate",
+    #              "Anno Domini", "Year of Our Lord"]
+
+    def __init__(self, dayfirst=False, yearfirst=False):
+        self._jump = self._convert(self.JUMP)
+        self._weekdays = self._convert(self.WEEKDAYS)
+        self._months = self._convert(self.MONTHS)
+        self._hms = self._convert(self.HMS)
+        self._ampm = self._convert(self.AMPM)
+        self._utczone = self._convert(self.UTCZONE)
+        self._pertain = self._convert(self.PERTAIN)
+
+        self.dayfirst = dayfirst
+        self.yearfirst = yearfirst
+
+        self._year = time.localtime().tm_year
+        self._century = self._year // 100 * 100
+
+    def _convert(self, lst):
+        dct = {}
+        for i, v in enumerate(lst):
+            if isinstance(v, tuple):
+                for v in v:
+                    dct[v.lower()] = i
+            else:
+                dct[v.lower()] = i
+        return dct
+
+    def jump(self, name):
+        return name.lower() in self._jump
+
+    def weekday(self, name):
+        try:
+            return self._weekdays[name.lower()]
+        except KeyError:
+            pass
+        return None
+
+    def month(self, name):
+        try:
+            return self._months[name.lower()] + 1
+        except KeyError:
+            pass
+        return None
+
+    def hms(self, name):
+        try:
+            return self._hms[name.lower()]
+        except KeyError:
+            return None
+
+    def ampm(self, name):
+        try:
+            return self._ampm[name.lower()]
+        except KeyError:
+            return None
+
+    def pertain(self, name):
+        return name.lower() in self._pertain
+
+    def utczone(self, name):
+        return name.lower() in self._utczone
+
+    def tzoffset(self, name):
+        if name in self._utczone:
+            return 0
+
+        return self.TZOFFSET.get(name)
+
+    def convertyear(self, year, century_specified=False):
+        """
+        Converts two-digit years to year within [-50, 49]
+        range of self._year (current local time)
+        """
+
+        # Function contract is that the year is always positive
+        assert year >= 0
+
+        if year < 100 and not century_specified:
+            # assume current century to start
+            year += self._century
+
+            if year >= self._year + 50:  # if too far in future
+                year -= 100
+            elif year < self._year - 50:  # if too far in past
+                year += 100
+
+        return year
+
+    def validate(self, res):
+        # move to info
+        if res.year is not None:
+            res.year = self.convertyear(res.year, res.century_specified)
+
+        if ((res.tzoffset == 0 and not res.tzname) or
+             (res.tzname == 'Z' or res.tzname == 'z')):
+            res.tzname = "UTC"
+            res.tzoffset = 0
+        elif res.tzoffset != 0 and res.tzname and self.utczone(res.tzname):
+            res.tzoffset = 0
+        return True
+
+
+class _ymd(list):
+    def __init__(self, *args, **kwargs):
+        super(self.__class__, self).__init__(*args, **kwargs)
+        self.century_specified = False
+        self.dstridx = None
+        self.mstridx = None
+        self.ystridx = None
+
+    @property
+    def has_year(self):
+        return self.ystridx is not None
+
+    @property
+    def has_month(self):
+        return self.mstridx is not None
+
+    @property
+    def has_day(self):
+        return self.dstridx is not None
+
+    def could_be_day(self, value):
+        if self.has_day:
+            return False
+        elif not self.has_month:
+            return 1 <= value <= 31
+        elif not self.has_year:
+            # Be permissive, assume leap year
+            month = self[self.mstridx]
+            return 1 <= value <= monthrange(2000, month)[1]
+        else:
+            month = self[self.mstridx]
+            year = self[self.ystridx]
+            return 1 <= value <= monthrange(year, month)[1]
+
+    def append(self, val, label=None):
+        if hasattr(val, '__len__'):
+            if val.isdigit() and len(val) > 2:
+                self.century_specified = True
+                if label not in [None, 'Y']:  # pragma: no cover
+                    raise ValueError(label)
+                label = 'Y'
+        elif val > 100:
+            self.century_specified = True
+            if label not in [None, 'Y']:  # pragma: no cover
+                raise ValueError(label)
+            label = 'Y'
+
+        super(self.__class__, self).append(int(val))
+
+        if label == 'M':
+            if self.has_month:
+                raise ValueError('Month is already set')
+            self.mstridx = len(self) - 1
+        elif label == 'D':
+            if self.has_day:
+                raise ValueError('Day is already set')
+            self.dstridx = len(self) - 1
+        elif label == 'Y':
+            if self.has_year:
+                raise ValueError('Year is already set')
+            self.ystridx = len(self) - 1
+
+    def _resolve_from_stridxs(self, strids):
+        """
+        Try to resolve the identities of year/month/day elements using
+        ystridx, mstridx, and dstridx, if enough of these are specified.
+        """
+        if len(self) == 3 and len(strids) == 2:
+            # we can back out the remaining stridx value
+            missing = [x for x in range(3) if x not in strids.values()]
+            key = [x for x in ['y', 'm', 'd'] if x not in strids]
+            assert len(missing) == len(key) == 1
+            key = key[0]
+            val = missing[0]
+            strids[key] = val
+
+        assert len(self) == len(strids)  # otherwise this should not be called
+        out = {key: self[strids[key]] for key in strids}
+        return (out.get('y'), out.get('m'), out.get('d'))
+
+    def resolve_ymd(self, yearfirst, dayfirst):
+        len_ymd = len(self)
+        year, month, day = (None, None, None)
+
+        strids = (('y', self.ystridx),
+                  ('m', self.mstridx),
+                  ('d', self.dstridx))
+
+        strids = {key: val for key, val in strids if val is not None}
+        if (len(self) == len(strids) > 0 or
+                (len(self) == 3 and len(strids) == 2)):
+            return self._resolve_from_stridxs(strids)
+
+        mstridx = self.mstridx
+
+        if len_ymd > 3:
+            raise ValueError("More than three YMD values")
+        elif len_ymd == 1 or (mstridx is not None and len_ymd == 2):
+            # One member, or two members with a month string
+            if mstridx is not None:
+                month = self[mstridx]
+                # since mstridx is 0 or 1, self[mstridx-1] always
+                # looks up the other element
+                other = self[mstridx - 1]
+            else:
+                other = self[0]
+
+            if len_ymd > 1 or mstridx is None:
+                if other > 31:
+                    year = other
+                else:
+                    day = other
+
+        elif len_ymd == 2:
+            # Two members with numbers
+            if self[0] > 31:
+                # 99-01
+                year, month = self
+            elif self[1] > 31:
+                # 01-99
+                month, year = self
+            elif dayfirst and self[1] <= 12:
+                # 13-01
+                day, month = self
+            else:
+                # 01-13
+                month, day = self
+
+        elif len_ymd == 3:
+            # Three members
+            if mstridx == 0:
+                if self[1] > 31:
+                    # Apr-2003-25
+                    month, year, day = self
+                else:
+                    month, day, year = self
+            elif mstridx == 1:
+                if self[0] > 31 or (yearfirst and self[2] <= 31):
+                    # 99-Jan-01
+                    year, month, day = self
+                else:
+                    # 01-Jan-01
+                    # Give precedence to day-first, since
+                    # two-digit years is usually hand-written.
+                    day, month, year = self
+
+            elif mstridx == 2:
+                # WTF!?
+                if self[1] > 31:
+                    # 01-99-Jan
+                    day, year, month = self
+                else:
+                    # 99-01-Jan
+                    year, day, month = self
+
+            else:
+                if (self[0] > 31 or
+                    self.ystridx == 0 or
+                        (yearfirst and self[1] <= 12 and self[2] <= 31)):
+                    # 99-01-01
+                    if dayfirst and self[2] <= 12:
+                        year, day, month = self
+                    else:
+                        year, month, day = self
+                elif self[0] > 12 or (dayfirst and self[1] <= 12):
+                    # 13-01-01
+                    day, month, year = self
+                else:
+                    # 01-13-01
+                    month, day, year = self
+
+        return year, month, day
+
+
+class parser(object):
+    def __init__(self, info=None):
+        self.info = info or parserinfo()
+
+    def parse(self, timestr, default=None,
+              ignoretz=False, tzinfos=None, **kwargs):
+        """
+        Parse the date/time string into a :class:`datetime.datetime` object.
+
+        :param timestr:
+            Any date/time string using the supported formats.
+
+        :param default:
+            The default datetime object, if this is a datetime object and not
+            ``None``, elements specified in ``timestr`` replace elements in the
+            default object.
+
+        :param ignoretz:
+            If set ``True``, time zones in parsed strings are ignored and a
+            naive :class:`datetime.datetime` object is returned.
+
+        :param tzinfos:
+            Additional time zone names / aliases which may be present in the
+            string. This argument maps time zone names (and optionally offsets
+            from those time zones) to time zones. This parameter can be a
+            dictionary with timezone aliases mapping time zone names to time
+            zones or a function taking two parameters (``tzname`` and
+            ``tzoffset``) and returning a time zone.
+
+            The timezones to which the names are mapped can be an integer
+            offset from UTC in seconds or a :class:`tzinfo` object.
+
+            .. doctest::
+               :options: +NORMALIZE_WHITESPACE
+
+                >>> from dateutil.parser import parse
+                >>> from dateutil.tz import gettz
+                >>> tzinfos = {"BRST": -7200, "CST": gettz("America/Chicago")}
+                >>> parse("2012-01-19 17:21:00 BRST", tzinfos=tzinfos)
+                datetime.datetime(2012, 1, 19, 17, 21, tzinfo=tzoffset(u'BRST', -7200))
+                >>> parse("2012-01-19 17:21:00 CST", tzinfos=tzinfos)
+                datetime.datetime(2012, 1, 19, 17, 21,
+                                  tzinfo=tzfile('/usr/share/zoneinfo/America/Chicago'))
+
+            This parameter is ignored if ``ignoretz`` is set.
+
+        :param \\*\\*kwargs:
+            Keyword arguments as passed to ``_parse()``.
+
+        :return:
+            Returns a :class:`datetime.datetime` object or, if the
+            ``fuzzy_with_tokens`` option is ``True``, returns a tuple, the
+            first element being a :class:`datetime.datetime` object, the second
+            a tuple containing the fuzzy tokens.
+
+        :raises ParserError:
+            Raised for invalid or unknown string format, if the provided
+            :class:`tzinfo` is not in a valid format, or if an invalid date
+            would be created.
+
+        :raises TypeError:
+            Raised for non-string or character stream input.
+
+        :raises OverflowError:
+            Raised if the parsed date exceeds the largest valid C integer on
+            your system.
+        """
+
+        if default is None:
+            default = datetime.datetime.now().replace(hour=0, minute=0,
+                                                      second=0, microsecond=0)
+
+        res, skipped_tokens = self._parse(timestr, **kwargs)
+
+        if res is None:
+            raise ParserError("Unknown string format: %s", timestr)
+
+        if len(res) == 0:
+            raise ParserError("String does not contain a date: %s", timestr)
+
+        try:
+            ret = self._build_naive(res, default)
+        except ValueError as e:
+            six.raise_from(ParserError(str(e) + ": %s", timestr), e)
+
+        if not ignoretz:
+            ret = self._build_tzaware(ret, res, tzinfos)
+
+        if kwargs.get('fuzzy_with_tokens', False):
+            return ret, skipped_tokens
+        else:
+            return ret
+
+    class _result(_resultbase):
+        __slots__ = ["year", "month", "day", "weekday",
+                     "hour", "minute", "second", "microsecond",
+                     "tzname", "tzoffset", "ampm","any_unused_tokens"]
+
+    def _parse(self, timestr, dayfirst=None, yearfirst=None, fuzzy=False,
+               fuzzy_with_tokens=False):
+        """
+        Private method which performs the heavy lifting of parsing, called from
+        ``parse()``, which passes on its ``kwargs`` to this function.
+
+        :param timestr:
+            The string to parse.
+
+        :param dayfirst:
+            Whether to interpret the first value in an ambiguous 3-integer date
+            (e.g. 01/05/09) as the day (``True``) or month (``False``). If
+            ``yearfirst`` is set to ``True``, this distinguishes between YDM
+            and YMD. If set to ``None``, this value is retrieved from the
+            current :class:`parserinfo` object (which itself defaults to
+            ``False``).
+
+        :param yearfirst:
+            Whether to interpret the first value in an ambiguous 3-integer date
+            (e.g. 01/05/09) as the year. If ``True``, the first number is taken
+            to be the year, otherwise the last number is taken to be the year.
+            If this is set to ``None``, the value is retrieved from the current
+            :class:`parserinfo` object (which itself defaults to ``False``).
+
+        :param fuzzy:
+            Whether to allow fuzzy parsing, allowing for string like "Today is
+            January 1, 2047 at 8:21:00AM".
+
+        :param fuzzy_with_tokens:
+            If ``True``, ``fuzzy`` is automatically set to True, and the parser
+            will return a tuple where the first element is the parsed
+            :class:`datetime.datetime` datetimestamp and the second element is
+            a tuple containing the portions of the string which were ignored:
+
+            .. doctest::
+
+                >>> from dateutil.parser import parse
+                >>> parse("Today is January 1, 2047 at 8:21:00AM", fuzzy_with_tokens=True)
+                (datetime.datetime(2047, 1, 1, 8, 21), (u'Today is ', u' ', u'at '))
+
+        """
+        if fuzzy_with_tokens:
+            fuzzy = True
+
+        info = self.info
+
+        if dayfirst is None:
+            dayfirst = info.dayfirst
+
+        if yearfirst is None:
+            yearfirst = info.yearfirst
+
+        res = self._result()
+        l = _timelex.split(timestr)         # Splits the timestr into tokens
+
+        skipped_idxs = []
+
+        # year/month/day list
+        ymd = _ymd()
+
+        len_l = len(l)
+        i = 0
+        try:
+            while i < len_l:
+
+                # Check if it's a number
+                value_repr = l[i]
+                try:
+                    value = float(value_repr)
+                except ValueError:
+                    value = None
+
+                if value is not None:
+                    # Numeric token
+                    i = self._parse_numeric_token(l, i, info, ymd, res, fuzzy)
+
+                # Check weekday
+                elif info.weekday(l[i]) is not None:
+                    value = info.weekday(l[i])
+                    res.weekday = value
+
+                # Check month name
+                elif info.month(l[i]) is not None:
+                    value = info.month(l[i])
+                    ymd.append(value, 'M')
+
+                    if i + 1 < len_l:
+                        if l[i + 1] in ('-', '/'):
+                            # Jan-01[-99]
+                            sep = l[i + 1]
+                            ymd.append(l[i + 2])
+
+                            if i + 3 < len_l and l[i + 3] == sep:
+                                # Jan-01-99
+                                ymd.append(l[i + 4])
+                                i += 2
+
+                            i += 2
+
+                        elif (i + 4 < len_l and l[i + 1] == l[i + 3] == ' ' and
+                              info.pertain(l[i + 2])):
+                            # Jan of 01
+                            # In this case, 01 is clearly year
+                            if l[i + 4].isdigit():
+                                # Convert it here to become unambiguous
+                                value = int(l[i + 4])
+                                year = str(info.convertyear(value))
+                                ymd.append(year, 'Y')
+                            else:
+                                # Wrong guess
+                                pass
+                                # TODO: not hit in tests
+                            i += 4
+
+                # Check am/pm
+                elif info.ampm(l[i]) is not None:
+                    value = info.ampm(l[i])
+                    val_is_ampm = self._ampm_valid(res.hour, res.ampm, fuzzy)
+
+                    if val_is_ampm:
+                        res.hour = self._adjust_ampm(res.hour, value)
+                        res.ampm = value
+
+                    elif fuzzy:
+                        skipped_idxs.append(i)
+
+                # Check for a timezone name
+                elif self._could_be_tzname(res.hour, res.tzname, res.tzoffset, l[i]):
+                    res.tzname = l[i]
+                    res.tzoffset = info.tzoffset(res.tzname)
+
+                    # Check for something like GMT+3, or BRST+3. Notice
+                    # that it doesn't mean "I am 3 hours after GMT", but
+                    # "my time +3 is GMT". If found, we reverse the
+                    # logic so that timezone parsing code will get it
+                    # right.
+                    if i + 1 < len_l and l[i + 1] in ('+', '-'):
+                        l[i + 1] = ('+', '-')[l[i + 1] == '+']
+                        res.tzoffset = None
+                        if info.utczone(res.tzname):
+                            # With something like GMT+3, the timezone
+                            # is *not* GMT.
+                            res.tzname = None
+
+                # Check for a numbered timezone
+                elif res.hour is not None and l[i] in ('+', '-'):
+                    signal = (-1, 1)[l[i] == '+']
+                    len_li = len(l[i + 1])
+
+                    # TODO: check that l[i + 1] is integer?
+                    if len_li == 4:
+                        # -0300
+                        hour_offset = int(l[i + 1][:2])
+                        min_offset = int(l[i + 1][2:])
+                    elif i + 2 < len_l and l[i + 2] == ':':
+                        # -03:00
+                        hour_offset = int(l[i + 1])
+                        min_offset = int(l[i + 3])  # TODO: Check that l[i+3] is minute-like?
+                        i += 2
+                    elif len_li <= 2:
+                        # -[0]3
+                        hour_offset = int(l[i + 1][:2])
+                        min_offset = 0
+                    else:
+                        raise ValueError(timestr)
+
+                    res.tzoffset = signal * (hour_offset * 3600 + min_offset * 60)
+
+                    # Look for a timezone name between parenthesis
+                    if (i + 5 < len_l and
+                            info.jump(l[i + 2]) and l[i + 3] == '(' and
+                            l[i + 5] == ')' and
+                            3 <= len(l[i + 4]) and
+                            self._could_be_tzname(res.hour, res.tzname,
+                                                  None, l[i + 4])):
+                        # -0300 (BRST)
+                        res.tzname = l[i + 4]
+                        i += 4
+
+                    i += 1
+
+                # Check jumps
+                elif not (info.jump(l[i]) or fuzzy):
+                    raise ValueError(timestr)
+
+                else:
+                    skipped_idxs.append(i)
+                i += 1
+
+            # Process year/month/day
+            year, month, day = ymd.resolve_ymd(yearfirst, dayfirst)
+
+            res.century_specified = ymd.century_specified
+            res.year = year
+            res.month = month
+            res.day = day
+
+        except (IndexError, ValueError):
+            return None, None
+
+        if not info.validate(res):
+            return None, None
+
+        if fuzzy_with_tokens:
+            skipped_tokens = self._recombine_skipped(l, skipped_idxs)
+            return res, tuple(skipped_tokens)
+        else:
+            return res, None
+
+    def _parse_numeric_token(self, tokens, idx, info, ymd, res, fuzzy):
+        # Token is a number
+        value_repr = tokens[idx]
+        try:
+            value = self._to_decimal(value_repr)
+        except Exception as e:
+            six.raise_from(ValueError('Unknown numeric token'), e)
+
+        len_li = len(value_repr)
+
+        len_l = len(tokens)
+
+        if (len(ymd) == 3 and len_li in (2, 4) and
+            res.hour is None and
+            (idx + 1 >= len_l or
+             (tokens[idx + 1] != ':' and
+              info.hms(tokens[idx + 1]) is None))):
+            # 19990101T23[59]
+            s = tokens[idx]
+            res.hour = int(s[:2])
+
+            if len_li == 4:
+                res.minute = int(s[2:])
+
+        elif len_li == 6 or (len_li > 6 and tokens[idx].find('.') == 6):
+            # YYMMDD or HHMMSS[.ss]
+            s = tokens[idx]
+
+            if not ymd and '.' not in tokens[idx]:
+                ymd.append(s[:2])
+                ymd.append(s[2:4])
+                ymd.append(s[4:])
+            else:
+                # 19990101T235959[.59]
+
+                # TODO: Check if res attributes already set.
+                res.hour = int(s[:2])
+                res.minute = int(s[2:4])
+                res.second, res.microsecond = self._parsems(s[4:])
+
+        elif len_li in (8, 12, 14):
+            # YYYYMMDD
+            s = tokens[idx]
+            ymd.append(s[:4], 'Y')
+            ymd.append(s[4:6])
+            ymd.append(s[6:8])
+
+            if len_li > 8:
+                res.hour = int(s[8:10])
+                res.minute = int(s[10:12])
+
+                if len_li > 12:
+                    res.second = int(s[12:])
+
+        elif self._find_hms_idx(idx, tokens, info, allow_jump=True) is not None:
+            # HH[ ]h or MM[ ]m or SS[.ss][ ]s
+            hms_idx = self._find_hms_idx(idx, tokens, info, allow_jump=True)
+            (idx, hms) = self._parse_hms(idx, tokens, info, hms_idx)
+            if hms is not None:
+                # TODO: checking that hour/minute/second are not
+                # already set?
+                self._assign_hms(res, value_repr, hms)
+
+        elif idx + 2 < len_l and tokens[idx + 1] == ':':
+            # HH:MM[:SS[.ss]]
+            res.hour = int(value)
+            value = self._to_decimal(tokens[idx + 2])  # TODO: try/except for this?
+            (res.minute, res.second) = self._parse_min_sec(value)
+
+            if idx + 4 < len_l and tokens[idx + 3] == ':':
+                res.second, res.microsecond = self._parsems(tokens[idx + 4])
+
+                idx += 2
+
+            idx += 2
+
+        elif idx + 1 < len_l and tokens[idx + 1] in ('-', '/', '.'):
+            sep = tokens[idx + 1]
+            ymd.append(value_repr)
+
+            if idx + 2 < len_l and not info.jump(tokens[idx + 2]):
+                if tokens[idx + 2].isdigit():
+                    # 01-01[-01]
+                    ymd.append(tokens[idx + 2])
+                else:
+                    # 01-Jan[-01]
+                    value = info.month(tokens[idx + 2])
+
+                    if value is not None:
+                        ymd.append(value, 'M')
+                    else:
+                        raise ValueError()
+
+                if idx + 3 < len_l and tokens[idx + 3] == sep:
+                    # We have three members
+                    value = info.month(tokens[idx + 4])
+
+                    if value is not None:
+                        ymd.append(value, 'M')
+                    else:
+                        ymd.append(tokens[idx + 4])
+                    idx += 2
+
+                idx += 1
+            idx += 1
+
+        elif idx + 1 >= len_l or info.jump(tokens[idx + 1]):
+            if idx + 2 < len_l and info.ampm(tokens[idx + 2]) is not None:
+                # 12 am
+                hour = int(value)
+                res.hour = self._adjust_ampm(hour, info.ampm(tokens[idx + 2]))
+                idx += 1
+            else:
+                # Year, month or day
+                ymd.append(value)
+            idx += 1
+
+        elif info.ampm(tokens[idx + 1]) is not None and (0 <= value < 24):
+            # 12am
+            hour = int(value)
+            res.hour = self._adjust_ampm(hour, info.ampm(tokens[idx + 1]))
+            idx += 1
+
+        elif ymd.could_be_day(value):
+            ymd.append(value)
+
+        elif not fuzzy:
+            raise ValueError()
+
+        return idx
+
+    def _find_hms_idx(self, idx, tokens, info, allow_jump):
+        len_l = len(tokens)
+
+        if idx+1 < len_l and info.hms(tokens[idx+1]) is not None:
+            # There is an "h", "m", or "s" label following this token.  We take
+            # assign the upcoming label to the current token.
+            # e.g. the "12" in 12h"
+            hms_idx = idx + 1
+
+        elif (allow_jump and idx+2 < len_l and tokens[idx+1] == ' ' and
+              info.hms(tokens[idx+2]) is not None):
+            # There is a space and then an "h", "m", or "s" label.
+            # e.g. the "12" in "12 h"
+            hms_idx = idx + 2
+
+        elif idx > 0 and info.hms(tokens[idx-1]) is not None:
+            # There is a "h", "m", or "s" preceding this token.  Since neither
+            # of the previous cases was hit, there is no label following this
+            # token, so we use the previous label.
+            # e.g. the "04" in "12h04"
+            hms_idx = idx-1
+
+        elif (1 < idx == len_l-1 and tokens[idx-1] == ' ' and
+              info.hms(tokens[idx-2]) is not None):
+            # If we are looking at the final token, we allow for a
+            # backward-looking check to skip over a space.
+            # TODO: Are we sure this is the right condition here?
+            hms_idx = idx - 2
+
+        else:
+            hms_idx = None
+
+        return hms_idx
+
+    def _assign_hms(self, res, value_repr, hms):
+        # See GH issue #427, fixing float rounding
+        value = self._to_decimal(value_repr)
+
+        if hms == 0:
+            # Hour
+            res.hour = int(value)
+            if value % 1:
+                res.minute = int(60*(value % 1))
+
+        elif hms == 1:
+            (res.minute, res.second) = self._parse_min_sec(value)
+
+        elif hms == 2:
+            (res.second, res.microsecond) = self._parsems(value_repr)
+
+    def _could_be_tzname(self, hour, tzname, tzoffset, token):
+        return (hour is not None and
+                tzname is None and
+                tzoffset is None and
+                len(token) <= 5 and
+                (all(x in string.ascii_uppercase for x in token)
+                 or token in self.info.UTCZONE))
+
+    def _ampm_valid(self, hour, ampm, fuzzy):
+        """
+        For fuzzy parsing, 'a' or 'am' (both valid English words)
+        may erroneously trigger the AM/PM flag. Deal with that
+        here.
+        """
+        val_is_ampm = True
+
+        # If there's already an AM/PM flag, this one isn't one.
+        if fuzzy and ampm is not None:
+            val_is_ampm = False
+
+        # If AM/PM is found and hour is not, raise a ValueError
+        if hour is None:
+            if fuzzy:
+                val_is_ampm = False
+            else:
+                raise ValueError('No hour specified with AM or PM flag.')
+        elif not 0 <= hour <= 12:
+            # If AM/PM is found, it's a 12 hour clock, so raise
+            # an error for invalid range
+            if fuzzy:
+                val_is_ampm = False
+            else:
+                raise ValueError('Invalid hour specified for 12-hour clock.')
+
+        return val_is_ampm
+
+    def _adjust_ampm(self, hour, ampm):
+        if hour < 12 and ampm == 1:
+            hour += 12
+        elif hour == 12 and ampm == 0:
+            hour = 0
+        return hour
+
+    def _parse_min_sec(self, value):
+        # TODO: Every usage of this function sets res.second to the return
+        # value. Are there any cases where second will be returned as None and
+        # we *don't* want to set res.second = None?
+        minute = int(value)
+        second = None
+
+        sec_remainder = value % 1
+        if sec_remainder:
+            second = int(60 * sec_remainder)
+        return (minute, second)
+
+    def _parse_hms(self, idx, tokens, info, hms_idx):
+        # TODO: Is this going to admit a lot of false-positives for when we
+        # just happen to have digits and "h", "m" or "s" characters in non-date
+        # text?  I guess hex hashes won't have that problem, but there's plenty
+        # of random junk out there.
+        if hms_idx is None:
+            hms = None
+            new_idx = idx
+        elif hms_idx > idx:
+            hms = info.hms(tokens[hms_idx])
+            new_idx = hms_idx
+        else:
+            # Looking backwards, increment one.
+            hms = info.hms(tokens[hms_idx]) + 1
+            new_idx = idx
+
+        return (new_idx, hms)
+
+    # ------------------------------------------------------------------
+    # Handling for individual tokens.  These are kept as methods instead
+    #  of functions for the sake of customizability via subclassing.
+
+    def _parsems(self, value):
+        """Parse a I[.F] seconds value into (seconds, microseconds)."""
+        if "." not in value:
+            return int(value), 0
+        else:
+            i, f = value.split(".")
+            return int(i), int(f.ljust(6, "0")[:6])
+
+    def _to_decimal(self, val):
+        try:
+            decimal_value = Decimal(val)
+            # See GH 662, edge case, infinite value should not be converted
+            #  via `_to_decimal`
+            if not decimal_value.is_finite():
+                raise ValueError("Converted decimal value is infinite or NaN")
+        except Exception as e:
+            msg = "Could not convert %s to decimal" % val
+            six.raise_from(ValueError(msg), e)
+        else:
+            return decimal_value
+
+    # ------------------------------------------------------------------
+    # Post-Parsing construction of datetime output.  These are kept as
+    #  methods instead of functions for the sake of customizability via
+    #  subclassing.
+
+    def _build_tzinfo(self, tzinfos, tzname, tzoffset):
+        if callable(tzinfos):
+            tzdata = tzinfos(tzname, tzoffset)
+        else:
+            tzdata = tzinfos.get(tzname)
+        # handle case where tzinfo is paased an options that returns None
+        # eg tzinfos = {'BRST' : None}
+        if isinstance(tzdata, datetime.tzinfo) or tzdata is None:
+            tzinfo = tzdata
+        elif isinstance(tzdata, text_type):
+            tzinfo = tz.tzstr(tzdata)
+        elif isinstance(tzdata, integer_types):
+            tzinfo = tz.tzoffset(tzname, tzdata)
+        else:
+            raise TypeError("Offset must be tzinfo subclass, tz string, "
+                            "or int offset.")
+        return tzinfo
+
+    def _build_tzaware(self, naive, res, tzinfos):
+        if (callable(tzinfos) or (tzinfos and res.tzname in tzinfos)):
+            tzinfo = self._build_tzinfo(tzinfos, res.tzname, res.tzoffset)
+            aware = naive.replace(tzinfo=tzinfo)
+            aware = self._assign_tzname(aware, res.tzname)
+
+        elif res.tzname and res.tzname in time.tzname:
+            aware = naive.replace(tzinfo=tz.tzlocal())
+
+            # Handle ambiguous local datetime
+            aware = self._assign_tzname(aware, res.tzname)
+
+            # This is mostly relevant for winter GMT zones parsed in the UK
+            if (aware.tzname() != res.tzname and
+                    res.tzname in self.info.UTCZONE):
+                aware = aware.replace(tzinfo=tz.UTC)
+
+        elif res.tzoffset == 0:
+            aware = naive.replace(tzinfo=tz.UTC)
+
+        elif res.tzoffset:
+            aware = naive.replace(tzinfo=tz.tzoffset(res.tzname, res.tzoffset))
+
+        elif not res.tzname and not res.tzoffset:
+            # i.e. no timezone information was found.
+            aware = naive
+
+        elif res.tzname:
+            # tz-like string was parsed but we don't know what to do
+            # with it
+            warnings.warn("tzname {tzname} identified but not understood.  "
+                          "Pass `tzinfos` argument in order to correctly "
+                          "return a timezone-aware datetime.  In a future "
+                          "version, this will raise an "
+                          "exception.".format(tzname=res.tzname),
+                          category=UnknownTimezoneWarning)
+            aware = naive
+
+        return aware
+
+    def _build_naive(self, res, default):
+        repl = {}
+        for attr in ("year", "month", "day", "hour",
+                     "minute", "second", "microsecond"):
+            value = getattr(res, attr)
+            if value is not None:
+                repl[attr] = value
+
+        if 'day' not in repl:
+            # If the default day exceeds the last day of the month, fall back
+            # to the end of the month.
+            cyear = default.year if res.year is None else res.year
+            cmonth = default.month if res.month is None else res.month
+            cday = default.day if res.day is None else res.day
+
+            if cday > monthrange(cyear, cmonth)[1]:
+                repl['day'] = monthrange(cyear, cmonth)[1]
+
+        naive = default.replace(**repl)
+
+        if res.weekday is not None and not res.day:
+            naive = naive + relativedelta.relativedelta(weekday=res.weekday)
+
+        return naive
+
+    def _assign_tzname(self, dt, tzname):
+        if dt.tzname() != tzname:
+            new_dt = tz.enfold(dt, fold=1)
+            if new_dt.tzname() == tzname:
+                return new_dt
+
+        return dt
+
+    def _recombine_skipped(self, tokens, skipped_idxs):
+        """
+        >>> tokens = ["foo", " ", "bar", " ", "19June2000", "baz"]
+        >>> skipped_idxs = [0, 1, 2, 5]
+        >>> _recombine_skipped(tokens, skipped_idxs)
+        ["foo bar", "baz"]
+        """
+        skipped_tokens = []
+        for i, idx in enumerate(sorted(skipped_idxs)):
+            if i > 0 and idx - 1 == skipped_idxs[i - 1]:
+                skipped_tokens[-1] = skipped_tokens[-1] + tokens[idx]
+            else:
+                skipped_tokens.append(tokens[idx])
+
+        return skipped_tokens
+
+
+DEFAULTPARSER = parser()
+
+
+def parse(timestr, parserinfo=None, **kwargs):
+    """
+
+    Parse a string in one of the supported formats, using the
+    ``parserinfo`` parameters.
+
+    :param timestr:
+        A string containing a date/time stamp.
+
+    :param parserinfo:
+        A :class:`parserinfo` object containing parameters for the parser.
+        If ``None``, the default arguments to the :class:`parserinfo`
+        constructor are used.
+
+    The ``**kwargs`` parameter takes the following keyword arguments:
+
+    :param default:
+        The default datetime object, if this is a datetime object and not
+        ``None``, elements specified in ``timestr`` replace elements in the
+        default object.
+
+    :param ignoretz:
+        If set ``True``, time zones in parsed strings are ignored and a naive
+        :class:`datetime` object is returned.
+
+    :param tzinfos:
+        Additional time zone names / aliases which may be present in the
+        string. This argument maps time zone names (and optionally offsets
+        from those time zones) to time zones. This parameter can be a
+        dictionary with timezone aliases mapping time zone names to time
+        zones or a function taking two parameters (``tzname`` and
+        ``tzoffset``) and returning a time zone.
+
+        The timezones to which the names are mapped can be an integer
+        offset from UTC in seconds or a :class:`tzinfo` object.
+
+        .. doctest::
+           :options: +NORMALIZE_WHITESPACE
+
+            >>> from dateutil.parser import parse
+            >>> from dateutil.tz import gettz
+            >>> tzinfos = {"BRST": -7200, "CST": gettz("America/Chicago")}
+            >>> parse("2012-01-19 17:21:00 BRST", tzinfos=tzinfos)
+            datetime.datetime(2012, 1, 19, 17, 21, tzinfo=tzoffset(u'BRST', -7200))
+            >>> parse("2012-01-19 17:21:00 CST", tzinfos=tzinfos)
+            datetime.datetime(2012, 1, 19, 17, 21,
+                              tzinfo=tzfile('/usr/share/zoneinfo/America/Chicago'))
+
+        This parameter is ignored if ``ignoretz`` is set.
+
+    :param dayfirst:
+        Whether to interpret the first value in an ambiguous 3-integer date
+        (e.g. 01/05/09) as the day (``True``) or month (``False``). If
+        ``yearfirst`` is set to ``True``, this distinguishes between YDM and
+        YMD. If set to ``None``, this value is retrieved from the current
+        :class:`parserinfo` object (which itself defaults to ``False``).
+
+    :param yearfirst:
+        Whether to interpret the first value in an ambiguous 3-integer date
+        (e.g. 01/05/09) as the year. If ``True``, the first number is taken to
+        be the year, otherwise the last number is taken to be the year. If
+        this is set to ``None``, the value is retrieved from the current
+        :class:`parserinfo` object (which itself defaults to ``False``).
+
+    :param fuzzy:
+        Whether to allow fuzzy parsing, allowing for string like "Today is
+        January 1, 2047 at 8:21:00AM".
+
+    :param fuzzy_with_tokens:
+        If ``True``, ``fuzzy`` is automatically set to True, and the parser
+        will return a tuple where the first element is the parsed
+        :class:`datetime.datetime` datetimestamp and the second element is
+        a tuple containing the portions of the string which were ignored:
+
+        .. doctest::
+
+            >>> from dateutil.parser import parse
+            >>> parse("Today is January 1, 2047 at 8:21:00AM", fuzzy_with_tokens=True)
+            (datetime.datetime(2047, 1, 1, 8, 21), (u'Today is ', u' ', u'at '))
+
+    :return:
+        Returns a :class:`datetime.datetime` object or, if the
+        ``fuzzy_with_tokens`` option is ``True``, returns a tuple, the
+        first element being a :class:`datetime.datetime` object, the second
+        a tuple containing the fuzzy tokens.
+
+    :raises ParserError:
+        Raised for invalid or unknown string formats, if the provided
+        :class:`tzinfo` is not in a valid format, or if an invalid date would
+        be created.
+
+    :raises OverflowError:
+        Raised if the parsed date exceeds the largest valid C integer on
+        your system.
+    """
+    if parserinfo:
+        return parser(parserinfo).parse(timestr, **kwargs)
+    else:
+        return DEFAULTPARSER.parse(timestr, **kwargs)
+
+
+class _tzparser(object):
+
+    class _result(_resultbase):
+
+        __slots__ = ["stdabbr", "stdoffset", "dstabbr", "dstoffset",
+                     "start", "end"]
+
+        class _attr(_resultbase):
+            __slots__ = ["month", "week", "weekday",
+                         "yday", "jyday", "day", "time"]
+
+        def __repr__(self):
+            return self._repr("")
+
+        def __init__(self):
+            _resultbase.__init__(self)
+            self.start = self._attr()
+            self.end = self._attr()
+
+    def parse(self, tzstr):
+        res = self._result()
+        l = [x for x in re.split(r'([,:.]|[a-zA-Z]+|[0-9]+)',tzstr) if x]
+        used_idxs = list()
+        try:
+
+            len_l = len(l)
+
+            i = 0
+            while i < len_l:
+                # BRST+3[BRDT[+2]]
+                j = i
+                while j < len_l and not [x for x in l[j]
+                                         if x in "0123456789:,-+"]:
+                    j += 1
+                if j != i:
+                    if not res.stdabbr:
+                        offattr = "stdoffset"
+                        res.stdabbr = "".join(l[i:j])
+                    else:
+                        offattr = "dstoffset"
+                        res.dstabbr = "".join(l[i:j])
+
+                    for ii in range(j):
+                        used_idxs.append(ii)
+                    i = j
+                    if (i < len_l and (l[i] in ('+', '-') or l[i][0] in
+                                       "0123456789")):
+                        if l[i] in ('+', '-'):
+                            # Yes, that's right.  See the TZ variable
+                            # documentation.
+                            signal = (1, -1)[l[i] == '+']
+                            used_idxs.append(i)
+                            i += 1
+                        else:
+                            signal = -1
+                        len_li = len(l[i])
+                        if len_li == 4:
+                            # -0300
+                            setattr(res, offattr, (int(l[i][:2]) * 3600 +
+                                                   int(l[i][2:]) * 60) * signal)
+                        elif i + 1 < len_l and l[i + 1] == ':':
+                            # -03:00
+                            setattr(res, offattr,
+                                    (int(l[i]) * 3600 +
+                                     int(l[i + 2]) * 60) * signal)
+                            used_idxs.append(i)
+                            i += 2
+                        elif len_li <= 2:
+                            # -[0]3
+                            setattr(res, offattr,
+                                    int(l[i][:2]) * 3600 * signal)
+                        else:
+                            return None
+                        used_idxs.append(i)
+                        i += 1
+                    if res.dstabbr:
+                        break
+                else:
+                    break
+
+
+            if i < len_l:
+                for j in range(i, len_l):
+                    if l[j] == ';':
+                        l[j] = ','
+
+                assert l[i] == ','
+
+                i += 1
+
+            if i >= len_l:
+                pass
+            elif (8 <= l.count(',') <= 9 and
+                  not [y for x in l[i:] if x != ','
+                       for y in x if y not in "0123456789+-"]):
+                # GMT0BST,3,0,30,3600,10,0,26,7200[,3600]
+                for x in (res.start, res.end):
+                    x.month = int(l[i])
+                    used_idxs.append(i)
+                    i += 2
+                    if l[i] == '-':
+                        value = int(l[i + 1]) * -1
+                        used_idxs.append(i)
+                        i += 1
+                    else:
+                        value = int(l[i])
+                    used_idxs.append(i)
+                    i += 2
+                    if value:
+                        x.week = value
+                        x.weekday = (int(l[i]) - 1) % 7
+                    else:
+                        x.day = int(l[i])
+                    used_idxs.append(i)
+                    i += 2
+                    x.time = int(l[i])
+                    used_idxs.append(i)
+                    i += 2
+                if i < len_l:
+                    if l[i] in ('-', '+'):
+                        signal = (-1, 1)[l[i] == "+"]
+                        used_idxs.append(i)
+                        i += 1
+                    else:
+                        signal = 1
+                    used_idxs.append(i)
+                    res.dstoffset = (res.stdoffset + int(l[i]) * signal)
+
+                # This was a made-up format that is not in normal use
+                warn(('Parsed time zone "%s"' % tzstr) +
+                     'is in a non-standard dateutil-specific format, which ' +
+                     'is now deprecated; support for parsing this format ' +
+                     'will be removed in future versions. It is recommended ' +
+                     'that you switch to a standard format like the GNU ' +
+                     'TZ variable format.', tz.DeprecatedTzFormatWarning)
+            elif (l.count(',') == 2 and l[i:].count('/') <= 2 and
+                  not [y for x in l[i:] if x not in (',', '/', 'J', 'M',
+                                                     '.', '-', ':')
+                       for y in x if y not in "0123456789"]):
+                for x in (res.start, res.end):
+                    if l[i] == 'J':
+                        # non-leap year day (1 based)
+                        used_idxs.append(i)
+                        i += 1
+                        x.jyday = int(l[i])
+                    elif l[i] == 'M':
+                        # month[-.]week[-.]weekday
+                        used_idxs.append(i)
+                        i += 1
+                        x.month = int(l[i])
+                        used_idxs.append(i)
+                        i += 1
+                        assert l[i] in ('-', '.')
+                        used_idxs.append(i)
+                        i += 1
+                        x.week = int(l[i])
+                        if x.week == 5:
+                            x.week = -1
+                        used_idxs.append(i)
+                        i += 1
+                        assert l[i] in ('-', '.')
+                        used_idxs.append(i)
+                        i += 1
+                        x.weekday = (int(l[i]) - 1) % 7
+                    else:
+                        # year day (zero based)
+                        x.yday = int(l[i]) + 1
+
+                    used_idxs.append(i)
+                    i += 1
+
+                    if i < len_l and l[i] == '/':
+                        used_idxs.append(i)
+                        i += 1
+                        # start time
+                        len_li = len(l[i])
+                        if len_li == 4:
+                            # -0300
+                            x.time = (int(l[i][:2]) * 3600 +
+                                      int(l[i][2:]) * 60)
+                        elif i + 1 < len_l and l[i + 1] == ':':
+                            # -03:00
+                            x.time = int(l[i]) * 3600 + int(l[i + 2]) * 60
+                            used_idxs.append(i)
+                            i += 2
+                            if i + 1 < len_l and l[i + 1] == ':':
+                                used_idxs.append(i)
+                                i += 2
+                                x.time += int(l[i])
+                        elif len_li <= 2:
+                            # -[0]3
+                            x.time = (int(l[i][:2]) * 3600)
+                        else:
+                            return None
+                        used_idxs.append(i)
+                        i += 1
+
+                    assert i == len_l or l[i] == ','
+
+                    i += 1
+
+                assert i >= len_l
+
+        except (IndexError, ValueError, AssertionError):
+            return None
+
+        unused_idxs = set(range(len_l)).difference(used_idxs)
+        res.any_unused_tokens = not {l[n] for n in unused_idxs}.issubset({",",":"})
+        return res
+
+
+DEFAULTTZPARSER = _tzparser()
+
+
+def _parsetz(tzstr):
+    return DEFAULTTZPARSER.parse(tzstr)
+
+
+class ParserError(ValueError):
+    """Exception subclass used for any failure to parse a datetime string.
+
+    This is a subclass of :py:exc:`ValueError`, and should be raised any time
+    earlier versions of ``dateutil`` would have raised ``ValueError``.
+
+    .. versionadded:: 2.8.1
+    """
+    def __str__(self):
+        try:
+            return self.args[0] % self.args[1:]
+        except (TypeError, IndexError):
+            return super(ParserError, self).__str__()
+
+    def __repr__(self):
+        args = ", ".join("'%s'" % arg for arg in self.args)
+        return "%s(%s)" % (self.__class__.__name__, args)
+
+
+class UnknownTimezoneWarning(RuntimeWarning):
+    """Raised when the parser finds a timezone it cannot parse into a tzinfo.
+
+    .. versionadded:: 2.7.0
+    """
+# vim:ts=4:sw=4:et
diff --git a/crates/hm-dsl-engine/vendor/dateutil/parser/isoparser.py b/crates/hm-dsl-engine/vendor/dateutil/parser/isoparser.py
new file mode 100644
index 00000000..7060087d
--- /dev/null
+++ b/crates/hm-dsl-engine/vendor/dateutil/parser/isoparser.py
@@ -0,0 +1,416 @@
+# -*- coding: utf-8 -*-
+"""
+This module offers a parser for ISO-8601 strings
+
+It is intended to support all valid date, time and datetime formats per the
+ISO-8601 specification.
+
+..versionadded:: 2.7.0
+"""
+from datetime import datetime, timedelta, time, date
+import calendar
+from dateutil import tz
+
+from functools import wraps
+
+import re
+import six
+
+__all__ = ["isoparse", "isoparser"]
+
+
+def _takes_ascii(f):
+    @wraps(f)
+    def func(self, str_in, *args, **kwargs):
+        # If it's a stream, read the whole thing
+        str_in = getattr(str_in, 'read', lambda: str_in)()
+
+        # If it's unicode, turn it into bytes, since ISO-8601 only covers ASCII
+        if isinstance(str_in, six.text_type):
+            # ASCII is the same in UTF-8
+            try:
+                str_in = str_in.encode('ascii')
+            except UnicodeEncodeError as e:
+                msg = 'ISO-8601 strings should contain only ASCII characters'
+                six.raise_from(ValueError(msg), e)
+
+        return f(self, str_in, *args, **kwargs)
+
+    return func
+
+
+class isoparser(object):
+    def __init__(self, sep=None):
+        """
+        :param sep:
+            A single character that separates date and time portions. If
+            ``None``, the parser will accept any single character.
+            For strict ISO-8601 adherence, pass ``'T'``.
+        """
+        if sep is not None:
+            if (len(sep) != 1 or ord(sep) >= 128 or sep in '0123456789'):
+                raise ValueError('Separator must be a single, non-numeric ' +
+                                 'ASCII character')
+
+            sep = sep.encode('ascii')
+
+        self._sep = sep
+
+    @_takes_ascii
+    def isoparse(self, dt_str):
+        """
+        Parse an ISO-8601 datetime string into a :class:`datetime.datetime`.
+
+        An ISO-8601 datetime string consists of a date portion, followed
+        optionally by a time portion - the date and time portions are separated
+        by a single character separator, which is ``T`` in the official
+        standard. Incomplete date formats (such as ``YYYY-MM``) may *not* be
+        combined with a time portion.
+
+        Supported date formats are:
+
+        Common:
+
+        - ``YYYY``
+        - ``YYYY-MM``
+        - ``YYYY-MM-DD`` or ``YYYYMMDD``
+
+        Uncommon:
+
+        - ``YYYY-Www`` or ``YYYYWww`` - ISO week (day defaults to 0)
+        - ``YYYY-Www-D`` or ``YYYYWwwD`` - ISO week and day
+
+        The ISO week and day numbering follows the same logic as
+        :func:`datetime.date.isocalendar`.
+
+        Supported time formats are:
+
+        - ``hh``
+        - ``hh:mm`` or ``hhmm``
+        - ``hh:mm:ss`` or ``hhmmss``
+        - ``hh:mm:ss.ssssss`` (Up to 6 sub-second digits)
+
+        Midnight is a special case for `hh`, as the standard supports both
+        00:00 and 24:00 as a representation. The decimal separator can be
+        either a dot or a comma.
+
+
+        .. caution::
+
+            Support for fractional components other than seconds is part of the
+            ISO-8601 standard, but is not currently implemented in this parser.
+
+        Supported time zone offset formats are:
+
+        - `Z` (UTC)
+        - `±HH:MM`
+        - `±HHMM`
+        - `±HH`
+
+        Offsets will be represented as :class:`dateutil.tz.tzoffset` objects,
+        with the exception of UTC, which will be represented as
+        :class:`dateutil.tz.tzutc`. Time zone offsets equivalent to UTC (such
+        as `+00:00`) will also be represented as :class:`dateutil.tz.tzutc`.
+
+        :param dt_str:
+            A string or stream containing only an ISO-8601 datetime string
+
+        :return:
+            Returns a :class:`datetime.datetime` representing the string.
+            Unspecified components default to their lowest value.
+
+        .. warning::
+
+            As of version 2.7.0, the strictness of the parser should not be
+            considered a stable part of the contract. Any valid ISO-8601 string
+            that parses correctly with the default settings will continue to
+            parse correctly in future versions, but invalid strings that
+            currently fail (e.g. ``2017-01-01T00:00+00:00:00``) are not
+            guaranteed to continue failing in future versions if they encode
+            a valid date.
+
+        .. versionadded:: 2.7.0
+        """
+        components, pos = self._parse_isodate(dt_str)
+
+        if len(dt_str) > pos:
+            if self._sep is None or dt_str[pos:pos + 1] == self._sep:
+                components += self._parse_isotime(dt_str[pos + 1:])
+            else:
+                raise ValueError('String contains unknown ISO components')
+
+        if len(components) > 3 and components[3] == 24:
+            components[3] = 0
+            return datetime(*components) + timedelta(days=1)
+
+        return datetime(*components)
+
+    @_takes_ascii
+    def parse_isodate(self, datestr):
+        """
+        Parse the date portion of an ISO string.
+
+        :param datestr:
+            The string portion of an ISO string, without a separator
+
+        :return:
+            Returns a :class:`datetime.date` object
+        """
+        components, pos = self._parse_isodate(datestr)
+        if pos < len(datestr):
+            raise ValueError('String contains unknown ISO ' +
+                             'components: {!r}'.format(datestr.decode('ascii')))
+        return date(*components)
+
+    @_takes_ascii
+    def parse_isotime(self, timestr):
+        """
+        Parse the time portion of an ISO string.
+
+        :param timestr:
+            The time portion of an ISO string, without a separator
+
+        :return:
+            Returns a :class:`datetime.time` object
+        """
+        components = self._parse_isotime(timestr)
+        if components[0] == 24:
+            components[0] = 0
+        return time(*components)
+
+    @_takes_ascii
+    def parse_tzstr(self, tzstr, zero_as_utc=True):
+        """
+        Parse a valid ISO time zone string.
+
+        See :func:`isoparser.isoparse` for details on supported formats.
+
+        :param tzstr:
+            A string representing an ISO time zone offset
+
+        :param zero_as_utc:
+            Whether to return :class:`dateutil.tz.tzutc` for zero-offset zones
+
+        :return:
+            Returns :class:`dateutil.tz.tzoffset` for offsets and
+            :class:`dateutil.tz.tzutc` for ``Z`` and (if ``zero_as_utc`` is
+            specified) offsets equivalent to UTC.
+        """
+        return self._parse_tzstr(tzstr, zero_as_utc=zero_as_utc)
+
+    # Constants
+    _DATE_SEP = b'-'
+    _TIME_SEP = b':'
+    _FRACTION_REGEX = re.compile(b'[\\.,]([0-9]+)')
+
+    def _parse_isodate(self, dt_str):
+        try:
+            return self._parse_isodate_common(dt_str)
+        except ValueError:
+            return self._parse_isodate_uncommon(dt_str)
+
+    def _parse_isodate_common(self, dt_str):
+        len_str = len(dt_str)
+        components = [1, 1, 1]
+
+        if len_str < 4:
+            raise ValueError('ISO string too short')
+
+        # Year
+        components[0] = int(dt_str[0:4])
+        pos = 4
+        if pos >= len_str:
+            return components, pos
+
+        has_sep = dt_str[pos:pos + 1] == self._DATE_SEP
+        if has_sep:
+            pos += 1
+
+        # Month
+        if len_str - pos < 2:
+            raise ValueError('Invalid common month')
+
+        components[1] = int(dt_str[pos:pos + 2])
+        pos += 2
+
+        if pos >= len_str:
+            if has_sep:
+                return components, pos
+            else:
+                raise ValueError('Invalid ISO format')
+
+        if has_sep:
+            if dt_str[pos:pos + 1] != self._DATE_SEP:
+                raise ValueError('Invalid separator in ISO string')
+            pos += 1
+
+        # Day
+        if len_str - pos < 2:
+            raise ValueError('Invalid common day')
+        components[2] = int(dt_str[pos:pos + 2])
+        return components, pos + 2
+
+    def _parse_isodate_uncommon(self, dt_str):
+        if len(dt_str) < 4:
+            raise ValueError('ISO string too short')
+
+        # All ISO formats start with the year
+        year = int(dt_str[0:4])
+
+        has_sep = dt_str[4:5] == self._DATE_SEP
+
+        pos = 4 + has_sep       # Skip '-' if it's there
+        if dt_str[pos:pos + 1] == b'W':
+            # YYYY-?Www-?D?
+            pos += 1
+            weekno = int(dt_str[pos:pos + 2])
+            pos += 2
+
+            dayno = 1
+            if len(dt_str) > pos:
+                if (dt_str[pos:pos + 1] == self._DATE_SEP) != has_sep:
+                    raise ValueError('Inconsistent use of dash separator')
+
+                pos += has_sep
+
+                dayno = int(dt_str[pos:pos + 1])
+                pos += 1
+
+            base_date = self._calculate_weekdate(year, weekno, dayno)
+        else:
+            # YYYYDDD or YYYY-DDD
+            if len(dt_str) - pos < 3:
+                raise ValueError('Invalid ordinal day')
+
+            ordinal_day = int(dt_str[pos:pos + 3])
+            pos += 3
+
+            if ordinal_day < 1 or ordinal_day > (365 + calendar.isleap(year)):
+                raise ValueError('Invalid ordinal day' +
+                                 ' {} for year {}'.format(ordinal_day, year))
+
+            base_date = date(year, 1, 1) + timedelta(days=ordinal_day - 1)
+
+        components = [base_date.year, base_date.month, base_date.day]
+        return components, pos
+
+    def _calculate_weekdate(self, year, week, day):
+        """
+        Calculate the day of corresponding to the ISO year-week-day calendar.
+
+        This function is effectively the inverse of
+        :func:`datetime.date.isocalendar`.
+
+        :param year:
+            The year in the ISO calendar
+
+        :param week:
+            The week in the ISO calendar - range is [1, 53]
+
+        :param day:
+            The day in the ISO calendar - range is [1 (MON), 7 (SUN)]
+
+        :return:
+            Returns a :class:`datetime.date`
+        """
+        if not 0 < week < 54:
+            raise ValueError('Invalid week: {}'.format(week))
+
+        if not 0 < day < 8:     # Range is 1-7
+            raise ValueError('Invalid weekday: {}'.format(day))
+
+        # Get week 1 for the specific year:
+        jan_4 = date(year, 1, 4)   # Week 1 always has January 4th in it
+        week_1 = jan_4 - timedelta(days=jan_4.isocalendar()[2] - 1)
+
+        # Now add the specific number of weeks and days to get what we want
+        week_offset = (week - 1) * 7 + (day - 1)
+        return week_1 + timedelta(days=week_offset)
+
+    def _parse_isotime(self, timestr):
+        len_str = len(timestr)
+        components = [0, 0, 0, 0, None]
+        pos = 0
+        comp = -1
+
+        if len_str < 2:
+            raise ValueError('ISO time too short')
+
+        has_sep = False
+
+        while pos < len_str and comp < 5:
+            comp += 1
+
+            if timestr[pos:pos + 1] in b'-+Zz':
+                # Detect time zone boundary
+                components[-1] = self._parse_tzstr(timestr[pos:])
+                pos = len_str
+                break
+
+            if comp == 1 and timestr[pos:pos+1] == self._TIME_SEP:
+                has_sep = True
+                pos += 1
+            elif comp == 2 and has_sep:
+                if timestr[pos:pos+1] != self._TIME_SEP:
+                    raise ValueError('Inconsistent use of colon separator')
+                pos += 1
+
+            if comp < 3:
+                # Hour, minute, second
+                components[comp] = int(timestr[pos:pos + 2])
+                pos += 2
+
+            if comp == 3:
+                # Fraction of a second
+                frac = self._FRACTION_REGEX.match(timestr[pos:])
+                if not frac:
+                    continue
+
+                us_str = frac.group(1)[:6]  # Truncate to microseconds
+                components[comp] = int(us_str) * 10**(6 - len(us_str))
+                pos += len(frac.group())
+
+        if pos < len_str:
+            raise ValueError('Unused components in ISO string')
+
+        if components[0] == 24:
+            # Standard supports 00:00 and 24:00 as representations of midnight
+            if any(component != 0 for component in components[1:4]):
+                raise ValueError('Hour may only be 24 at 24:00:00.000')
+
+        return components
+
+    def _parse_tzstr(self, tzstr, zero_as_utc=True):
+        if tzstr == b'Z' or tzstr == b'z':
+            return tz.UTC
+
+        if len(tzstr) not in {3, 5, 6}:
+            raise ValueError('Time zone offset must be 1, 3, 5 or 6 characters')
+
+        if tzstr[0:1] == b'-':
+            mult = -1
+        elif tzstr[0:1] == b'+':
+            mult = 1
+        else:
+            raise ValueError('Time zone offset requires sign')
+
+        hours = int(tzstr[1:3])
+        if len(tzstr) == 3:
+            minutes = 0
+        else:
+            minutes = int(tzstr[(4 if tzstr[3:4] == self._TIME_SEP else 3):])
+
+        if zero_as_utc and hours == 0 and minutes == 0:
+            return tz.UTC
+        else:
+            if minutes > 59:
+                raise ValueError('Invalid minutes in time zone offset')
+
+            if hours > 23:
+                raise ValueError('Invalid hours in time zone offset')
+
+            return tz.tzoffset(None, mult * (hours * 60 + minutes) * 60)
+
+
+DEFAULT_ISOPARSER = isoparser()
+isoparse = DEFAULT_ISOPARSER.isoparse
diff --git a/crates/hm-dsl-engine/vendor/dateutil/relativedelta.py b/crates/hm-dsl-engine/vendor/dateutil/relativedelta.py
new file mode 100644
index 00000000..cd323a54
--- /dev/null
+++ b/crates/hm-dsl-engine/vendor/dateutil/relativedelta.py
@@ -0,0 +1,599 @@
+# -*- coding: utf-8 -*-
+import datetime
+import calendar
+
+import operator
+from math import copysign
+
+from six import integer_types
+from warnings import warn
+
+from ._common import weekday
+
+MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7))
+
+__all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"]
+
+
+class relativedelta(object):
+    """
+    The relativedelta type is designed to be applied to an existing datetime and
+    can replace specific components of that datetime, or represents an interval
+    of time.
+
+    It is based on the specification of the excellent work done by M.-A. Lemburg
+    in his
+    `mx.DateTime `_ extension.
+    However, notice that this type does *NOT* implement the same algorithm as
+    his work. Do *NOT* expect it to behave like mx.DateTime's counterpart.
+
+    There are two different ways to build a relativedelta instance. The
+    first one is passing it two date/datetime classes::
+
+        relativedelta(datetime1, datetime2)
+
+    The second one is passing it any number of the following keyword arguments::
+
+        relativedelta(arg1=x,arg2=y,arg3=z...)
+
+        year, month, day, hour, minute, second, microsecond:
+            Absolute information (argument is singular); adding or subtracting a
+            relativedelta with absolute information does not perform an arithmetic
+            operation, but rather REPLACES the corresponding value in the
+            original datetime with the value(s) in relativedelta.
+
+        years, months, weeks, days, hours, minutes, seconds, microseconds:
+            Relative information, may be negative (argument is plural); adding
+            or subtracting a relativedelta with relative information performs
+            the corresponding arithmetic operation on the original datetime value
+            with the information in the relativedelta.
+
+        weekday:
+            One of the weekday instances (MO, TU, etc) available in the
+            relativedelta module. These instances may receive a parameter N,
+            specifying the Nth weekday, which could be positive or negative
+            (like MO(+1) or MO(-2)). Not specifying it is the same as specifying
+            +1. You can also use an integer, where 0=MO. This argument is always
+            relative e.g. if the calculated date is already Monday, using MO(1)
+            or MO(-1) won't change the day. To effectively make it absolute, use
+            it in combination with the day argument (e.g. day=1, MO(1) for first
+            Monday of the month).
+
+        leapdays:
+            Will add given days to the date found, if year is a leap
+            year, and the date found is post 28 of february.
+
+        yearday, nlyearday:
+            Set the yearday or the non-leap year day (jump leap days).
+            These are converted to day/month/leapdays information.
+
+    There are relative and absolute forms of the keyword
+    arguments. The plural is relative, and the singular is
+    absolute. For each argument in the order below, the absolute form
+    is applied first (by setting each attribute to that value) and
+    then the relative form (by adding the value to the attribute).
+
+    The order of attributes considered when this relativedelta is
+    added to a datetime is:
+
+    1. Year
+    2. Month
+    3. Day
+    4. Hours
+    5. Minutes
+    6. Seconds
+    7. Microseconds
+
+    Finally, weekday is applied, using the rule described above.
+
+    For example
+
+    >>> from datetime import datetime
+    >>> from dateutil.relativedelta import relativedelta, MO
+    >>> dt = datetime(2018, 4, 9, 13, 37, 0)
+    >>> delta = relativedelta(hours=25, day=1, weekday=MO(1))
+    >>> dt + delta
+    datetime.datetime(2018, 4, 2, 14, 37)
+
+    First, the day is set to 1 (the first of the month), then 25 hours
+    are added, to get to the 2nd day and 14th hour, finally the
+    weekday is applied, but since the 2nd is already a Monday there is
+    no effect.
+
+    """
+
+    def __init__(self, dt1=None, dt2=None,
+                 years=0, months=0, days=0, leapdays=0, weeks=0,
+                 hours=0, minutes=0, seconds=0, microseconds=0,
+                 year=None, month=None, day=None, weekday=None,
+                 yearday=None, nlyearday=None,
+                 hour=None, minute=None, second=None, microsecond=None):
+
+        if dt1 and dt2:
+            # datetime is a subclass of date. So both must be date
+            if not (isinstance(dt1, datetime.date) and
+                    isinstance(dt2, datetime.date)):
+                raise TypeError("relativedelta only diffs datetime/date")
+
+            # We allow two dates, or two datetimes, so we coerce them to be
+            # of the same type
+            if (isinstance(dt1, datetime.datetime) !=
+                    isinstance(dt2, datetime.datetime)):
+                if not isinstance(dt1, datetime.datetime):
+                    dt1 = datetime.datetime.fromordinal(dt1.toordinal())
+                elif not isinstance(dt2, datetime.datetime):
+                    dt2 = datetime.datetime.fromordinal(dt2.toordinal())
+
+            self.years = 0
+            self.months = 0
+            self.days = 0
+            self.leapdays = 0
+            self.hours = 0
+            self.minutes = 0
+            self.seconds = 0
+            self.microseconds = 0
+            self.year = None
+            self.month = None
+            self.day = None
+            self.weekday = None
+            self.hour = None
+            self.minute = None
+            self.second = None
+            self.microsecond = None
+            self._has_time = 0
+
+            # Get year / month delta between the two
+            months = (dt1.year - dt2.year) * 12 + (dt1.month - dt2.month)
+            self._set_months(months)
+
+            # Remove the year/month delta so the timedelta is just well-defined
+            # time units (seconds, days and microseconds)
+            dtm = self.__radd__(dt2)
+
+            # If we've overshot our target, make an adjustment
+            if dt1 < dt2:
+                compare = operator.gt
+                increment = 1
+            else:
+                compare = operator.lt
+                increment = -1
+
+            while compare(dt1, dtm):
+                months += increment
+                self._set_months(months)
+                dtm = self.__radd__(dt2)
+
+            # Get the timedelta between the "months-adjusted" date and dt1
+            delta = dt1 - dtm
+            self.seconds = delta.seconds + delta.days * 86400
+            self.microseconds = delta.microseconds
+        else:
+            # Check for non-integer values in integer-only quantities
+            if any(x is not None and x != int(x) for x in (years, months)):
+                raise ValueError("Non-integer years and months are "
+                                 "ambiguous and not currently supported.")
+
+            # Relative information
+            self.years = int(years)
+            self.months = int(months)
+            self.days = days + weeks * 7
+            self.leapdays = leapdays
+            self.hours = hours
+            self.minutes = minutes
+            self.seconds = seconds
+            self.microseconds = microseconds
+
+            # Absolute information
+            self.year = year
+            self.month = month
+            self.day = day
+            self.hour = hour
+            self.minute = minute
+            self.second = second
+            self.microsecond = microsecond
+
+            if any(x is not None and int(x) != x
+                   for x in (year, month, day, hour,
+                             minute, second, microsecond)):
+                # For now we'll deprecate floats - later it'll be an error.
+                warn("Non-integer value passed as absolute information. " +
+                     "This is not a well-defined condition and will raise " +
+                     "errors in future versions.", DeprecationWarning)
+
+            if isinstance(weekday, integer_types):
+                self.weekday = weekdays[weekday]
+            else:
+                self.weekday = weekday
+
+            yday = 0
+            if nlyearday:
+                yday = nlyearday
+            elif yearday:
+                yday = yearday
+                if yearday > 59:
+                    self.leapdays = -1
+            if yday:
+                ydayidx = [31, 59, 90, 120, 151, 181, 212,
+                           243, 273, 304, 334, 366]
+                for idx, ydays in enumerate(ydayidx):
+                    if yday <= ydays:
+                        self.month = idx+1
+                        if idx == 0:
+                            self.day = yday
+                        else:
+                            self.day = yday-ydayidx[idx-1]
+                        break
+                else:
+                    raise ValueError("invalid year day (%d)" % yday)
+
+        self._fix()
+
+    def _fix(self):
+        if abs(self.microseconds) > 999999:
+            s = _sign(self.microseconds)
+            div, mod = divmod(self.microseconds * s, 1000000)
+            self.microseconds = mod * s
+            self.seconds += div * s
+        if abs(self.seconds) > 59:
+            s = _sign(self.seconds)
+            div, mod = divmod(self.seconds * s, 60)
+            self.seconds = mod * s
+            self.minutes += div * s
+        if abs(self.minutes) > 59:
+            s = _sign(self.minutes)
+            div, mod = divmod(self.minutes * s, 60)
+            self.minutes = mod * s
+            self.hours += div * s
+        if abs(self.hours) > 23:
+            s = _sign(self.hours)
+            div, mod = divmod(self.hours * s, 24)
+            self.hours = mod * s
+            self.days += div * s
+        if abs(self.months) > 11:
+            s = _sign(self.months)
+            div, mod = divmod(self.months * s, 12)
+            self.months = mod * s
+            self.years += div * s
+        if (self.hours or self.minutes or self.seconds or self.microseconds
+                or self.hour is not None or self.minute is not None or
+                self.second is not None or self.microsecond is not None):
+            self._has_time = 1
+        else:
+            self._has_time = 0
+
+    @property
+    def weeks(self):
+        return int(self.days / 7.0)
+
+    @weeks.setter
+    def weeks(self, value):
+        self.days = self.days - (self.weeks * 7) + value * 7
+
+    def _set_months(self, months):
+        self.months = months
+        if abs(self.months) > 11:
+            s = _sign(self.months)
+            div, mod = divmod(self.months * s, 12)
+            self.months = mod * s
+            self.years = div * s
+        else:
+            self.years = 0
+
+    def normalized(self):
+        """
+        Return a version of this object represented entirely using integer
+        values for the relative attributes.
+
+        >>> relativedelta(days=1.5, hours=2).normalized()
+        relativedelta(days=+1, hours=+14)
+
+        :return:
+            Returns a :class:`dateutil.relativedelta.relativedelta` object.
+        """
+        # Cascade remainders down (rounding each to roughly nearest microsecond)
+        days = int(self.days)
+
+        hours_f = round(self.hours + 24 * (self.days - days), 11)
+        hours = int(hours_f)
+
+        minutes_f = round(self.minutes + 60 * (hours_f - hours), 10)
+        minutes = int(minutes_f)
+
+        seconds_f = round(self.seconds + 60 * (minutes_f - minutes), 8)
+        seconds = int(seconds_f)
+
+        microseconds = round(self.microseconds + 1e6 * (seconds_f - seconds))
+
+        # Constructor carries overflow back up with call to _fix()
+        return self.__class__(years=self.years, months=self.months,
+                              days=days, hours=hours, minutes=minutes,
+                              seconds=seconds, microseconds=microseconds,
+                              leapdays=self.leapdays, year=self.year,
+                              month=self.month, day=self.day,
+                              weekday=self.weekday, hour=self.hour,
+                              minute=self.minute, second=self.second,
+                              microsecond=self.microsecond)
+
+    def __add__(self, other):
+        if isinstance(other, relativedelta):
+            return self.__class__(years=other.years + self.years,
+                                 months=other.months + self.months,
+                                 days=other.days + self.days,
+                                 hours=other.hours + self.hours,
+                                 minutes=other.minutes + self.minutes,
+                                 seconds=other.seconds + self.seconds,
+                                 microseconds=(other.microseconds +
+                                               self.microseconds),
+                                 leapdays=other.leapdays or self.leapdays,
+                                 year=(other.year if other.year is not None
+                                       else self.year),
+                                 month=(other.month if other.month is not None
+                                        else self.month),
+                                 day=(other.day if other.day is not None
+                                      else self.day),
+                                 weekday=(other.weekday if other.weekday is not None
+                                          else self.weekday),
+                                 hour=(other.hour if other.hour is not None
+                                       else self.hour),
+                                 minute=(other.minute if other.minute is not None
+                                         else self.minute),
+                                 second=(other.second if other.second is not None
+                                         else self.second),
+                                 microsecond=(other.microsecond if other.microsecond
+                                              is not None else
+                                              self.microsecond))
+        if isinstance(other, datetime.timedelta):
+            return self.__class__(years=self.years,
+                                  months=self.months,
+                                  days=self.days + other.days,
+                                  hours=self.hours,
+                                  minutes=self.minutes,
+                                  seconds=self.seconds + other.seconds,
+                                  microseconds=self.microseconds + other.microseconds,
+                                  leapdays=self.leapdays,
+                                  year=self.year,
+                                  month=self.month,
+                                  day=self.day,
+                                  weekday=self.weekday,
+                                  hour=self.hour,
+                                  minute=self.minute,
+                                  second=self.second,
+                                  microsecond=self.microsecond)
+        if not isinstance(other, datetime.date):
+            return NotImplemented
+        elif self._has_time and not isinstance(other, datetime.datetime):
+            other = datetime.datetime.fromordinal(other.toordinal())
+        year = (self.year or other.year)+self.years
+        month = self.month or other.month
+        if self.months:
+            assert 1 <= abs(self.months) <= 12
+            month += self.months
+            if month > 12:
+                year += 1
+                month -= 12
+            elif month < 1:
+                year -= 1
+                month += 12
+        day = min(calendar.monthrange(year, month)[1],
+                  self.day or other.day)
+        repl = {"year": year, "month": month, "day": day}
+        for attr in ["hour", "minute", "second", "microsecond"]:
+            value = getattr(self, attr)
+            if value is not None:
+                repl[attr] = value
+        days = self.days
+        if self.leapdays and month > 2 and calendar.isleap(year):
+            days += self.leapdays
+        ret = (other.replace(**repl)
+               + datetime.timedelta(days=days,
+                                    hours=self.hours,
+                                    minutes=self.minutes,
+                                    seconds=self.seconds,
+                                    microseconds=self.microseconds))
+        if self.weekday:
+            weekday, nth = self.weekday.weekday, self.weekday.n or 1
+            jumpdays = (abs(nth) - 1) * 7
+            if nth > 0:
+                jumpdays += (7 - ret.weekday() + weekday) % 7
+            else:
+                jumpdays += (ret.weekday() - weekday) % 7
+                jumpdays *= -1
+            ret += datetime.timedelta(days=jumpdays)
+        return ret
+
+    def __radd__(self, other):
+        return self.__add__(other)
+
+    def __rsub__(self, other):
+        return self.__neg__().__radd__(other)
+
+    def __sub__(self, other):
+        if not isinstance(other, relativedelta):
+            return NotImplemented   # In case the other object defines __rsub__
+        return self.__class__(years=self.years - other.years,
+                             months=self.months - other.months,
+                             days=self.days - other.days,
+                             hours=self.hours - other.hours,
+                             minutes=self.minutes - other.minutes,
+                             seconds=self.seconds - other.seconds,
+                             microseconds=self.microseconds - other.microseconds,
+                             leapdays=self.leapdays or other.leapdays,
+                             year=(self.year if self.year is not None
+                                   else other.year),
+                             month=(self.month if self.month is not None else
+                                    other.month),
+                             day=(self.day if self.day is not None else
+                                  other.day),
+                             weekday=(self.weekday if self.weekday is not None else
+                                      other.weekday),
+                             hour=(self.hour if self.hour is not None else
+                                   other.hour),
+                             minute=(self.minute if self.minute is not None else
+                                     other.minute),
+                             second=(self.second if self.second is not None else
+                                     other.second),
+                             microsecond=(self.microsecond if self.microsecond
+                                          is not None else
+                                          other.microsecond))
+
+    def __abs__(self):
+        return self.__class__(years=abs(self.years),
+                              months=abs(self.months),
+                              days=abs(self.days),
+                              hours=abs(self.hours),
+                              minutes=abs(self.minutes),
+                              seconds=abs(self.seconds),
+                              microseconds=abs(self.microseconds),
+                              leapdays=self.leapdays,
+                              year=self.year,
+                              month=self.month,
+                              day=self.day,
+                              weekday=self.weekday,
+                              hour=self.hour,
+                              minute=self.minute,
+                              second=self.second,
+                              microsecond=self.microsecond)
+
+    def __neg__(self):
+        return self.__class__(years=-self.years,
+                             months=-self.months,
+                             days=-self.days,
+                             hours=-self.hours,
+                             minutes=-self.minutes,
+                             seconds=-self.seconds,
+                             microseconds=-self.microseconds,
+                             leapdays=self.leapdays,
+                             year=self.year,
+                             month=self.month,
+                             day=self.day,
+                             weekday=self.weekday,
+                             hour=self.hour,
+                             minute=self.minute,
+                             second=self.second,
+                             microsecond=self.microsecond)
+
+    def __bool__(self):
+        return not (not self.years and
+                    not self.months and
+                    not self.days and
+                    not self.hours and
+                    not self.minutes and
+                    not self.seconds and
+                    not self.microseconds and
+                    not self.leapdays and
+                    self.year is None and
+                    self.month is None and
+                    self.day is None and
+                    self.weekday is None and
+                    self.hour is None and
+                    self.minute is None and
+                    self.second is None and
+                    self.microsecond is None)
+    # Compatibility with Python 2.x
+    __nonzero__ = __bool__
+
+    def __mul__(self, other):
+        try:
+            f = float(other)
+        except TypeError:
+            return NotImplemented
+
+        return self.__class__(years=int(self.years * f),
+                             months=int(self.months * f),
+                             days=int(self.days * f),
+                             hours=int(self.hours * f),
+                             minutes=int(self.minutes * f),
+                             seconds=int(self.seconds * f),
+                             microseconds=int(self.microseconds * f),
+                             leapdays=self.leapdays,
+                             year=self.year,
+                             month=self.month,
+                             day=self.day,
+                             weekday=self.weekday,
+                             hour=self.hour,
+                             minute=self.minute,
+                             second=self.second,
+                             microsecond=self.microsecond)
+
+    __rmul__ = __mul__
+
+    def __eq__(self, other):
+        if not isinstance(other, relativedelta):
+            return NotImplemented
+        if self.weekday or other.weekday:
+            if not self.weekday or not other.weekday:
+                return False
+            if self.weekday.weekday != other.weekday.weekday:
+                return False
+            n1, n2 = self.weekday.n, other.weekday.n
+            if n1 != n2 and not ((not n1 or n1 == 1) and (not n2 or n2 == 1)):
+                return False
+        return (self.years == other.years and
+                self.months == other.months and
+                self.days == other.days and
+                self.hours == other.hours and
+                self.minutes == other.minutes and
+                self.seconds == other.seconds and
+                self.microseconds == other.microseconds and
+                self.leapdays == other.leapdays and
+                self.year == other.year and
+                self.month == other.month and
+                self.day == other.day and
+                self.hour == other.hour and
+                self.minute == other.minute and
+                self.second == other.second and
+                self.microsecond == other.microsecond)
+
+    def __hash__(self):
+        return hash((
+            self.weekday,
+            self.years,
+            self.months,
+            self.days,
+            self.hours,
+            self.minutes,
+            self.seconds,
+            self.microseconds,
+            self.leapdays,
+            self.year,
+            self.month,
+            self.day,
+            self.hour,
+            self.minute,
+            self.second,
+            self.microsecond,
+        ))
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    def __div__(self, other):
+        try:
+            reciprocal = 1 / float(other)
+        except TypeError:
+            return NotImplemented
+
+        return self.__mul__(reciprocal)
+
+    __truediv__ = __div__
+
+    def __repr__(self):
+        l = []
+        for attr in ["years", "months", "days", "leapdays",
+                     "hours", "minutes", "seconds", "microseconds"]:
+            value = getattr(self, attr)
+            if value:
+                l.append("{attr}={value:+g}".format(attr=attr, value=value))
+        for attr in ["year", "month", "day", "weekday",
+                     "hour", "minute", "second", "microsecond"]:
+            value = getattr(self, attr)
+            if value is not None:
+                l.append("{attr}={value}".format(attr=attr, value=repr(value)))
+        return "{classname}({attrs})".format(classname=self.__class__.__name__,
+                                             attrs=", ".join(l))
+
+
+def _sign(x):
+    return int(copysign(1, x))
+
+# vim:ts=4:sw=4:et
diff --git a/crates/hm-dsl-engine/vendor/dateutil/rrule.py b/crates/hm-dsl-engine/vendor/dateutil/rrule.py
new file mode 100644
index 00000000..571a0d2b
--- /dev/null
+++ b/crates/hm-dsl-engine/vendor/dateutil/rrule.py
@@ -0,0 +1,1737 @@
+# -*- coding: utf-8 -*-
+"""
+The rrule module offers a small, complete, and very fast, implementation of
+the recurrence rules documented in the
+`iCalendar RFC `_,
+including support for caching of results.
+"""
+import calendar
+import datetime
+import heapq
+import itertools
+import re
+import sys
+from functools import wraps
+# For warning about deprecation of until and count
+from warnings import warn
+
+from six import advance_iterator, integer_types
+
+from six.moves import _thread, range
+
+from ._common import weekday as weekdaybase
+
+try:
+    from math import gcd
+except ImportError:
+    from fractions import gcd
+
+__all__ = ["rrule", "rruleset", "rrulestr",
+           "YEARLY", "MONTHLY", "WEEKLY", "DAILY",
+           "HOURLY", "MINUTELY", "SECONDLY",
+           "MO", "TU", "WE", "TH", "FR", "SA", "SU"]
+
+# Every mask is 7 days longer to handle cross-year weekly periods.
+M366MASK = tuple([1]*31+[2]*29+[3]*31+[4]*30+[5]*31+[6]*30 +
+                 [7]*31+[8]*31+[9]*30+[10]*31+[11]*30+[12]*31+[1]*7)
+M365MASK = list(M366MASK)
+M29, M30, M31 = list(range(1, 30)), list(range(1, 31)), list(range(1, 32))
+MDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7])
+MDAY365MASK = list(MDAY366MASK)
+M29, M30, M31 = list(range(-29, 0)), list(range(-30, 0)), list(range(-31, 0))
+NMDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7])
+NMDAY365MASK = list(NMDAY366MASK)
+M366RANGE = (0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366)
+M365RANGE = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365)
+WDAYMASK = [0, 1, 2, 3, 4, 5, 6]*55
+del M29, M30, M31, M365MASK[59], MDAY365MASK[59], NMDAY365MASK[31]
+MDAY365MASK = tuple(MDAY365MASK)
+M365MASK = tuple(M365MASK)
+
+FREQNAMES = ['YEARLY', 'MONTHLY', 'WEEKLY', 'DAILY', 'HOURLY', 'MINUTELY', 'SECONDLY']
+
+(YEARLY,
+ MONTHLY,
+ WEEKLY,
+ DAILY,
+ HOURLY,
+ MINUTELY,
+ SECONDLY) = list(range(7))
+
+# Imported on demand.
+easter = None
+parser = None
+
+
+class weekday(weekdaybase):
+    """
+    This version of weekday does not allow n = 0.
+    """
+    def __init__(self, wkday, n=None):
+        if n == 0:
+            raise ValueError("Can't create weekday with n==0")
+
+        super(weekday, self).__init__(wkday, n)
+
+
+MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7))
+
+
+def _invalidates_cache(f):
+    """
+    Decorator for rruleset methods which may invalidate the
+    cached length.
+    """
+    @wraps(f)
+    def inner_func(self, *args, **kwargs):
+        rv = f(self, *args, **kwargs)
+        self._invalidate_cache()
+        return rv
+
+    return inner_func
+
+
+class rrulebase(object):
+    def __init__(self, cache=False):
+        if cache:
+            self._cache = []
+            self._cache_lock = _thread.allocate_lock()
+            self._invalidate_cache()
+        else:
+            self._cache = None
+            self._cache_complete = False
+            self._len = None
+
+    def __iter__(self):
+        if self._cache_complete:
+            return iter(self._cache)
+        elif self._cache is None:
+            return self._iter()
+        else:
+            return self._iter_cached()
+
+    def _invalidate_cache(self):
+        if self._cache is not None:
+            self._cache = []
+            self._cache_complete = False
+            self._cache_gen = self._iter()
+
+            if self._cache_lock.locked():
+                self._cache_lock.release()
+
+        self._len = None
+
+    def _iter_cached(self):
+        i = 0
+        gen = self._cache_gen
+        cache = self._cache
+        acquire = self._cache_lock.acquire
+        release = self._cache_lock.release
+        while gen:
+            if i == len(cache):
+                acquire()
+                if self._cache_complete:
+                    break
+                try:
+                    for j in range(10):
+                        cache.append(advance_iterator(gen))
+                except StopIteration:
+                    self._cache_gen = gen = None
+                    self._cache_complete = True
+                    break
+                release()
+            yield cache[i]
+            i += 1
+        while i < self._len:
+            yield cache[i]
+            i += 1
+
+    def __getitem__(self, item):
+        if self._cache_complete:
+            return self._cache[item]
+        elif isinstance(item, slice):
+            if item.step and item.step < 0:
+                return list(iter(self))[item]
+            else:
+                return list(itertools.islice(self,
+                                             item.start or 0,
+                                             item.stop or sys.maxsize,
+                                             item.step or 1))
+        elif item >= 0:
+            gen = iter(self)
+            try:
+                for i in range(item+1):
+                    res = advance_iterator(gen)
+            except StopIteration:
+                raise IndexError
+            return res
+        else:
+            return list(iter(self))[item]
+
+    def __contains__(self, item):
+        if self._cache_complete:
+            return item in self._cache
+        else:
+            for i in self:
+                if i == item:
+                    return True
+                elif i > item:
+                    return False
+        return False
+
+    # __len__() introduces a large performance penalty.
+    def count(self):
+        """ Returns the number of recurrences in this set. It will have go
+            through the whole recurrence, if this hasn't been done before. """
+        if self._len is None:
+            for x in self:
+                pass
+        return self._len
+
+    def before(self, dt, inc=False):
+        """ Returns the last recurrence before the given datetime instance. The
+            inc keyword defines what happens if dt is an occurrence. With
+            inc=True, if dt itself is an occurrence, it will be returned. """
+        if self._cache_complete:
+            gen = self._cache
+        else:
+            gen = self
+        last = None
+        if inc:
+            for i in gen:
+                if i > dt:
+                    break
+                last = i
+        else:
+            for i in gen:
+                if i >= dt:
+                    break
+                last = i
+        return last
+
+    def after(self, dt, inc=False):
+        """ Returns the first recurrence after the given datetime instance. The
+            inc keyword defines what happens if dt is an occurrence. With
+            inc=True, if dt itself is an occurrence, it will be returned.  """
+        if self._cache_complete:
+            gen = self._cache
+        else:
+            gen = self
+        if inc:
+            for i in gen:
+                if i >= dt:
+                    return i
+        else:
+            for i in gen:
+                if i > dt:
+                    return i
+        return None
+
+    def xafter(self, dt, count=None, inc=False):
+        """
+        Generator which yields up to `count` recurrences after the given
+        datetime instance, equivalent to `after`.
+
+        :param dt:
+            The datetime at which to start generating recurrences.
+
+        :param count:
+            The maximum number of recurrences to generate. If `None` (default),
+            dates are generated until the recurrence rule is exhausted.
+
+        :param inc:
+            If `dt` is an instance of the rule and `inc` is `True`, it is
+            included in the output.
+
+        :yields: Yields a sequence of `datetime` objects.
+        """
+
+        if self._cache_complete:
+            gen = self._cache
+        else:
+            gen = self
+
+        # Select the comparison function
+        if inc:
+            comp = lambda dc, dtc: dc >= dtc
+        else:
+            comp = lambda dc, dtc: dc > dtc
+
+        # Generate dates
+        n = 0
+        for d in gen:
+            if comp(d, dt):
+                if count is not None:
+                    n += 1
+                    if n > count:
+                        break
+
+                yield d
+
+    def between(self, after, before, inc=False, count=1):
+        """ Returns all the occurrences of the rrule between after and before.
+        The inc keyword defines what happens if after and/or before are
+        themselves occurrences. With inc=True, they will be included in the
+        list, if they are found in the recurrence set. """
+        if self._cache_complete:
+            gen = self._cache
+        else:
+            gen = self
+        started = False
+        l = []
+        if inc:
+            for i in gen:
+                if i > before:
+                    break
+                elif not started:
+                    if i >= after:
+                        started = True
+                        l.append(i)
+                else:
+                    l.append(i)
+        else:
+            for i in gen:
+                if i >= before:
+                    break
+                elif not started:
+                    if i > after:
+                        started = True
+                        l.append(i)
+                else:
+                    l.append(i)
+        return l
+
+
+class rrule(rrulebase):
+    """
+    That's the base of the rrule operation. It accepts all the keywords
+    defined in the RFC as its constructor parameters (except byday,
+    which was renamed to byweekday) and more. The constructor prototype is::
+
+            rrule(freq)
+
+    Where freq must be one of YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY,
+    or SECONDLY.
+
+    .. note::
+        Per RFC section 3.3.10, recurrence instances falling on invalid dates
+        and times are ignored rather than coerced:
+
+            Recurrence rules may generate recurrence instances with an invalid
+            date (e.g., February 30) or nonexistent local time (e.g., 1:30 AM
+            on a day where the local time is moved forward by an hour at 1:00
+            AM).  Such recurrence instances MUST be ignored and MUST NOT be
+            counted as part of the recurrence set.
+
+        This can lead to possibly surprising behavior when, for example, the
+        start date occurs at the end of the month:
+
+        >>> from dateutil.rrule import rrule, MONTHLY
+        >>> from datetime import datetime
+        >>> start_date = datetime(2014, 12, 31)
+        >>> list(rrule(freq=MONTHLY, count=4, dtstart=start_date))
+        ... # doctest: +NORMALIZE_WHITESPACE
+        [datetime.datetime(2014, 12, 31, 0, 0),
+         datetime.datetime(2015, 1, 31, 0, 0),
+         datetime.datetime(2015, 3, 31, 0, 0),
+         datetime.datetime(2015, 5, 31, 0, 0)]
+
+    Additionally, it supports the following keyword arguments:
+
+    :param dtstart:
+        The recurrence start. Besides being the base for the recurrence,
+        missing parameters in the final recurrence instances will also be
+        extracted from this date. If not given, datetime.now() will be used
+        instead.
+    :param interval:
+        The interval between each freq iteration. For example, when using
+        YEARLY, an interval of 2 means once every two years, but with HOURLY,
+        it means once every two hours. The default interval is 1.
+    :param wkst:
+        The week start day. Must be one of the MO, TU, WE constants, or an
+        integer, specifying the first day of the week. This will affect
+        recurrences based on weekly periods. The default week start is got
+        from calendar.firstweekday(), and may be modified by
+        calendar.setfirstweekday().
+    :param count:
+        If given, this determines how many occurrences will be generated.
+
+        .. note::
+            As of version 2.5.0, the use of the keyword ``until`` in conjunction
+            with ``count`` is deprecated, to make sure ``dateutil`` is fully
+            compliant with `RFC-5545 Sec. 3.3.10 `_. Therefore, ``until`` and ``count``
+            **must not** occur in the same call to ``rrule``.
+    :param until:
+        If given, this must be a datetime instance specifying the upper-bound
+        limit of the recurrence. The last recurrence in the rule is the greatest
+        datetime that is less than or equal to the value specified in the
+        ``until`` parameter.
+
+        .. note::
+            As of version 2.5.0, the use of the keyword ``until`` in conjunction
+            with ``count`` is deprecated, to make sure ``dateutil`` is fully
+            compliant with `RFC-5545 Sec. 3.3.10 `_. Therefore, ``until`` and ``count``
+            **must not** occur in the same call to ``rrule``.
+    :param bysetpos:
+        If given, it must be either an integer, or a sequence of integers,
+        positive or negative. Each given integer will specify an occurrence
+        number, corresponding to the nth occurrence of the rule inside the
+        frequency period. For example, a bysetpos of -1 if combined with a
+        MONTHLY frequency, and a byweekday of (MO, TU, WE, TH, FR), will
+        result in the last work day of every month.
+    :param bymonth:
+        If given, it must be either an integer, or a sequence of integers,
+        meaning the months to apply the recurrence to.
+    :param bymonthday:
+        If given, it must be either an integer, or a sequence of integers,
+        meaning the month days to apply the recurrence to.
+    :param byyearday:
+        If given, it must be either an integer, or a sequence of integers,
+        meaning the year days to apply the recurrence to.
+    :param byeaster:
+        If given, it must be either an integer, or a sequence of integers,
+        positive or negative. Each integer will define an offset from the
+        Easter Sunday. Passing the offset 0 to byeaster will yield the Easter
+        Sunday itself. This is an extension to the RFC specification.
+    :param byweekno:
+        If given, it must be either an integer, or a sequence of integers,
+        meaning the week numbers to apply the recurrence to. Week numbers
+        have the meaning described in ISO8601, that is, the first week of
+        the year is that containing at least four days of the new year.
+    :param byweekday:
+        If given, it must be either an integer (0 == MO), a sequence of
+        integers, one of the weekday constants (MO, TU, etc), or a sequence
+        of these constants. When given, these variables will define the
+        weekdays where the recurrence will be applied. It's also possible to
+        use an argument n for the weekday instances, which will mean the nth
+        occurrence of this weekday in the period. For example, with MONTHLY,
+        or with YEARLY and BYMONTH, using FR(+1) in byweekday will specify the
+        first friday of the month where the recurrence happens. Notice that in
+        the RFC documentation, this is specified as BYDAY, but was renamed to
+        avoid the ambiguity of that keyword.
+    :param byhour:
+        If given, it must be either an integer, or a sequence of integers,
+        meaning the hours to apply the recurrence to.
+    :param byminute:
+        If given, it must be either an integer, or a sequence of integers,
+        meaning the minutes to apply the recurrence to.
+    :param bysecond:
+        If given, it must be either an integer, or a sequence of integers,
+        meaning the seconds to apply the recurrence to.
+    :param cache:
+        If given, it must be a boolean value specifying to enable or disable
+        caching of results. If you will use the same rrule instance multiple
+        times, enabling caching will improve the performance considerably.
+     """
+    def __init__(self, freq, dtstart=None,
+                 interval=1, wkst=None, count=None, until=None, bysetpos=None,
+                 bymonth=None, bymonthday=None, byyearday=None, byeaster=None,
+                 byweekno=None, byweekday=None,
+                 byhour=None, byminute=None, bysecond=None,
+                 cache=False):
+        super(rrule, self).__init__(cache)
+        global easter
+        if not dtstart:
+            if until and until.tzinfo:
+                dtstart = datetime.datetime.now(tz=until.tzinfo).replace(microsecond=0)
+            else:
+                dtstart = datetime.datetime.now().replace(microsecond=0)
+        elif not isinstance(dtstart, datetime.datetime):
+            dtstart = datetime.datetime.fromordinal(dtstart.toordinal())
+        else:
+            dtstart = dtstart.replace(microsecond=0)
+        self._dtstart = dtstart
+        self._tzinfo = dtstart.tzinfo
+        self._freq = freq
+        self._interval = interval
+        self._count = count
+
+        # Cache the original byxxx rules, if they are provided, as the _byxxx
+        # attributes do not necessarily map to the inputs, and this can be
+        # a problem in generating the strings. Only store things if they've
+        # been supplied (the string retrieval will just use .get())
+        self._original_rule = {}
+
+        if until and not isinstance(until, datetime.datetime):
+            until = datetime.datetime.fromordinal(until.toordinal())
+        self._until = until
+
+        if self._dtstart and self._until:
+            if (self._dtstart.tzinfo is not None) != (self._until.tzinfo is not None):
+                # According to RFC5545 Section 3.3.10:
+                # https://tools.ietf.org/html/rfc5545#section-3.3.10
+                #
+                # > If the "DTSTART" property is specified as a date with UTC
+                # > time or a date with local time and time zone reference,
+                # > then the UNTIL rule part MUST be specified as a date with
+                # > UTC time.
+                raise ValueError(
+                    'RRULE UNTIL values must be specified in UTC when DTSTART '
+                    'is timezone-aware'
+                )
+
+        if count is not None and until:
+            warn("Using both 'count' and 'until' is inconsistent with RFC 5545"
+                 " and has been deprecated in dateutil. Future versions will "
+                 "raise an error.", DeprecationWarning)
+
+        if wkst is None:
+            self._wkst = calendar.firstweekday()
+        elif isinstance(wkst, integer_types):
+            self._wkst = wkst
+        else:
+            self._wkst = wkst.weekday
+
+        if bysetpos is None:
+            self._bysetpos = None
+        elif isinstance(bysetpos, integer_types):
+            if bysetpos == 0 or not (-366 <= bysetpos <= 366):
+                raise ValueError("bysetpos must be between 1 and 366, "
+                                 "or between -366 and -1")
+            self._bysetpos = (bysetpos,)
+        else:
+            self._bysetpos = tuple(bysetpos)
+            for pos in self._bysetpos:
+                if pos == 0 or not (-366 <= pos <= 366):
+                    raise ValueError("bysetpos must be between 1 and 366, "
+                                     "or between -366 and -1")
+
+        if self._bysetpos:
+            self._original_rule['bysetpos'] = self._bysetpos
+
+        if (byweekno is None and byyearday is None and bymonthday is None and
+                byweekday is None and byeaster is None):
+            if freq == YEARLY:
+                if bymonth is None:
+                    bymonth = dtstart.month
+                    self._original_rule['bymonth'] = None
+                bymonthday = dtstart.day
+                self._original_rule['bymonthday'] = None
+            elif freq == MONTHLY:
+                bymonthday = dtstart.day
+                self._original_rule['bymonthday'] = None
+            elif freq == WEEKLY:
+                byweekday = dtstart.weekday()
+                self._original_rule['byweekday'] = None
+
+        # bymonth
+        if bymonth is None:
+            self._bymonth = None
+        else:
+            if isinstance(bymonth, integer_types):
+                bymonth = (bymonth,)
+
+            self._bymonth = tuple(sorted(set(bymonth)))
+
+            if 'bymonth' not in self._original_rule:
+                self._original_rule['bymonth'] = self._bymonth
+
+        # byyearday
+        if byyearday is None:
+            self._byyearday = None
+        else:
+            if isinstance(byyearday, integer_types):
+                byyearday = (byyearday,)
+
+            self._byyearday = tuple(sorted(set(byyearday)))
+            self._original_rule['byyearday'] = self._byyearday
+
+        # byeaster
+        if byeaster is not None:
+            if not easter:
+                from dateutil import easter
+            if isinstance(byeaster, integer_types):
+                self._byeaster = (byeaster,)
+            else:
+                self._byeaster = tuple(sorted(byeaster))
+
+            self._original_rule['byeaster'] = self._byeaster
+        else:
+            self._byeaster = None
+
+        # bymonthday
+        if bymonthday is None:
+            self._bymonthday = ()
+            self._bynmonthday = ()
+        else:
+            if isinstance(bymonthday, integer_types):
+                bymonthday = (bymonthday,)
+
+            bymonthday = set(bymonthday)            # Ensure it's unique
+
+            self._bymonthday = tuple(sorted(x for x in bymonthday if x > 0))
+            self._bynmonthday = tuple(sorted(x for x in bymonthday if x < 0))
+
+            # Storing positive numbers first, then negative numbers
+            if 'bymonthday' not in self._original_rule:
+                self._original_rule['bymonthday'] = tuple(
+                    itertools.chain(self._bymonthday, self._bynmonthday))
+
+        # byweekno
+        if byweekno is None:
+            self._byweekno = None
+        else:
+            if isinstance(byweekno, integer_types):
+                byweekno = (byweekno,)
+
+            self._byweekno = tuple(sorted(set(byweekno)))
+
+            self._original_rule['byweekno'] = self._byweekno
+
+        # byweekday / bynweekday
+        if byweekday is None:
+            self._byweekday = None
+            self._bynweekday = None
+        else:
+            # If it's one of the valid non-sequence types, convert to a
+            # single-element sequence before the iterator that builds the
+            # byweekday set.
+            if isinstance(byweekday, integer_types) or hasattr(byweekday, "n"):
+                byweekday = (byweekday,)
+
+            self._byweekday = set()
+            self._bynweekday = set()
+            for wday in byweekday:
+                if isinstance(wday, integer_types):
+                    self._byweekday.add(wday)
+                elif not wday.n or freq > MONTHLY:
+                    self._byweekday.add(wday.weekday)
+                else:
+                    self._bynweekday.add((wday.weekday, wday.n))
+
+            if not self._byweekday:
+                self._byweekday = None
+            elif not self._bynweekday:
+                self._bynweekday = None
+
+            if self._byweekday is not None:
+                self._byweekday = tuple(sorted(self._byweekday))
+                orig_byweekday = [weekday(x) for x in self._byweekday]
+            else:
+                orig_byweekday = ()
+
+            if self._bynweekday is not None:
+                self._bynweekday = tuple(sorted(self._bynweekday))
+                orig_bynweekday = [weekday(*x) for x in self._bynweekday]
+            else:
+                orig_bynweekday = ()
+
+            if 'byweekday' not in self._original_rule:
+                self._original_rule['byweekday'] = tuple(itertools.chain(
+                    orig_byweekday, orig_bynweekday))
+
+        # byhour
+        if byhour is None:
+            if freq < HOURLY:
+                self._byhour = {dtstart.hour}
+            else:
+                self._byhour = None
+        else:
+            if isinstance(byhour, integer_types):
+                byhour = (byhour,)
+
+            if freq == HOURLY:
+                self._byhour = self.__construct_byset(start=dtstart.hour,
+                                                      byxxx=byhour,
+                                                      base=24)
+            else:
+                self._byhour = set(byhour)
+
+            self._byhour = tuple(sorted(self._byhour))
+            self._original_rule['byhour'] = self._byhour
+
+        # byminute
+        if byminute is None:
+            if freq < MINUTELY:
+                self._byminute = {dtstart.minute}
+            else:
+                self._byminute = None
+        else:
+            if isinstance(byminute, integer_types):
+                byminute = (byminute,)
+
+            if freq == MINUTELY:
+                self._byminute = self.__construct_byset(start=dtstart.minute,
+                                                        byxxx=byminute,
+                                                        base=60)
+            else:
+                self._byminute = set(byminute)
+
+            self._byminute = tuple(sorted(self._byminute))
+            self._original_rule['byminute'] = self._byminute
+
+        # bysecond
+        if bysecond is None:
+            if freq < SECONDLY:
+                self._bysecond = ((dtstart.second,))
+            else:
+                self._bysecond = None
+        else:
+            if isinstance(bysecond, integer_types):
+                bysecond = (bysecond,)
+
+            self._bysecond = set(bysecond)
+
+            if freq == SECONDLY:
+                self._bysecond = self.__construct_byset(start=dtstart.second,
+                                                        byxxx=bysecond,
+                                                        base=60)
+            else:
+                self._bysecond = set(bysecond)
+
+            self._bysecond = tuple(sorted(self._bysecond))
+            self._original_rule['bysecond'] = self._bysecond
+
+        if self._freq >= HOURLY:
+            self._timeset = None
+        else:
+            self._timeset = []
+            for hour in self._byhour:
+                for minute in self._byminute:
+                    for second in self._bysecond:
+                        self._timeset.append(
+                            datetime.time(hour, minute, second,
+                                          tzinfo=self._tzinfo))
+            self._timeset.sort()
+            self._timeset = tuple(self._timeset)
+
+    def __str__(self):
+        """
+        Output a string that would generate this RRULE if passed to rrulestr.
+        This is mostly compatible with RFC5545, except for the
+        dateutil-specific extension BYEASTER.
+        """
+
+        output = []
+        h, m, s = [None] * 3
+        if self._dtstart:
+            output.append(self._dtstart.strftime('DTSTART:%Y%m%dT%H%M%S'))
+            h, m, s = self._dtstart.timetuple()[3:6]
+
+        parts = ['FREQ=' + FREQNAMES[self._freq]]
+        if self._interval != 1:
+            parts.append('INTERVAL=' + str(self._interval))
+
+        if self._wkst:
+            parts.append('WKST=' + repr(weekday(self._wkst))[0:2])
+
+        if self._count is not None:
+            parts.append('COUNT=' + str(self._count))
+
+        if self._until:
+            parts.append(self._until.strftime('UNTIL=%Y%m%dT%H%M%S'))
+
+        if self._original_rule.get('byweekday') is not None:
+            # The str() method on weekday objects doesn't generate
+            # RFC5545-compliant strings, so we should modify that.
+            original_rule = dict(self._original_rule)
+            wday_strings = []
+            for wday in original_rule['byweekday']:
+                if wday.n:
+                    wday_strings.append('{n:+d}{wday}'.format(
+                        n=wday.n,
+                        wday=repr(wday)[0:2]))
+                else:
+                    wday_strings.append(repr(wday))
+
+            original_rule['byweekday'] = wday_strings
+        else:
+            original_rule = self._original_rule
+
+        partfmt = '{name}={vals}'
+        for name, key in [('BYSETPOS', 'bysetpos'),
+                          ('BYMONTH', 'bymonth'),
+                          ('BYMONTHDAY', 'bymonthday'),
+                          ('BYYEARDAY', 'byyearday'),
+                          ('BYWEEKNO', 'byweekno'),
+                          ('BYDAY', 'byweekday'),
+                          ('BYHOUR', 'byhour'),
+                          ('BYMINUTE', 'byminute'),
+                          ('BYSECOND', 'bysecond'),
+                          ('BYEASTER', 'byeaster')]:
+            value = original_rule.get(key)
+            if value:
+                parts.append(partfmt.format(name=name, vals=(','.join(str(v)
+                                                             for v in value))))
+
+        output.append('RRULE:' + ';'.join(parts))
+        return '\n'.join(output)
+
+    def replace(self, **kwargs):
+        """Return new rrule with same attributes except for those attributes given new
+           values by whichever keyword arguments are specified."""
+        new_kwargs = {"interval": self._interval,
+                      "count": self._count,
+                      "dtstart": self._dtstart,
+                      "freq": self._freq,
+                      "until": self._until,
+                      "wkst": self._wkst,
+                      "cache": False if self._cache is None else True }
+        new_kwargs.update(self._original_rule)
+        new_kwargs.update(kwargs)
+        return rrule(**new_kwargs)
+
+    def _iter(self):
+        year, month, day, hour, minute, second, weekday, yearday, _ = \
+            self._dtstart.timetuple()
+
+        # Some local variables to speed things up a bit
+        freq = self._freq
+        interval = self._interval
+        wkst = self._wkst
+        until = self._until
+        bymonth = self._bymonth
+        byweekno = self._byweekno
+        byyearday = self._byyearday
+        byweekday = self._byweekday
+        byeaster = self._byeaster
+        bymonthday = self._bymonthday
+        bynmonthday = self._bynmonthday
+        bysetpos = self._bysetpos
+        byhour = self._byhour
+        byminute = self._byminute
+        bysecond = self._bysecond
+
+        ii = _iterinfo(self)
+        ii.rebuild(year, month)
+
+        getdayset = {YEARLY: ii.ydayset,
+                     MONTHLY: ii.mdayset,
+                     WEEKLY: ii.wdayset,
+                     DAILY: ii.ddayset,
+                     HOURLY: ii.ddayset,
+                     MINUTELY: ii.ddayset,
+                     SECONDLY: ii.ddayset}[freq]
+
+        if freq < HOURLY:
+            timeset = self._timeset
+        else:
+            gettimeset = {HOURLY: ii.htimeset,
+                          MINUTELY: ii.mtimeset,
+                          SECONDLY: ii.stimeset}[freq]
+            if ((freq >= HOURLY and
+                 self._byhour and hour not in self._byhour) or
+                (freq >= MINUTELY and
+                 self._byminute and minute not in self._byminute) or
+                (freq >= SECONDLY and
+                 self._bysecond and second not in self._bysecond)):
+                timeset = ()
+            else:
+                timeset = gettimeset(hour, minute, second)
+
+        total = 0
+        count = self._count
+        while True:
+            # Get dayset with the right frequency
+            dayset, start, end = getdayset(year, month, day)
+
+            # Do the "hard" work ;-)
+            filtered = False
+            for i in dayset[start:end]:
+                if ((bymonth and ii.mmask[i] not in bymonth) or
+                    (byweekno and not ii.wnomask[i]) or
+                    (byweekday and ii.wdaymask[i] not in byweekday) or
+                    (ii.nwdaymask and not ii.nwdaymask[i]) or
+                    (byeaster and not ii.eastermask[i]) or
+                    ((bymonthday or bynmonthday) and
+                     ii.mdaymask[i] not in bymonthday and
+                     ii.nmdaymask[i] not in bynmonthday) or
+                    (byyearday and
+                     ((i < ii.yearlen and i+1 not in byyearday and
+                       -ii.yearlen+i not in byyearday) or
+                      (i >= ii.yearlen and i+1-ii.yearlen not in byyearday and
+                       -ii.nextyearlen+i-ii.yearlen not in byyearday)))):
+                    dayset[i] = None
+                    filtered = True
+
+            # Output results
+            if bysetpos and timeset:
+                poslist = []
+                for pos in bysetpos:
+                    if pos < 0:
+                        daypos, timepos = divmod(pos, len(timeset))
+                    else:
+                        daypos, timepos = divmod(pos-1, len(timeset))
+                    try:
+                        i = [x for x in dayset[start:end]
+                             if x is not None][daypos]
+                        time = timeset[timepos]
+                    except IndexError:
+                        pass
+                    else:
+                        date = datetime.date.fromordinal(ii.yearordinal+i)
+                        res = datetime.datetime.combine(date, time)
+                        if res not in poslist:
+                            poslist.append(res)
+                poslist.sort()
+                for res in poslist:
+                    if until and res > until:
+                        self._len = total
+                        return
+                    elif res >= self._dtstart:
+                        if count is not None:
+                            count -= 1
+                            if count < 0:
+                                self._len = total
+                                return
+                        total += 1
+                        yield res
+            else:
+                for i in dayset[start:end]:
+                    if i is not None:
+                        date = datetime.date.fromordinal(ii.yearordinal + i)
+                        for time in timeset:
+                            res = datetime.datetime.combine(date, time)
+                            if until and res > until:
+                                self._len = total
+                                return
+                            elif res >= self._dtstart:
+                                if count is not None:
+                                    count -= 1
+                                    if count < 0:
+                                        self._len = total
+                                        return
+
+                                total += 1
+                                yield res
+
+            # Handle frequency and interval
+            fixday = False
+            if freq == YEARLY:
+                year += interval
+                if year > datetime.MAXYEAR:
+                    self._len = total
+                    return
+                ii.rebuild(year, month)
+            elif freq == MONTHLY:
+                month += interval
+                if month > 12:
+                    div, mod = divmod(month, 12)
+                    month = mod
+                    year += div
+                    if month == 0:
+                        month = 12
+                        year -= 1
+                    if year > datetime.MAXYEAR:
+                        self._len = total
+                        return
+                ii.rebuild(year, month)
+            elif freq == WEEKLY:
+                if wkst > weekday:
+                    day += -(weekday+1+(6-wkst))+self._interval*7
+                else:
+                    day += -(weekday-wkst)+self._interval*7
+                weekday = wkst
+                fixday = True
+            elif freq == DAILY:
+                day += interval
+                fixday = True
+            elif freq == HOURLY:
+                if filtered:
+                    # Jump to one iteration before next day
+                    hour += ((23-hour)//interval)*interval
+
+                if byhour:
+                    ndays, hour = self.__mod_distance(value=hour,
+                                                      byxxx=self._byhour,
+                                                      base=24)
+                else:
+                    ndays, hour = divmod(hour+interval, 24)
+
+                if ndays:
+                    day += ndays
+                    fixday = True
+
+                timeset = gettimeset(hour, minute, second)
+            elif freq == MINUTELY:
+                if filtered:
+                    # Jump to one iteration before next day
+                    minute += ((1439-(hour*60+minute))//interval)*interval
+
+                valid = False
+                rep_rate = (24*60)
+                for j in range(rep_rate // gcd(interval, rep_rate)):
+                    if byminute:
+                        nhours, minute = \
+                            self.__mod_distance(value=minute,
+                                                byxxx=self._byminute,
+                                                base=60)
+                    else:
+                        nhours, minute = divmod(minute+interval, 60)
+
+                    div, hour = divmod(hour+nhours, 24)
+                    if div:
+                        day += div
+                        fixday = True
+                        filtered = False
+
+                    if not byhour or hour in byhour:
+                        valid = True
+                        break
+
+                if not valid:
+                    raise ValueError('Invalid combination of interval and ' +
+                                     'byhour resulting in empty rule.')
+
+                timeset = gettimeset(hour, minute, second)
+            elif freq == SECONDLY:
+                if filtered:
+                    # Jump to one iteration before next day
+                    second += (((86399 - (hour * 3600 + minute * 60 + second))
+                                // interval) * interval)
+
+                rep_rate = (24 * 3600)
+                valid = False
+                for j in range(0, rep_rate // gcd(interval, rep_rate)):
+                    if bysecond:
+                        nminutes, second = \
+                            self.__mod_distance(value=second,
+                                                byxxx=self._bysecond,
+                                                base=60)
+                    else:
+                        nminutes, second = divmod(second+interval, 60)
+
+                    div, minute = divmod(minute+nminutes, 60)
+                    if div:
+                        hour += div
+                        div, hour = divmod(hour, 24)
+                        if div:
+                            day += div
+                            fixday = True
+
+                    if ((not byhour or hour in byhour) and
+                            (not byminute or minute in byminute) and
+                            (not bysecond or second in bysecond)):
+                        valid = True
+                        break
+
+                if not valid:
+                    raise ValueError('Invalid combination of interval, ' +
+                                     'byhour and byminute resulting in empty' +
+                                     ' rule.')
+
+                timeset = gettimeset(hour, minute, second)
+
+            if fixday and day > 28:
+                daysinmonth = calendar.monthrange(year, month)[1]
+                if day > daysinmonth:
+                    while day > daysinmonth:
+                        day -= daysinmonth
+                        month += 1
+                        if month == 13:
+                            month = 1
+                            year += 1
+                            if year > datetime.MAXYEAR:
+                                self._len = total
+                                return
+                        daysinmonth = calendar.monthrange(year, month)[1]
+                    ii.rebuild(year, month)
+
+    def __construct_byset(self, start, byxxx, base):
+        """
+        If a `BYXXX` sequence is passed to the constructor at the same level as
+        `FREQ` (e.g. `FREQ=HOURLY,BYHOUR={2,4,7},INTERVAL=3`), there are some
+        specifications which cannot be reached given some starting conditions.
+
+        This occurs whenever the interval is not coprime with the base of a
+        given unit and the difference between the starting position and the
+        ending position is not coprime with the greatest common denominator
+        between the interval and the base. For example, with a FREQ of hourly
+        starting at 17:00 and an interval of 4, the only valid values for
+        BYHOUR would be {21, 1, 5, 9, 13, 17}, because 4 and 24 are not
+        coprime.
+
+        :param start:
+            Specifies the starting position.
+        :param byxxx:
+            An iterable containing the list of allowed values.
+        :param base:
+            The largest allowable value for the specified frequency (e.g.
+            24 hours, 60 minutes).
+
+        This does not preserve the type of the iterable, returning a set, since
+        the values should be unique and the order is irrelevant, this will
+        speed up later lookups.
+
+        In the event of an empty set, raises a :exception:`ValueError`, as this
+        results in an empty rrule.
+        """
+
+        cset = set()
+
+        # Support a single byxxx value.
+        if isinstance(byxxx, integer_types):
+            byxxx = (byxxx, )
+
+        for num in byxxx:
+            i_gcd = gcd(self._interval, base)
+            # Use divmod rather than % because we need to wrap negative nums.
+            if i_gcd == 1 or divmod(num - start, i_gcd)[1] == 0:
+                cset.add(num)
+
+        if len(cset) == 0:
+            raise ValueError("Invalid rrule byxxx generates an empty set.")
+
+        return cset
+
+    def __mod_distance(self, value, byxxx, base):
+        """
+        Calculates the next value in a sequence where the `FREQ` parameter is
+        specified along with a `BYXXX` parameter at the same "level"
+        (e.g. `HOURLY` specified with `BYHOUR`).
+
+        :param value:
+            The old value of the component.
+        :param byxxx:
+            The `BYXXX` set, which should have been generated by
+            `rrule._construct_byset`, or something else which checks that a
+            valid rule is present.
+        :param base:
+            The largest allowable value for the specified frequency (e.g.
+            24 hours, 60 minutes).
+
+        If a valid value is not found after `base` iterations (the maximum
+        number before the sequence would start to repeat), this raises a
+        :exception:`ValueError`, as no valid values were found.
+
+        This returns a tuple of `divmod(n*interval, base)`, where `n` is the
+        smallest number of `interval` repetitions until the next specified
+        value in `byxxx` is found.
+        """
+        accumulator = 0
+        for ii in range(1, base + 1):
+            # Using divmod() over % to account for negative intervals
+            div, value = divmod(value + self._interval, base)
+            accumulator += div
+            if value in byxxx:
+                return (accumulator, value)
+
+
+class _iterinfo(object):
+    __slots__ = ["rrule", "lastyear", "lastmonth",
+                 "yearlen", "nextyearlen", "yearordinal", "yearweekday",
+                 "mmask", "mrange", "mdaymask", "nmdaymask",
+                 "wdaymask", "wnomask", "nwdaymask", "eastermask"]
+
+    def __init__(self, rrule):
+        for attr in self.__slots__:
+            setattr(self, attr, None)
+        self.rrule = rrule
+
+    def rebuild(self, year, month):
+        # Every mask is 7 days longer to handle cross-year weekly periods.
+        rr = self.rrule
+        if year != self.lastyear:
+            self.yearlen = 365 + calendar.isleap(year)
+            self.nextyearlen = 365 + calendar.isleap(year + 1)
+            firstyday = datetime.date(year, 1, 1)
+            self.yearordinal = firstyday.toordinal()
+            self.yearweekday = firstyday.weekday()
+
+            wday = datetime.date(year, 1, 1).weekday()
+            if self.yearlen == 365:
+                self.mmask = M365MASK
+                self.mdaymask = MDAY365MASK
+                self.nmdaymask = NMDAY365MASK
+                self.wdaymask = WDAYMASK[wday:]
+                self.mrange = M365RANGE
+            else:
+                self.mmask = M366MASK
+                self.mdaymask = MDAY366MASK
+                self.nmdaymask = NMDAY366MASK
+                self.wdaymask = WDAYMASK[wday:]
+                self.mrange = M366RANGE
+
+            if not rr._byweekno:
+                self.wnomask = None
+            else:
+                self.wnomask = [0]*(self.yearlen+7)
+                # no1wkst = firstwkst = self.wdaymask.index(rr._wkst)
+                no1wkst = firstwkst = (7-self.yearweekday+rr._wkst) % 7
+                if no1wkst >= 4:
+                    no1wkst = 0
+                    # Number of days in the year, plus the days we got
+                    # from last year.
+                    wyearlen = self.yearlen+(self.yearweekday-rr._wkst) % 7
+                else:
+                    # Number of days in the year, minus the days we
+                    # left in last year.
+                    wyearlen = self.yearlen-no1wkst
+                div, mod = divmod(wyearlen, 7)
+                numweeks = div+mod//4
+                for n in rr._byweekno:
+                    if n < 0:
+                        n += numweeks+1
+                    if not (0 < n <= numweeks):
+                        continue
+                    if n > 1:
+                        i = no1wkst+(n-1)*7
+                        if no1wkst != firstwkst:
+                            i -= 7-firstwkst
+                    else:
+                        i = no1wkst
+                    for j in range(7):
+                        self.wnomask[i] = 1
+                        i += 1
+                        if self.wdaymask[i] == rr._wkst:
+                            break
+                if 1 in rr._byweekno:
+                    # Check week number 1 of next year as well
+                    # TODO: Check -numweeks for next year.
+                    i = no1wkst+numweeks*7
+                    if no1wkst != firstwkst:
+                        i -= 7-firstwkst
+                    if i < self.yearlen:
+                        # If week starts in next year, we
+                        # don't care about it.
+                        for j in range(7):
+                            self.wnomask[i] = 1
+                            i += 1
+                            if self.wdaymask[i] == rr._wkst:
+                                break
+                if no1wkst:
+                    # Check last week number of last year as
+                    # well. If no1wkst is 0, either the year
+                    # started on week start, or week number 1
+                    # got days from last year, so there are no
+                    # days from last year's last week number in
+                    # this year.
+                    if -1 not in rr._byweekno:
+                        lyearweekday = datetime.date(year-1, 1, 1).weekday()
+                        lno1wkst = (7-lyearweekday+rr._wkst) % 7
+                        lyearlen = 365+calendar.isleap(year-1)
+                        if lno1wkst >= 4:
+                            lno1wkst = 0
+                            lnumweeks = 52+(lyearlen +
+                                            (lyearweekday-rr._wkst) % 7) % 7//4
+                        else:
+                            lnumweeks = 52+(self.yearlen-no1wkst) % 7//4
+                    else:
+                        lnumweeks = -1
+                    if lnumweeks in rr._byweekno:
+                        for i in range(no1wkst):
+                            self.wnomask[i] = 1
+
+        if (rr._bynweekday and (month != self.lastmonth or
+                                year != self.lastyear)):
+            ranges = []
+            if rr._freq == YEARLY:
+                if rr._bymonth:
+                    for month in rr._bymonth:
+                        ranges.append(self.mrange[month-1:month+1])
+                else:
+                    ranges = [(0, self.yearlen)]
+            elif rr._freq == MONTHLY:
+                ranges = [self.mrange[month-1:month+1]]
+            if ranges:
+                # Weekly frequency won't get here, so we may not
+                # care about cross-year weekly periods.
+                self.nwdaymask = [0]*self.yearlen
+                for first, last in ranges:
+                    last -= 1
+                    for wday, n in rr._bynweekday:
+                        if n < 0:
+                            i = last+(n+1)*7
+                            i -= (self.wdaymask[i]-wday) % 7
+                        else:
+                            i = first+(n-1)*7
+                            i += (7-self.wdaymask[i]+wday) % 7
+                        if first <= i <= last:
+                            self.nwdaymask[i] = 1
+
+        if rr._byeaster:
+            self.eastermask = [0]*(self.yearlen+7)
+            eyday = easter.easter(year).toordinal()-self.yearordinal
+            for offset in rr._byeaster:
+                self.eastermask[eyday+offset] = 1
+
+        self.lastyear = year
+        self.lastmonth = month
+
+    def ydayset(self, year, month, day):
+        return list(range(self.yearlen)), 0, self.yearlen
+
+    def mdayset(self, year, month, day):
+        dset = [None]*self.yearlen
+        start, end = self.mrange[month-1:month+1]
+        for i in range(start, end):
+            dset[i] = i
+        return dset, start, end
+
+    def wdayset(self, year, month, day):
+        # We need to handle cross-year weeks here.
+        dset = [None]*(self.yearlen+7)
+        i = datetime.date(year, month, day).toordinal()-self.yearordinal
+        start = i
+        for j in range(7):
+            dset[i] = i
+            i += 1
+            # if (not (0 <= i < self.yearlen) or
+            #    self.wdaymask[i] == self.rrule._wkst):
+            # This will cross the year boundary, if necessary.
+            if self.wdaymask[i] == self.rrule._wkst:
+                break
+        return dset, start, i
+
+    def ddayset(self, year, month, day):
+        dset = [None] * self.yearlen
+        i = datetime.date(year, month, day).toordinal() - self.yearordinal
+        dset[i] = i
+        return dset, i, i + 1
+
+    def htimeset(self, hour, minute, second):
+        tset = []
+        rr = self.rrule
+        for minute in rr._byminute:
+            for second in rr._bysecond:
+                tset.append(datetime.time(hour, minute, second,
+                                          tzinfo=rr._tzinfo))
+        tset.sort()
+        return tset
+
+    def mtimeset(self, hour, minute, second):
+        tset = []
+        rr = self.rrule
+        for second in rr._bysecond:
+            tset.append(datetime.time(hour, minute, second, tzinfo=rr._tzinfo))
+        tset.sort()
+        return tset
+
+    def stimeset(self, hour, minute, second):
+        return (datetime.time(hour, minute, second,
+                tzinfo=self.rrule._tzinfo),)
+
+
+class rruleset(rrulebase):
+    """ The rruleset type allows more complex recurrence setups, mixing
+    multiple rules, dates, exclusion rules, and exclusion dates. The type
+    constructor takes the following keyword arguments:
+
+    :param cache: If True, caching of results will be enabled, improving
+                  performance of multiple queries considerably. """
+
+    class _genitem(object):
+        def __init__(self, genlist, gen):
+            try:
+                self.dt = advance_iterator(gen)
+                genlist.append(self)
+            except StopIteration:
+                pass
+            self.genlist = genlist
+            self.gen = gen
+
+        def __next__(self):
+            try:
+                self.dt = advance_iterator(self.gen)
+            except StopIteration:
+                if self.genlist[0] is self:
+                    heapq.heappop(self.genlist)
+                else:
+                    self.genlist.remove(self)
+                    heapq.heapify(self.genlist)
+
+        next = __next__
+
+        def __lt__(self, other):
+            return self.dt < other.dt
+
+        def __gt__(self, other):
+            return self.dt > other.dt
+
+        def __eq__(self, other):
+            return self.dt == other.dt
+
+        def __ne__(self, other):
+            return self.dt != other.dt
+
+    def __init__(self, cache=False):
+        super(rruleset, self).__init__(cache)
+        self._rrule = []
+        self._rdate = []
+        self._exrule = []
+        self._exdate = []
+
+    @_invalidates_cache
+    def rrule(self, rrule):
+        """ Include the given :py:class:`rrule` instance in the recurrence set
+            generation. """
+        self._rrule.append(rrule)
+
+    @_invalidates_cache
+    def rdate(self, rdate):
+        """ Include the given :py:class:`datetime` instance in the recurrence
+            set generation. """
+        self._rdate.append(rdate)
+
+    @_invalidates_cache
+    def exrule(self, exrule):
+        """ Include the given rrule instance in the recurrence set exclusion
+            list. Dates which are part of the given recurrence rules will not
+            be generated, even if some inclusive rrule or rdate matches them.
+        """
+        self._exrule.append(exrule)
+
+    @_invalidates_cache
+    def exdate(self, exdate):
+        """ Include the given datetime instance in the recurrence set
+            exclusion list. Dates included that way will not be generated,
+            even if some inclusive rrule or rdate matches them. """
+        self._exdate.append(exdate)
+
+    def _iter(self):
+        rlist = []
+        self._rdate.sort()
+        self._genitem(rlist, iter(self._rdate))
+        for gen in [iter(x) for x in self._rrule]:
+            self._genitem(rlist, gen)
+        exlist = []
+        self._exdate.sort()
+        self._genitem(exlist, iter(self._exdate))
+        for gen in [iter(x) for x in self._exrule]:
+            self._genitem(exlist, gen)
+        lastdt = None
+        total = 0
+        heapq.heapify(rlist)
+        heapq.heapify(exlist)
+        while rlist:
+            ritem = rlist[0]
+            if not lastdt or lastdt != ritem.dt:
+                while exlist and exlist[0] < ritem:
+                    exitem = exlist[0]
+                    advance_iterator(exitem)
+                    if exlist and exlist[0] is exitem:
+                        heapq.heapreplace(exlist, exitem)
+                if not exlist or ritem != exlist[0]:
+                    total += 1
+                    yield ritem.dt
+                lastdt = ritem.dt
+            advance_iterator(ritem)
+            if rlist and rlist[0] is ritem:
+                heapq.heapreplace(rlist, ritem)
+        self._len = total
+
+
+
+
+class _rrulestr(object):
+    """ Parses a string representation of a recurrence rule or set of
+    recurrence rules.
+
+    :param s:
+        Required, a string defining one or more recurrence rules.
+
+    :param dtstart:
+        If given, used as the default recurrence start if not specified in the
+        rule string.
+
+    :param cache:
+        If set ``True`` caching of results will be enabled, improving
+        performance of multiple queries considerably.
+
+    :param unfold:
+        If set ``True`` indicates that a rule string is split over more
+        than one line and should be joined before processing.
+
+    :param forceset:
+        If set ``True`` forces a :class:`dateutil.rrule.rruleset` to
+        be returned.
+
+    :param compatible:
+        If set ``True`` forces ``unfold`` and ``forceset`` to be ``True``.
+
+    :param ignoretz:
+        If set ``True``, time zones in parsed strings are ignored and a naive
+        :class:`datetime.datetime` object is returned.
+
+    :param tzids:
+        If given, a callable or mapping used to retrieve a
+        :class:`datetime.tzinfo` from a string representation.
+        Defaults to :func:`dateutil.tz.gettz`.
+
+    :param tzinfos:
+        Additional time zone names / aliases which may be present in a string
+        representation.  See :func:`dateutil.parser.parse` for more
+        information.
+
+    :return:
+        Returns a :class:`dateutil.rrule.rruleset` or
+        :class:`dateutil.rrule.rrule`
+    """
+
+    _freq_map = {"YEARLY": YEARLY,
+                 "MONTHLY": MONTHLY,
+                 "WEEKLY": WEEKLY,
+                 "DAILY": DAILY,
+                 "HOURLY": HOURLY,
+                 "MINUTELY": MINUTELY,
+                 "SECONDLY": SECONDLY}
+
+    _weekday_map = {"MO": 0, "TU": 1, "WE": 2, "TH": 3,
+                    "FR": 4, "SA": 5, "SU": 6}
+
+    def _handle_int(self, rrkwargs, name, value, **kwargs):
+        rrkwargs[name.lower()] = int(value)
+
+    def _handle_int_list(self, rrkwargs, name, value, **kwargs):
+        rrkwargs[name.lower()] = [int(x) for x in value.split(',')]
+
+    _handle_INTERVAL = _handle_int
+    _handle_COUNT = _handle_int
+    _handle_BYSETPOS = _handle_int_list
+    _handle_BYMONTH = _handle_int_list
+    _handle_BYMONTHDAY = _handle_int_list
+    _handle_BYYEARDAY = _handle_int_list
+    _handle_BYEASTER = _handle_int_list
+    _handle_BYWEEKNO = _handle_int_list
+    _handle_BYHOUR = _handle_int_list
+    _handle_BYMINUTE = _handle_int_list
+    _handle_BYSECOND = _handle_int_list
+
+    def _handle_FREQ(self, rrkwargs, name, value, **kwargs):
+        rrkwargs["freq"] = self._freq_map[value]
+
+    def _handle_UNTIL(self, rrkwargs, name, value, **kwargs):
+        global parser
+        if not parser:
+            from dateutil import parser
+        try:
+            rrkwargs["until"] = parser.parse(value,
+                                             ignoretz=kwargs.get("ignoretz"),
+                                             tzinfos=kwargs.get("tzinfos"))
+        except ValueError:
+            raise ValueError("invalid until date")
+
+    def _handle_WKST(self, rrkwargs, name, value, **kwargs):
+        rrkwargs["wkst"] = self._weekday_map[value]
+
+    def _handle_BYWEEKDAY(self, rrkwargs, name, value, **kwargs):
+        """
+        Two ways to specify this: +1MO or MO(+1)
+        """
+        l = []
+        for wday in value.split(','):
+            if '(' in wday:
+                # If it's of the form TH(+1), etc.
+                splt = wday.split('(')
+                w = splt[0]
+                n = int(splt[1][:-1])
+            elif len(wday):
+                # If it's of the form +1MO
+                for i in range(len(wday)):
+                    if wday[i] not in '+-0123456789':
+                        break
+                n = wday[:i] or None
+                w = wday[i:]
+                if n:
+                    n = int(n)
+            else:
+                raise ValueError("Invalid (empty) BYDAY specification.")
+
+            l.append(weekdays[self._weekday_map[w]](n))
+        rrkwargs["byweekday"] = l
+
+    _handle_BYDAY = _handle_BYWEEKDAY
+
+    def _parse_rfc_rrule(self, line,
+                         dtstart=None,
+                         cache=False,
+                         ignoretz=False,
+                         tzinfos=None):
+        if line.find(':') != -1:
+            name, value = line.split(':')
+            if name != "RRULE":
+                raise ValueError("unknown parameter name")
+        else:
+            value = line
+        rrkwargs = {}
+        for pair in value.split(';'):
+            name, value = pair.split('=')
+            name = name.upper()
+            value = value.upper()
+            try:
+                getattr(self, "_handle_"+name)(rrkwargs, name, value,
+                                               ignoretz=ignoretz,
+                                               tzinfos=tzinfos)
+            except AttributeError:
+                raise ValueError("unknown parameter '%s'" % name)
+            except (KeyError, ValueError):
+                raise ValueError("invalid '%s': %s" % (name, value))
+        return rrule(dtstart=dtstart, cache=cache, **rrkwargs)
+
+    def _parse_date_value(self, date_value, parms, rule_tzids,
+                          ignoretz, tzids, tzinfos):
+        global parser
+        if not parser:
+            from dateutil import parser
+
+        datevals = []
+        value_found = False
+        TZID = None
+
+        for parm in parms:
+            if parm.startswith("TZID="):
+                try:
+                    tzkey = rule_tzids[parm.split('TZID=')[-1]]
+                except KeyError:
+                    continue
+                if tzids is None:
+                    from . import tz
+                    tzlookup = tz.gettz
+                elif callable(tzids):
+                    tzlookup = tzids
+                else:
+                    tzlookup = getattr(tzids, 'get', None)
+                    if tzlookup is None:
+                        msg = ('tzids must be a callable, mapping, or None, '
+                               'not %s' % tzids)
+                        raise ValueError(msg)
+
+                TZID = tzlookup(tzkey)
+                continue
+
+            # RFC 5445 3.8.2.4: The VALUE parameter is optional, but may be found
+            # only once.
+            if parm not in {"VALUE=DATE-TIME", "VALUE=DATE"}:
+                raise ValueError("unsupported parm: " + parm)
+            else:
+                if value_found:
+                    msg = ("Duplicate value parameter found in: " + parm)
+                    raise ValueError(msg)
+                value_found = True
+
+        for datestr in date_value.split(','):
+            date = parser.parse(datestr, ignoretz=ignoretz, tzinfos=tzinfos)
+            if TZID is not None:
+                if date.tzinfo is None:
+                    date = date.replace(tzinfo=TZID)
+                else:
+                    raise ValueError('DTSTART/EXDATE specifies multiple timezone')
+            datevals.append(date)
+
+        return datevals
+
+    def _parse_rfc(self, s,
+                   dtstart=None,
+                   cache=False,
+                   unfold=False,
+                   forceset=False,
+                   compatible=False,
+                   ignoretz=False,
+                   tzids=None,
+                   tzinfos=None):
+        global parser
+        if compatible:
+            forceset = True
+            unfold = True
+
+        TZID_NAMES = dict(map(
+            lambda x: (x.upper(), x),
+            re.findall('TZID=(?P[^:]+):', s)
+        ))
+        s = s.upper()
+        if not s.strip():
+            raise ValueError("empty string")
+        if unfold:
+            lines = s.splitlines()
+            i = 0
+            while i < len(lines):
+                line = lines[i].rstrip()
+                if not line:
+                    del lines[i]
+                elif i > 0 and line[0] == " ":
+                    lines[i-1] += line[1:]
+                    del lines[i]
+                else:
+                    i += 1
+        else:
+            lines = s.split()
+        if (not forceset and len(lines) == 1 and (s.find(':') == -1 or
+                                                  s.startswith('RRULE:'))):
+            return self._parse_rfc_rrule(lines[0], cache=cache,
+                                         dtstart=dtstart, ignoretz=ignoretz,
+                                         tzinfos=tzinfos)
+        else:
+            rrulevals = []
+            rdatevals = []
+            exrulevals = []
+            exdatevals = []
+            for line in lines:
+                if not line:
+                    continue
+                if line.find(':') == -1:
+                    name = "RRULE"
+                    value = line
+                else:
+                    name, value = line.split(':', 1)
+                parms = name.split(';')
+                if not parms:
+                    raise ValueError("empty property name")
+                name = parms[0]
+                parms = parms[1:]
+                if name == "RRULE":
+                    for parm in parms:
+                        raise ValueError("unsupported RRULE parm: "+parm)
+                    rrulevals.append(value)
+                elif name == "RDATE":
+                    for parm in parms:
+                        if parm != "VALUE=DATE-TIME":
+                            raise ValueError("unsupported RDATE parm: "+parm)
+                    rdatevals.append(value)
+                elif name == "EXRULE":
+                    for parm in parms:
+                        raise ValueError("unsupported EXRULE parm: "+parm)
+                    exrulevals.append(value)
+                elif name == "EXDATE":
+                    exdatevals.extend(
+                        self._parse_date_value(value, parms,
+                                               TZID_NAMES, ignoretz,
+                                               tzids, tzinfos)
+                    )
+                elif name == "DTSTART":
+                    dtvals = self._parse_date_value(value, parms, TZID_NAMES,
+                                                    ignoretz, tzids, tzinfos)
+                    if len(dtvals) != 1:
+                        raise ValueError("Multiple DTSTART values specified:" +
+                                         value)
+                    dtstart = dtvals[0]
+                else:
+                    raise ValueError("unsupported property: "+name)
+            if (forceset or len(rrulevals) > 1 or rdatevals
+                    or exrulevals or exdatevals):
+                if not parser and (rdatevals or exdatevals):
+                    from dateutil import parser
+                rset = rruleset(cache=cache)
+                for value in rrulevals:
+                    rset.rrule(self._parse_rfc_rrule(value, dtstart=dtstart,
+                                                     ignoretz=ignoretz,
+                                                     tzinfos=tzinfos))
+                for value in rdatevals:
+                    for datestr in value.split(','):
+                        rset.rdate(parser.parse(datestr,
+                                                ignoretz=ignoretz,
+                                                tzinfos=tzinfos))
+                for value in exrulevals:
+                    rset.exrule(self._parse_rfc_rrule(value, dtstart=dtstart,
+                                                      ignoretz=ignoretz,
+                                                      tzinfos=tzinfos))
+                for value in exdatevals:
+                    rset.exdate(value)
+                if compatible and dtstart:
+                    rset.rdate(dtstart)
+                return rset
+            else:
+                return self._parse_rfc_rrule(rrulevals[0],
+                                             dtstart=dtstart,
+                                             cache=cache,
+                                             ignoretz=ignoretz,
+                                             tzinfos=tzinfos)
+
+    def __call__(self, s, **kwargs):
+        return self._parse_rfc(s, **kwargs)
+
+
+rrulestr = _rrulestr()
+
+# vim:ts=4:sw=4:et
diff --git a/crates/hm-dsl-engine/vendor/dateutil/tz/__init__.py b/crates/hm-dsl-engine/vendor/dateutil/tz/__init__.py
new file mode 100644
index 00000000..af1352c4
--- /dev/null
+++ b/crates/hm-dsl-engine/vendor/dateutil/tz/__init__.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+from .tz import *
+from .tz import __doc__
+
+__all__ = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange",
+           "tzstr", "tzical", "tzwin", "tzwinlocal", "gettz",
+           "enfold", "datetime_ambiguous", "datetime_exists",
+           "resolve_imaginary", "UTC", "DeprecatedTzFormatWarning"]
+
+
+class DeprecatedTzFormatWarning(Warning):
+    """Warning raised when time zones are parsed from deprecated formats."""
diff --git a/crates/hm-dsl-engine/vendor/dateutil/tz/_common.py b/crates/hm-dsl-engine/vendor/dateutil/tz/_common.py
new file mode 100644
index 00000000..e6ac1183
--- /dev/null
+++ b/crates/hm-dsl-engine/vendor/dateutil/tz/_common.py
@@ -0,0 +1,419 @@
+from six import PY2
+
+from functools import wraps
+
+from datetime import datetime, timedelta, tzinfo
+
+
+ZERO = timedelta(0)
+
+__all__ = ['tzname_in_python2', 'enfold']
+
+
+def tzname_in_python2(namefunc):
+    """Change unicode output into bytestrings in Python 2
+
+    tzname() API changed in Python 3. It used to return bytes, but was changed
+    to unicode strings
+    """
+    if PY2:
+        @wraps(namefunc)
+        def adjust_encoding(*args, **kwargs):
+            name = namefunc(*args, **kwargs)
+            if name is not None:
+                name = name.encode()
+
+            return name
+
+        return adjust_encoding
+    else:
+        return namefunc
+
+
+# The following is adapted from Alexander Belopolsky's tz library
+# https://github.com/abalkin/tz
+if hasattr(datetime, 'fold'):
+    # This is the pre-python 3.6 fold situation
+    def enfold(dt, fold=1):
+        """
+        Provides a unified interface for assigning the ``fold`` attribute to
+        datetimes both before and after the implementation of PEP-495.
+
+        :param fold:
+            The value for the ``fold`` attribute in the returned datetime. This
+            should be either 0 or 1.
+
+        :return:
+            Returns an object for which ``getattr(dt, 'fold', 0)`` returns
+            ``fold`` for all versions of Python. In versions prior to
+            Python 3.6, this is a ``_DatetimeWithFold`` object, which is a
+            subclass of :py:class:`datetime.datetime` with the ``fold``
+            attribute added, if ``fold`` is 1.
+
+        .. versionadded:: 2.6.0
+        """
+        return dt.replace(fold=fold)
+
+else:
+    class _DatetimeWithFold(datetime):
+        """
+        This is a class designed to provide a PEP 495-compliant interface for
+        Python versions before 3.6. It is used only for dates in a fold, so
+        the ``fold`` attribute is fixed at ``1``.
+
+        .. versionadded:: 2.6.0
+        """
+        __slots__ = ()
+
+        def replace(self, *args, **kwargs):
+            """
+            Return a datetime with the same attributes, except for those
+            attributes given new values by whichever keyword arguments are
+            specified. Note that tzinfo=None can be specified to create a naive
+            datetime from an aware datetime with no conversion of date and time
+            data.
+
+            This is reimplemented in ``_DatetimeWithFold`` because pypy3 will
+            return a ``datetime.datetime`` even if ``fold`` is unchanged.
+            """
+            argnames = (
+                'year', 'month', 'day', 'hour', 'minute', 'second',
+                'microsecond', 'tzinfo'
+            )
+
+            for arg, argname in zip(args, argnames):
+                if argname in kwargs:
+                    raise TypeError('Duplicate argument: {}'.format(argname))
+
+                kwargs[argname] = arg
+
+            for argname in argnames:
+                if argname not in kwargs:
+                    kwargs[argname] = getattr(self, argname)
+
+            dt_class = self.__class__ if kwargs.get('fold', 1) else datetime
+
+            return dt_class(**kwargs)
+
+        @property
+        def fold(self):
+            return 1
+
+    def enfold(dt, fold=1):
+        """
+        Provides a unified interface for assigning the ``fold`` attribute to
+        datetimes both before and after the implementation of PEP-495.
+
+        :param fold:
+            The value for the ``fold`` attribute in the returned datetime. This
+            should be either 0 or 1.
+
+        :return:
+            Returns an object for which ``getattr(dt, 'fold', 0)`` returns
+            ``fold`` for all versions of Python. In versions prior to
+            Python 3.6, this is a ``_DatetimeWithFold`` object, which is a
+            subclass of :py:class:`datetime.datetime` with the ``fold``
+            attribute added, if ``fold`` is 1.
+
+        .. versionadded:: 2.6.0
+        """
+        if getattr(dt, 'fold', 0) == fold:
+            return dt
+
+        args = dt.timetuple()[:6]
+        args += (dt.microsecond, dt.tzinfo)
+
+        if fold:
+            return _DatetimeWithFold(*args)
+        else:
+            return datetime(*args)
+
+
+def _validate_fromutc_inputs(f):
+    """
+    The CPython version of ``fromutc`` checks that the input is a ``datetime``
+    object and that ``self`` is attached as its ``tzinfo``.
+    """
+    @wraps(f)
+    def fromutc(self, dt):
+        if not isinstance(dt, datetime):
+            raise TypeError("fromutc() requires a datetime argument")
+        if dt.tzinfo is not self:
+            raise ValueError("dt.tzinfo is not self")
+
+        return f(self, dt)
+
+    return fromutc
+
+
+class _tzinfo(tzinfo):
+    """
+    Base class for all ``dateutil`` ``tzinfo`` objects.
+    """
+
+    def is_ambiguous(self, dt):
+        """
+        Whether or not the "wall time" of a given datetime is ambiguous in this
+        zone.
+
+        :param dt:
+            A :py:class:`datetime.datetime`, naive or time zone aware.
+
+
+        :return:
+            Returns ``True`` if ambiguous, ``False`` otherwise.
+
+        .. versionadded:: 2.6.0
+        """
+
+        dt = dt.replace(tzinfo=self)
+
+        wall_0 = enfold(dt, fold=0)
+        wall_1 = enfold(dt, fold=1)
+
+        same_offset = wall_0.utcoffset() == wall_1.utcoffset()
+        same_dt = wall_0.replace(tzinfo=None) == wall_1.replace(tzinfo=None)
+
+        return same_dt and not same_offset
+
+    def _fold_status(self, dt_utc, dt_wall):
+        """
+        Determine the fold status of a "wall" datetime, given a representation
+        of the same datetime as a (naive) UTC datetime. This is calculated based
+        on the assumption that ``dt.utcoffset() - dt.dst()`` is constant for all
+        datetimes, and that this offset is the actual number of hours separating
+        ``dt_utc`` and ``dt_wall``.
+
+        :param dt_utc:
+            Representation of the datetime as UTC
+
+        :param dt_wall:
+            Representation of the datetime as "wall time". This parameter must
+            either have a `fold` attribute or have a fold-naive
+            :class:`datetime.tzinfo` attached, otherwise the calculation may
+            fail.
+        """
+        if self.is_ambiguous(dt_wall):
+            delta_wall = dt_wall - dt_utc
+            _fold = int(delta_wall == (dt_utc.utcoffset() - dt_utc.dst()))
+        else:
+            _fold = 0
+
+        return _fold
+
+    def _fold(self, dt):
+        return getattr(dt, 'fold', 0)
+
+    def _fromutc(self, dt):
+        """
+        Given a timezone-aware datetime in a given timezone, calculates a
+        timezone-aware datetime in a new timezone.
+
+        Since this is the one time that we *know* we have an unambiguous
+        datetime object, we take this opportunity to determine whether the
+        datetime is ambiguous and in a "fold" state (e.g. if it's the first
+        occurrence, chronologically, of the ambiguous datetime).
+
+        :param dt:
+            A timezone-aware :class:`datetime.datetime` object.
+        """
+
+        # Re-implement the algorithm from Python's datetime.py
+        dtoff = dt.utcoffset()
+        if dtoff is None:
+            raise ValueError("fromutc() requires a non-None utcoffset() "
+                             "result")
+
+        # The original datetime.py code assumes that `dst()` defaults to
+        # zero during ambiguous times. PEP 495 inverts this presumption, so
+        # for pre-PEP 495 versions of python, we need to tweak the algorithm.
+        dtdst = dt.dst()
+        if dtdst is None:
+            raise ValueError("fromutc() requires a non-None dst() result")
+        delta = dtoff - dtdst
+
+        dt += delta
+        # Set fold=1 so we can default to being in the fold for
+        # ambiguous dates.
+        dtdst = enfold(dt, fold=1).dst()
+        if dtdst is None:
+            raise ValueError("fromutc(): dt.dst gave inconsistent "
+                             "results; cannot convert")
+        return dt + dtdst
+
+    @_validate_fromutc_inputs
+    def fromutc(self, dt):
+        """
+        Given a timezone-aware datetime in a given timezone, calculates a
+        timezone-aware datetime in a new timezone.
+
+        Since this is the one time that we *know* we have an unambiguous
+        datetime object, we take this opportunity to determine whether the
+        datetime is ambiguous and in a "fold" state (e.g. if it's the first
+        occurrence, chronologically, of the ambiguous datetime).
+
+        :param dt:
+            A timezone-aware :class:`datetime.datetime` object.
+        """
+        dt_wall = self._fromutc(dt)
+
+        # Calculate the fold status given the two datetimes.
+        _fold = self._fold_status(dt, dt_wall)
+
+        # Set the default fold value for ambiguous dates
+        return enfold(dt_wall, fold=_fold)
+
+
+class tzrangebase(_tzinfo):
+    """
+    This is an abstract base class for time zones represented by an annual
+    transition into and out of DST. Child classes should implement the following
+    methods:
+
+        * ``__init__(self, *args, **kwargs)``
+        * ``transitions(self, year)`` - this is expected to return a tuple of
+          datetimes representing the DST on and off transitions in standard
+          time.
+
+    A fully initialized ``tzrangebase`` subclass should also provide the
+    following attributes:
+        * ``hasdst``: Boolean whether or not the zone uses DST.
+        * ``_dst_offset`` / ``_std_offset``: :class:`datetime.timedelta` objects
+          representing the respective UTC offsets.
+        * ``_dst_abbr`` / ``_std_abbr``: Strings representing the timezone short
+          abbreviations in DST and STD, respectively.
+        * ``_hasdst``: Whether or not the zone has DST.
+
+    .. versionadded:: 2.6.0
+    """
+    def __init__(self):
+        raise NotImplementedError('tzrangebase is an abstract base class')
+
+    def utcoffset(self, dt):
+        isdst = self._isdst(dt)
+
+        if isdst is None:
+            return None
+        elif isdst:
+            return self._dst_offset
+        else:
+            return self._std_offset
+
+    def dst(self, dt):
+        isdst = self._isdst(dt)
+
+        if isdst is None:
+            return None
+        elif isdst:
+            return self._dst_base_offset
+        else:
+            return ZERO
+
+    @tzname_in_python2
+    def tzname(self, dt):
+        if self._isdst(dt):
+            return self._dst_abbr
+        else:
+            return self._std_abbr
+
+    def fromutc(self, dt):
+        """ Given a datetime in UTC, return local time """
+        if not isinstance(dt, datetime):
+            raise TypeError("fromutc() requires a datetime argument")
+
+        if dt.tzinfo is not self:
+            raise ValueError("dt.tzinfo is not self")
+
+        # Get transitions - if there are none, fixed offset
+        transitions = self.transitions(dt.year)
+        if transitions is None:
+            return dt + self.utcoffset(dt)
+
+        # Get the transition times in UTC
+        dston, dstoff = transitions
+
+        dston -= self._std_offset
+        dstoff -= self._std_offset
+
+        utc_transitions = (dston, dstoff)
+        dt_utc = dt.replace(tzinfo=None)
+
+        isdst = self._naive_isdst(dt_utc, utc_transitions)
+
+        if isdst:
+            dt_wall = dt + self._dst_offset
+        else:
+            dt_wall = dt + self._std_offset
+
+        _fold = int(not isdst and self.is_ambiguous(dt_wall))
+
+        return enfold(dt_wall, fold=_fold)
+
+    def is_ambiguous(self, dt):
+        """
+        Whether or not the "wall time" of a given datetime is ambiguous in this
+        zone.
+
+        :param dt:
+            A :py:class:`datetime.datetime`, naive or time zone aware.
+
+
+        :return:
+            Returns ``True`` if ambiguous, ``False`` otherwise.
+
+        .. versionadded:: 2.6.0
+        """
+        if not self.hasdst:
+            return False
+
+        start, end = self.transitions(dt.year)
+
+        dt = dt.replace(tzinfo=None)
+        return (end <= dt < end + self._dst_base_offset)
+
+    def _isdst(self, dt):
+        if not self.hasdst:
+            return False
+        elif dt is None:
+            return None
+
+        transitions = self.transitions(dt.year)
+
+        if transitions is None:
+            return False
+
+        dt = dt.replace(tzinfo=None)
+
+        isdst = self._naive_isdst(dt, transitions)
+
+        # Handle ambiguous dates
+        if not isdst and self.is_ambiguous(dt):
+            return not self._fold(dt)
+        else:
+            return isdst
+
+    def _naive_isdst(self, dt, transitions):
+        dston, dstoff = transitions
+
+        dt = dt.replace(tzinfo=None)
+
+        if dston < dstoff:
+            isdst = dston <= dt < dstoff
+        else:
+            isdst = not dstoff <= dt < dston
+
+        return isdst
+
+    @property
+    def _dst_base_offset(self):
+        return self._dst_offset - self._std_offset
+
+    __hash__ = None
+
+    def __ne__(self, other):
+        return not (self == other)
+
+    def __repr__(self):
+        return "%s(...)" % self.__class__.__name__
+
+    __reduce__ = object.__reduce__
diff --git a/crates/hm-dsl-engine/vendor/dateutil/tz/_factories.py b/crates/hm-dsl-engine/vendor/dateutil/tz/_factories.py
new file mode 100644
index 00000000..f8a65891
--- /dev/null
+++ b/crates/hm-dsl-engine/vendor/dateutil/tz/_factories.py
@@ -0,0 +1,80 @@
+from datetime import timedelta
+import weakref
+from collections import OrderedDict
+
+from six.moves import _thread
+
+
+class _TzSingleton(type):
+    def __init__(cls, *args, **kwargs):
+        cls.__instance = None
+        super(_TzSingleton, cls).__init__(*args, **kwargs)
+
+    def __call__(cls):
+        if cls.__instance is None:
+            cls.__instance = super(_TzSingleton, cls).__call__()
+        return cls.__instance
+
+
+class _TzFactory(type):
+    def instance(cls, *args, **kwargs):
+        """Alternate constructor that returns a fresh instance"""
+        return type.__call__(cls, *args, **kwargs)
+
+
+class _TzOffsetFactory(_TzFactory):
+    def __init__(cls, *args, **kwargs):
+        cls.__instances = weakref.WeakValueDictionary()
+        cls.__strong_cache = OrderedDict()
+        cls.__strong_cache_size = 8
+
+        cls._cache_lock = _thread.allocate_lock()
+
+    def __call__(cls, name, offset):
+        if isinstance(offset, timedelta):
+            key = (name, offset.total_seconds())
+        else:
+            key = (name, offset)
+
+        instance = cls.__instances.get(key, None)
+        if instance is None:
+            instance = cls.__instances.setdefault(key,
+                                                  cls.instance(name, offset))
+
+        # This lock may not be necessary in Python 3. See GH issue #901
+        with cls._cache_lock:
+            cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance)
+
+            # Remove an item if the strong cache is overpopulated
+            if len(cls.__strong_cache) > cls.__strong_cache_size:
+                cls.__strong_cache.popitem(last=False)
+
+        return instance
+
+
+class _TzStrFactory(_TzFactory):
+    def __init__(cls, *args, **kwargs):
+        cls.__instances = weakref.WeakValueDictionary()
+        cls.__strong_cache = OrderedDict()
+        cls.__strong_cache_size = 8
+
+        cls.__cache_lock = _thread.allocate_lock()
+
+    def __call__(cls, s, posix_offset=False):
+        key = (s, posix_offset)
+        instance = cls.__instances.get(key, None)
+
+        if instance is None:
+            instance = cls.__instances.setdefault(key,
+                cls.instance(s, posix_offset))
+
+        # This lock may not be necessary in Python 3. See GH issue #901
+        with cls.__cache_lock:
+            cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance)
+
+            # Remove an item if the strong cache is overpopulated
+            if len(cls.__strong_cache) > cls.__strong_cache_size:
+                cls.__strong_cache.popitem(last=False)
+
+        return instance
+
diff --git a/crates/hm-dsl-engine/vendor/dateutil/tz/tz.py b/crates/hm-dsl-engine/vendor/dateutil/tz/tz.py
new file mode 100644
index 00000000..61759144
--- /dev/null
+++ b/crates/hm-dsl-engine/vendor/dateutil/tz/tz.py
@@ -0,0 +1,1849 @@
+# -*- coding: utf-8 -*-
+"""
+This module offers timezone implementations subclassing the abstract
+:py:class:`datetime.tzinfo` type. There are classes to handle tzfile format
+files (usually are in :file:`/etc/localtime`, :file:`/usr/share/zoneinfo`,
+etc), TZ environment string (in all known formats), given ranges (with help
+from relative deltas), local machine timezone, fixed offset timezone, and UTC
+timezone.
+"""
+import datetime
+import struct
+import time
+import sys
+import os
+import bisect
+import weakref
+from collections import OrderedDict
+
+import six
+from six import string_types
+from six.moves import _thread
+from ._common import tzname_in_python2, _tzinfo
+from ._common import tzrangebase, enfold
+from ._common import _validate_fromutc_inputs
+
+from ._factories import _TzSingleton, _TzOffsetFactory
+from ._factories import _TzStrFactory
+try:
+    from .win import tzwin, tzwinlocal
+except ImportError:
+    tzwin = tzwinlocal = None
+
+# For warning about rounding tzinfo
+from warnings import warn
+
+ZERO = datetime.timedelta(0)
+EPOCH = datetime.datetime(1970, 1, 1, 0, 0)
+EPOCHORDINAL = EPOCH.toordinal()
+
+
+@six.add_metaclass(_TzSingleton)
+class tzutc(datetime.tzinfo):
+    """
+    This is a tzinfo object that represents the UTC time zone.
+
+    **Examples:**
+
+    .. doctest::
+
+        >>> from datetime import *
+        >>> from dateutil.tz import *
+
+        >>> datetime.now()
+        datetime.datetime(2003, 9, 27, 9, 40, 1, 521290)
+
+        >>> datetime.now(tzutc())
+        datetime.datetime(2003, 9, 27, 12, 40, 12, 156379, tzinfo=tzutc())
+
+        >>> datetime.now(tzutc()).tzname()
+        'UTC'
+
+    .. versionchanged:: 2.7.0
+        ``tzutc()`` is now a singleton, so the result of ``tzutc()`` will
+        always return the same object.
+
+        .. doctest::
+
+            >>> from dateutil.tz import tzutc, UTC
+            >>> tzutc() is tzutc()
+            True
+            >>> tzutc() is UTC
+            True
+    """
+    def utcoffset(self, dt):
+        return ZERO
+
+    def dst(self, dt):
+        return ZERO
+
+    @tzname_in_python2
+    def tzname(self, dt):
+        return "UTC"
+
+    def is_ambiguous(self, dt):
+        """
+        Whether or not the "wall time" of a given datetime is ambiguous in this
+        zone.
+
+        :param dt:
+            A :py:class:`datetime.datetime`, naive or time zone aware.
+
+
+        :return:
+            Returns ``True`` if ambiguous, ``False`` otherwise.
+
+        .. versionadded:: 2.6.0
+        """
+        return False
+
+    @_validate_fromutc_inputs
+    def fromutc(self, dt):
+        """
+        Fast track version of fromutc() returns the original ``dt`` object for
+        any valid :py:class:`datetime.datetime` object.
+        """
+        return dt
+
+    def __eq__(self, other):
+        if not isinstance(other, (tzutc, tzoffset)):
+            return NotImplemented
+
+        return (isinstance(other, tzutc) or
+                (isinstance(other, tzoffset) and other._offset == ZERO))
+
+    __hash__ = None
+
+    def __ne__(self, other):
+        return not (self == other)
+
+    def __repr__(self):
+        return "%s()" % self.__class__.__name__
+
+    __reduce__ = object.__reduce__
+
+
+#: Convenience constant providing a :class:`tzutc()` instance
+#:
+#: .. versionadded:: 2.7.0
+UTC = tzutc()
+
+
+@six.add_metaclass(_TzOffsetFactory)
+class tzoffset(datetime.tzinfo):
+    """
+    A simple class for representing a fixed offset from UTC.
+
+    :param name:
+        The timezone name, to be returned when ``tzname()`` is called.
+    :param offset:
+        The time zone offset in seconds, or (since version 2.6.0, represented
+        as a :py:class:`datetime.timedelta` object).
+    """
+    def __init__(self, name, offset):
+        self._name = name
+
+        try:
+            # Allow a timedelta
+            offset = offset.total_seconds()
+        except (TypeError, AttributeError):
+            pass
+
+        self._offset = datetime.timedelta(seconds=_get_supported_offset(offset))
+
+    def utcoffset(self, dt):
+        return self._offset
+
+    def dst(self, dt):
+        return ZERO
+
+    @tzname_in_python2
+    def tzname(self, dt):
+        return self._name
+
+    @_validate_fromutc_inputs
+    def fromutc(self, dt):
+        return dt + self._offset
+
+    def is_ambiguous(self, dt):
+        """
+        Whether or not the "wall time" of a given datetime is ambiguous in this
+        zone.
+
+        :param dt:
+            A :py:class:`datetime.datetime`, naive or time zone aware.
+        :return:
+            Returns ``True`` if ambiguous, ``False`` otherwise.
+
+        .. versionadded:: 2.6.0
+        """
+        return False
+
+    def __eq__(self, other):
+        if not isinstance(other, tzoffset):
+            return NotImplemented
+
+        return self._offset == other._offset
+
+    __hash__ = None
+
+    def __ne__(self, other):
+        return not (self == other)
+
+    def __repr__(self):
+        return "%s(%s, %s)" % (self.__class__.__name__,
+                               repr(self._name),
+                               int(self._offset.total_seconds()))
+
+    __reduce__ = object.__reduce__
+
+
+class tzlocal(_tzinfo):
+    """
+    A :class:`tzinfo` subclass built around the ``time`` timezone functions.
+    """
+    def __init__(self):
+        super(tzlocal, self).__init__()
+
+        self._std_offset = datetime.timedelta(seconds=-time.timezone)
+        if time.daylight:
+            self._dst_offset = datetime.timedelta(seconds=-time.altzone)
+        else:
+            self._dst_offset = self._std_offset
+
+        self._dst_saved = self._dst_offset - self._std_offset
+        self._hasdst = bool(self._dst_saved)
+        self._tznames = tuple(time.tzname)
+
+    def utcoffset(self, dt):
+        if dt is None and self._hasdst:
+            return None
+
+        if self._isdst(dt):
+            return self._dst_offset
+        else:
+            return self._std_offset
+
+    def dst(self, dt):
+        if dt is None and self._hasdst:
+            return None
+
+        if self._isdst(dt):
+            return self._dst_offset - self._std_offset
+        else:
+            return ZERO
+
+    @tzname_in_python2
+    def tzname(self, dt):
+        return self._tznames[self._isdst(dt)]
+
+    def is_ambiguous(self, dt):
+        """
+        Whether or not the "wall time" of a given datetime is ambiguous in this
+        zone.
+
+        :param dt:
+            A :py:class:`datetime.datetime`, naive or time zone aware.
+
+
+        :return:
+            Returns ``True`` if ambiguous, ``False`` otherwise.
+
+        .. versionadded:: 2.6.0
+        """
+        naive_dst = self._naive_is_dst(dt)
+        return (not naive_dst and
+                (naive_dst != self._naive_is_dst(dt - self._dst_saved)))
+
+    def _naive_is_dst(self, dt):
+        timestamp = _datetime_to_timestamp(dt)
+        return time.localtime(timestamp + time.timezone).tm_isdst
+
+    def _isdst(self, dt, fold_naive=True):
+        # We can't use mktime here. It is unstable when deciding if
+        # the hour near to a change is DST or not.
+        #
+        # timestamp = time.mktime((dt.year, dt.month, dt.day, dt.hour,
+        #                         dt.minute, dt.second, dt.weekday(), 0, -1))
+        # return time.localtime(timestamp).tm_isdst
+        #
+        # The code above yields the following result:
+        #
+        # >>> import tz, datetime
+        # >>> t = tz.tzlocal()
+        # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
+        # 'BRDT'
+        # >>> datetime.datetime(2003,2,16,0,tzinfo=t).tzname()
+        # 'BRST'
+        # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
+        # 'BRST'
+        # >>> datetime.datetime(2003,2,15,22,tzinfo=t).tzname()
+        # 'BRDT'
+        # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
+        # 'BRDT'
+        #
+        # Here is a more stable implementation:
+        #
+        if not self._hasdst:
+            return False
+
+        # Check for ambiguous times:
+        dstval = self._naive_is_dst(dt)
+        fold = getattr(dt, 'fold', None)
+
+        if self.is_ambiguous(dt):
+            if fold is not None:
+                return not self._fold(dt)
+            else:
+                return True
+
+        return dstval
+
+    def __eq__(self, other):
+        if isinstance(other, tzlocal):
+            return (self._std_offset == other._std_offset and
+                    self._dst_offset == other._dst_offset)
+        elif isinstance(other, tzutc):
+            return (not self._hasdst and
+                    self._tznames[0] in {'UTC', 'GMT'} and
+                    self._std_offset == ZERO)
+        elif isinstance(other, tzoffset):
+            return (not self._hasdst and
+                    self._tznames[0] == other._name and
+                    self._std_offset == other._offset)
+        else:
+            return NotImplemented
+
+    __hash__ = None
+
+    def __ne__(self, other):
+        return not (self == other)
+
+    def __repr__(self):
+        return "%s()" % self.__class__.__name__
+
+    __reduce__ = object.__reduce__
+
+
+class _ttinfo(object):
+    __slots__ = ["offset", "delta", "isdst", "abbr",
+                 "isstd", "isgmt", "dstoffset"]
+
+    def __init__(self):
+        for attr in self.__slots__:
+            setattr(self, attr, None)
+
+    def __repr__(self):
+        l = []
+        for attr in self.__slots__:
+            value = getattr(self, attr)
+            if value is not None:
+                l.append("%s=%s" % (attr, repr(value)))
+        return "%s(%s)" % (self.__class__.__name__, ", ".join(l))
+
+    def __eq__(self, other):
+        if not isinstance(other, _ttinfo):
+            return NotImplemented
+
+        return (self.offset == other.offset and
+                self.delta == other.delta and
+                self.isdst == other.isdst and
+                self.abbr == other.abbr and
+                self.isstd == other.isstd and
+                self.isgmt == other.isgmt and
+                self.dstoffset == other.dstoffset)
+
+    __hash__ = None
+
+    def __ne__(self, other):
+        return not (self == other)
+
+    def __getstate__(self):
+        state = {}
+        for name in self.__slots__:
+            state[name] = getattr(self, name, None)
+        return state
+
+    def __setstate__(self, state):
+        for name in self.__slots__:
+            if name in state:
+                setattr(self, name, state[name])
+
+
+class _tzfile(object):
+    """
+    Lightweight class for holding the relevant transition and time zone
+    information read from binary tzfiles.
+    """
+    attrs = ['trans_list', 'trans_list_utc', 'trans_idx', 'ttinfo_list',
+             'ttinfo_std', 'ttinfo_dst', 'ttinfo_before', 'ttinfo_first']
+
+    def __init__(self, **kwargs):
+        for attr in self.attrs:
+            setattr(self, attr, kwargs.get(attr, None))
+
+
+class tzfile(_tzinfo):
+    """
+    This is a ``tzinfo`` subclass that allows one to use the ``tzfile(5)``
+    format timezone files to extract current and historical zone information.
+
+    :param fileobj:
+        This can be an opened file stream or a file name that the time zone
+        information can be read from.
+
+    :param filename:
+        This is an optional parameter specifying the source of the time zone
+        information in the event that ``fileobj`` is a file object. If omitted
+        and ``fileobj`` is a file stream, this parameter will be set either to
+        ``fileobj``'s ``name`` attribute or to ``repr(fileobj)``.
+
+    See `Sources for Time Zone and Daylight Saving Time Data
+    `_ for more information.
+    Time zone files can be compiled from the `IANA Time Zone database files
+    `_ with the `zic time zone compiler
+    `_
+
+    .. note::
+
+        Only construct a ``tzfile`` directly if you have a specific timezone
+        file on disk that you want to read into a Python ``tzinfo`` object.
+        If you want to get a ``tzfile`` representing a specific IANA zone,
+        (e.g. ``'America/New_York'``), you should call
+        :func:`dateutil.tz.gettz` with the zone identifier.
+
+
+    **Examples:**
+
+    Using the US Eastern time zone as an example, we can see that a ``tzfile``
+    provides time zone information for the standard Daylight Saving offsets:
+
+    .. testsetup:: tzfile
+
+        from dateutil.tz import gettz
+        from datetime import datetime
+
+    .. doctest:: tzfile
+
+        >>> NYC = gettz('America/New_York')
+        >>> NYC
+        tzfile('/usr/share/zoneinfo/America/New_York')
+
+        >>> print(datetime(2016, 1, 3, tzinfo=NYC))     # EST
+        2016-01-03 00:00:00-05:00
+
+        >>> print(datetime(2016, 7, 7, tzinfo=NYC))     # EDT
+        2016-07-07 00:00:00-04:00
+
+
+    The ``tzfile`` structure contains a fully history of the time zone,
+    so historical dates will also have the right offsets. For example, before
+    the adoption of the UTC standards, New York used local solar  mean time:
+
+    .. doctest:: tzfile
+
+       >>> print(datetime(1901, 4, 12, tzinfo=NYC))    # LMT
+       1901-04-12 00:00:00-04:56
+
+    And during World War II, New York was on "Eastern War Time", which was a
+    state of permanent daylight saving time:
+
+    .. doctest:: tzfile
+
+        >>> print(datetime(1944, 2, 7, tzinfo=NYC))    # EWT
+        1944-02-07 00:00:00-04:00
+
+    """
+
+    def __init__(self, fileobj, filename=None):
+        super(tzfile, self).__init__()
+
+        file_opened_here = False
+        if isinstance(fileobj, string_types):
+            self._filename = fileobj
+            fileobj = open(fileobj, 'rb')
+            file_opened_here = True
+        elif filename is not None:
+            self._filename = filename
+        elif hasattr(fileobj, "name"):
+            self._filename = fileobj.name
+        else:
+            self._filename = repr(fileobj)
+
+        if fileobj is not None:
+            if not file_opened_here:
+                fileobj = _nullcontext(fileobj)
+
+            with fileobj as file_stream:
+                tzobj = self._read_tzfile(file_stream)
+
+            self._set_tzdata(tzobj)
+
+    def _set_tzdata(self, tzobj):
+        """ Set the time zone data of this object from a _tzfile object """
+        # Copy the relevant attributes over as private attributes
+        for attr in _tzfile.attrs:
+            setattr(self, '_' + attr, getattr(tzobj, attr))
+
+    def _read_tzfile(self, fileobj):
+        out = _tzfile()
+
+        # From tzfile(5):
+        #
+        # The time zone information files used by tzset(3)
+        # begin with the magic characters "TZif" to identify
+        # them as time zone information files, followed by
+        # sixteen bytes reserved for future use, followed by
+        # six four-byte values of type long, written in a
+        # ``standard'' byte order (the high-order  byte
+        # of the value is written first).
+        if fileobj.read(4).decode() != "TZif":
+            raise ValueError("magic not found")
+
+        fileobj.read(16)
+
+        (
+            # The number of UTC/local indicators stored in the file.
+            ttisgmtcnt,
+
+            # The number of standard/wall indicators stored in the file.
+            ttisstdcnt,
+
+            # The number of leap seconds for which data is
+            # stored in the file.
+            leapcnt,
+
+            # The number of "transition times" for which data
+            # is stored in the file.
+            timecnt,
+
+            # The number of "local time types" for which data
+            # is stored in the file (must not be zero).
+            typecnt,
+
+            # The  number  of  characters  of "time zone
+            # abbreviation strings" stored in the file.
+            charcnt,
+
+        ) = struct.unpack(">6l", fileobj.read(24))
+
+        # The above header is followed by tzh_timecnt four-byte
+        # values  of  type long,  sorted  in ascending order.
+        # These values are written in ``standard'' byte order.
+        # Each is used as a transition time (as  returned  by
+        # time(2)) at which the rules for computing local time
+        # change.
+
+        if timecnt:
+            out.trans_list_utc = list(struct.unpack(">%dl" % timecnt,
+                                                    fileobj.read(timecnt*4)))
+        else:
+            out.trans_list_utc = []
+
+        # Next come tzh_timecnt one-byte values of type unsigned
+        # char; each one tells which of the different types of
+        # ``local time'' types described in the file is associated
+        # with the same-indexed transition time. These values
+        # serve as indices into an array of ttinfo structures that
+        # appears next in the file.
+
+        if timecnt:
+            out.trans_idx = struct.unpack(">%dB" % timecnt,
+                                          fileobj.read(timecnt))
+        else:
+            out.trans_idx = []
+
+        # Each ttinfo structure is written as a four-byte value
+        # for tt_gmtoff  of  type long,  in  a  standard  byte
+        # order, followed  by a one-byte value for tt_isdst
+        # and a one-byte  value  for  tt_abbrind.   In  each
+        # structure, tt_gmtoff  gives  the  number  of
+        # seconds to be added to UTC, tt_isdst tells whether
+        # tm_isdst should be set by  localtime(3),  and
+        # tt_abbrind serves  as an index into the array of
+        # time zone abbreviation characters that follow the
+        # ttinfo structure(s) in the file.
+
+        ttinfo = []
+
+        for i in range(typecnt):
+            ttinfo.append(struct.unpack(">lbb", fileobj.read(6)))
+
+        abbr = fileobj.read(charcnt).decode()
+
+        # Then there are tzh_leapcnt pairs of four-byte
+        # values, written in  standard byte  order;  the
+        # first  value  of  each pair gives the time (as
+        # returned by time(2)) at which a leap second
+        # occurs;  the  second  gives the  total  number of
+        # leap seconds to be applied after the given time.
+        # The pairs of values are sorted in ascending order
+        # by time.
+
+        # Not used, for now (but seek for correct file position)
+        if leapcnt:
+            fileobj.seek(leapcnt * 8, os.SEEK_CUR)
+
+        # Then there are tzh_ttisstdcnt standard/wall
+        # indicators, each stored as a one-byte value;
+        # they tell whether the transition times associated
+        # with local time types were specified as standard
+        # time or wall clock time, and are used when
+        # a time zone file is used in handling POSIX-style
+        # time zone environment variables.
+
+        if ttisstdcnt:
+            isstd = struct.unpack(">%db" % ttisstdcnt,
+                                  fileobj.read(ttisstdcnt))
+
+        # Finally, there are tzh_ttisgmtcnt UTC/local
+        # indicators, each stored as a one-byte value;
+        # they tell whether the transition times associated
+        # with local time types were specified as UTC or
+        # local time, and are used when a time zone file
+        # is used in handling POSIX-style time zone envi-
+        # ronment variables.
+
+        if ttisgmtcnt:
+            isgmt = struct.unpack(">%db" % ttisgmtcnt,
+                                  fileobj.read(ttisgmtcnt))
+
+        # Build ttinfo list
+        out.ttinfo_list = []
+        for i in range(typecnt):
+            gmtoff, isdst, abbrind = ttinfo[i]
+            gmtoff = _get_supported_offset(gmtoff)
+            tti = _ttinfo()
+            tti.offset = gmtoff
+            tti.dstoffset = datetime.timedelta(0)
+            tti.delta = datetime.timedelta(seconds=gmtoff)
+            tti.isdst = isdst
+            tti.abbr = abbr[abbrind:abbr.find('\x00', abbrind)]
+            tti.isstd = (ttisstdcnt > i and isstd[i] != 0)
+            tti.isgmt = (ttisgmtcnt > i and isgmt[i] != 0)
+            out.ttinfo_list.append(tti)
+
+        # Replace ttinfo indexes for ttinfo objects.
+        out.trans_idx = [out.ttinfo_list[idx] for idx in out.trans_idx]
+
+        # Set standard, dst, and before ttinfos. before will be
+        # used when a given time is before any transitions,
+        # and will be set to the first non-dst ttinfo, or to
+        # the first dst, if all of them are dst.
+        out.ttinfo_std = None
+        out.ttinfo_dst = None
+        out.ttinfo_before = None
+        if out.ttinfo_list:
+            if not out.trans_list_utc:
+                out.ttinfo_std = out.ttinfo_first = out.ttinfo_list[0]
+            else:
+                for i in range(timecnt-1, -1, -1):
+                    tti = out.trans_idx[i]
+                    if not out.ttinfo_std and not tti.isdst:
+                        out.ttinfo_std = tti
+                    elif not out.ttinfo_dst and tti.isdst:
+                        out.ttinfo_dst = tti
+
+                    if out.ttinfo_std and out.ttinfo_dst:
+                        break
+                else:
+                    if out.ttinfo_dst and not out.ttinfo_std:
+                        out.ttinfo_std = out.ttinfo_dst
+
+                for tti in out.ttinfo_list:
+                    if not tti.isdst:
+                        out.ttinfo_before = tti
+                        break
+                else:
+                    out.ttinfo_before = out.ttinfo_list[0]
+
+        # Now fix transition times to become relative to wall time.
+        #
+        # I'm not sure about this. In my tests, the tz source file
+        # is setup to wall time, and in the binary file isstd and
+        # isgmt are off, so it should be in wall time. OTOH, it's
+        # always in gmt time. Let me know if you have comments
+        # about this.
+        lastdst = None
+        lastoffset = None
+        lastdstoffset = None
+        lastbaseoffset = None
+        out.trans_list = []
+
+        for i, tti in enumerate(out.trans_idx):
+            offset = tti.offset
+            dstoffset = 0
+
+            if lastdst is not None:
+                if tti.isdst:
+                    if not lastdst:
+                        dstoffset = offset - lastoffset
+
+                    if not dstoffset and lastdstoffset:
+                        dstoffset = lastdstoffset
+
+                    tti.dstoffset = datetime.timedelta(seconds=dstoffset)
+                    lastdstoffset = dstoffset
+
+            # If a time zone changes its base offset during a DST transition,
+            # then you need to adjust by the previous base offset to get the
+            # transition time in local time. Otherwise you use the current
+            # base offset. Ideally, I would have some mathematical proof of
+            # why this is true, but I haven't really thought about it enough.
+            baseoffset = offset - dstoffset
+            adjustment = baseoffset
+            if (lastbaseoffset is not None and baseoffset != lastbaseoffset
+                    and tti.isdst != lastdst):
+                # The base DST has changed
+                adjustment = lastbaseoffset
+
+            lastdst = tti.isdst
+            lastoffset = offset
+            lastbaseoffset = baseoffset
+
+            out.trans_list.append(out.trans_list_utc[i] + adjustment)
+
+        out.trans_idx = tuple(out.trans_idx)
+        out.trans_list = tuple(out.trans_list)
+        out.trans_list_utc = tuple(out.trans_list_utc)
+
+        return out
+
+    def _find_last_transition(self, dt, in_utc=False):
+        # If there's no list, there are no transitions to find
+        if not self._trans_list:
+            return None
+
+        timestamp = _datetime_to_timestamp(dt)
+
+        # Find where the timestamp fits in the transition list - if the
+        # timestamp is a transition time, it's part of the "after" period.
+        trans_list = self._trans_list_utc if in_utc else self._trans_list
+        idx = bisect.bisect_right(trans_list, timestamp)
+
+        # We want to know when the previous transition was, so subtract off 1
+        return idx - 1
+
+    def _get_ttinfo(self, idx):
+        # For no list or after the last transition, default to _ttinfo_std
+        if idx is None or (idx + 1) >= len(self._trans_list):
+            return self._ttinfo_std
+
+        # If there is a list and the time is before it, return _ttinfo_before
+        if idx < 0:
+            return self._ttinfo_before
+
+        return self._trans_idx[idx]
+
+    def _find_ttinfo(self, dt):
+        idx = self._resolve_ambiguous_time(dt)
+
+        return self._get_ttinfo(idx)
+
+    def fromutc(self, dt):
+        """
+        The ``tzfile`` implementation of :py:func:`datetime.tzinfo.fromutc`.
+
+        :param dt:
+            A :py:class:`datetime.datetime` object.
+
+        :raises TypeError:
+            Raised if ``dt`` is not a :py:class:`datetime.datetime` object.
+
+        :raises ValueError:
+            Raised if this is called with a ``dt`` which does not have this
+            ``tzinfo`` attached.
+
+        :return:
+            Returns a :py:class:`datetime.datetime` object representing the
+            wall time in ``self``'s time zone.
+        """
+        # These isinstance checks are in datetime.tzinfo, so we'll preserve
+        # them, even if we don't care about duck typing.
+        if not isinstance(dt, datetime.datetime):
+            raise TypeError("fromutc() requires a datetime argument")
+
+        if dt.tzinfo is not self:
+            raise ValueError("dt.tzinfo is not self")
+
+        # First treat UTC as wall time and get the transition we're in.
+        idx = self._find_last_transition(dt, in_utc=True)
+        tti = self._get_ttinfo(idx)
+
+        dt_out = dt + datetime.timedelta(seconds=tti.offset)
+
+        fold = self.is_ambiguous(dt_out, idx=idx)
+
+        return enfold(dt_out, fold=int(fold))
+
+    def is_ambiguous(self, dt, idx=None):
+        """
+        Whether or not the "wall time" of a given datetime is ambiguous in this
+        zone.
+
+        :param dt:
+            A :py:class:`datetime.datetime`, naive or time zone aware.
+
+
+        :return:
+            Returns ``True`` if ambiguous, ``False`` otherwise.
+
+        .. versionadded:: 2.6.0
+        """
+        if idx is None:
+            idx = self._find_last_transition(dt)
+
+        # Calculate the difference in offsets from current to previous
+        timestamp = _datetime_to_timestamp(dt)
+        tti = self._get_ttinfo(idx)
+
+        if idx is None or idx <= 0:
+            return False
+
+        od = self._get_ttinfo(idx - 1).offset - tti.offset
+        tt = self._trans_list[idx]          # Transition time
+
+        return timestamp < tt + od
+
+    def _resolve_ambiguous_time(self, dt):
+        idx = self._find_last_transition(dt)
+
+        # If we have no transitions, return the index
+        _fold = self._fold(dt)
+        if idx is None or idx == 0:
+            return idx
+
+        # If it's ambiguous and we're in a fold, shift to a different index.
+        idx_offset = int(not _fold and self.is_ambiguous(dt, idx))
+
+        return idx - idx_offset
+
+    def utcoffset(self, dt):
+        if dt is None:
+            return None
+
+        if not self._ttinfo_std:
+            return ZERO
+
+        return self._find_ttinfo(dt).delta
+
+    def dst(self, dt):
+        if dt is None:
+            return None
+
+        if not self._ttinfo_dst:
+            return ZERO
+
+        tti = self._find_ttinfo(dt)
+
+        if not tti.isdst:
+            return ZERO
+
+        # The documentation says that utcoffset()-dst() must
+        # be constant for every dt.
+        return tti.dstoffset
+
+    @tzname_in_python2
+    def tzname(self, dt):
+        if not self._ttinfo_std or dt is None:
+            return None
+        return self._find_ttinfo(dt).abbr
+
+    def __eq__(self, other):
+        if not isinstance(other, tzfile):
+            return NotImplemented
+        return (self._trans_list == other._trans_list and
+                self._trans_idx == other._trans_idx and
+                self._ttinfo_list == other._ttinfo_list)
+
+    __hash__ = None
+
+    def __ne__(self, other):
+        return not (self == other)
+
+    def __repr__(self):
+        return "%s(%s)" % (self.__class__.__name__, repr(self._filename))
+
+    def __reduce__(self):
+        return self.__reduce_ex__(None)
+
+    def __reduce_ex__(self, protocol):
+        return (self.__class__, (None, self._filename), self.__dict__)
+
+
+class tzrange(tzrangebase):
+    """
+    The ``tzrange`` object is a time zone specified by a set of offsets and
+    abbreviations, equivalent to the way the ``TZ`` variable can be specified
+    in POSIX-like systems, but using Python delta objects to specify DST
+    start, end and offsets.
+
+    :param stdabbr:
+        The abbreviation for standard time (e.g. ``'EST'``).
+
+    :param stdoffset:
+        An integer or :class:`datetime.timedelta` object or equivalent
+        specifying the base offset from UTC.
+
+        If unspecified, +00:00 is used.
+
+    :param dstabbr:
+        The abbreviation for DST / "Summer" time (e.g. ``'EDT'``).
+
+        If specified, with no other DST information, DST is assumed to occur
+        and the default behavior or ``dstoffset``, ``start`` and ``end`` is
+        used. If unspecified and no other DST information is specified, it
+        is assumed that this zone has no DST.
+
+        If this is unspecified and other DST information is *is* specified,
+        DST occurs in the zone but the time zone abbreviation is left
+        unchanged.
+
+    :param dstoffset:
+        A an integer or :class:`datetime.timedelta` object or equivalent
+        specifying the UTC offset during DST. If unspecified and any other DST
+        information is specified, it is assumed to be the STD offset +1 hour.
+
+    :param start:
+        A :class:`relativedelta.relativedelta` object or equivalent specifying
+        the time and time of year that daylight savings time starts. To
+        specify, for example, that DST starts at 2AM on the 2nd Sunday in
+        March, pass:
+
+            ``relativedelta(hours=2, month=3, day=1, weekday=SU(+2))``
+
+        If unspecified and any other DST information is specified, the default
+        value is 2 AM on the first Sunday in April.
+
+    :param end:
+        A :class:`relativedelta.relativedelta` object or equivalent
+        representing the time and time of year that daylight savings time
+        ends, with the same specification method as in ``start``. One note is
+        that this should point to the first time in the *standard* zone, so if
+        a transition occurs at 2AM in the DST zone and the clocks are set back
+        1 hour to 1AM, set the ``hours`` parameter to +1.
+
+
+    **Examples:**
+
+    .. testsetup:: tzrange
+
+        from dateutil.tz import tzrange, tzstr
+
+    .. doctest:: tzrange
+
+        >>> tzstr('EST5EDT') == tzrange("EST", -18000, "EDT")
+        True
+
+        >>> from dateutil.relativedelta import *
+        >>> range1 = tzrange("EST", -18000, "EDT")
+        >>> range2 = tzrange("EST", -18000, "EDT", -14400,
+        ...                  relativedelta(hours=+2, month=4, day=1,
+        ...                                weekday=SU(+1)),
+        ...                  relativedelta(hours=+1, month=10, day=31,
+        ...                                weekday=SU(-1)))
+        >>> tzstr('EST5EDT') == range1 == range2
+        True
+
+    """
+    def __init__(self, stdabbr, stdoffset=None,
+                 dstabbr=None, dstoffset=None,
+                 start=None, end=None):
+
+        global relativedelta
+        from dateutil import relativedelta
+
+        self._std_abbr = stdabbr
+        self._dst_abbr = dstabbr
+
+        try:
+            stdoffset = stdoffset.total_seconds()
+        except (TypeError, AttributeError):
+            pass
+
+        try:
+            dstoffset = dstoffset.total_seconds()
+        except (TypeError, AttributeError):
+            pass
+
+        if stdoffset is not None:
+            self._std_offset = datetime.timedelta(seconds=stdoffset)
+        else:
+            self._std_offset = ZERO
+
+        if dstoffset is not None:
+            self._dst_offset = datetime.timedelta(seconds=dstoffset)
+        elif dstabbr and stdoffset is not None:
+            self._dst_offset = self._std_offset + datetime.timedelta(hours=+1)
+        else:
+            self._dst_offset = ZERO
+
+        if dstabbr and start is None:
+            self._start_delta = relativedelta.relativedelta(
+                hours=+2, month=4, day=1, weekday=relativedelta.SU(+1))
+        else:
+            self._start_delta = start
+
+        if dstabbr and end is None:
+            self._end_delta = relativedelta.relativedelta(
+                hours=+1, month=10, day=31, weekday=relativedelta.SU(-1))
+        else:
+            self._end_delta = end
+
+        self._dst_base_offset_ = self._dst_offset - self._std_offset
+        self.hasdst = bool(self._start_delta)
+
+    def transitions(self, year):
+        """
+        For a given year, get the DST on and off transition times, expressed
+        always on the standard time side. For zones with no transitions, this
+        function returns ``None``.
+
+        :param year:
+            The year whose transitions you would like to query.
+
+        :return:
+            Returns a :class:`tuple` of :class:`datetime.datetime` objects,
+            ``(dston, dstoff)`` for zones with an annual DST transition, or
+            ``None`` for fixed offset zones.
+        """
+        if not self.hasdst:
+            return None
+
+        base_year = datetime.datetime(year, 1, 1)
+
+        start = base_year + self._start_delta
+        end = base_year + self._end_delta
+
+        return (start, end)
+
+    def __eq__(self, other):
+        if not isinstance(other, tzrange):
+            return NotImplemented
+
+        return (self._std_abbr == other._std_abbr and
+                self._dst_abbr == other._dst_abbr and
+                self._std_offset == other._std_offset and
+                self._dst_offset == other._dst_offset and
+                self._start_delta == other._start_delta and
+                self._end_delta == other._end_delta)
+
+    @property
+    def _dst_base_offset(self):
+        return self._dst_base_offset_
+
+
+@six.add_metaclass(_TzStrFactory)
+class tzstr(tzrange):
+    """
+    ``tzstr`` objects are time zone objects specified by a time-zone string as
+    it would be passed to a ``TZ`` variable on POSIX-style systems (see
+    the `GNU C Library: TZ Variable`_ for more details).
+
+    There is one notable exception, which is that POSIX-style time zones use an
+    inverted offset format, so normally ``GMT+3`` would be parsed as an offset
+    3 hours *behind* GMT. The ``tzstr`` time zone object will parse this as an
+    offset 3 hours *ahead* of GMT. If you would like to maintain the POSIX
+    behavior, pass a ``True`` value to ``posix_offset``.
+
+    The :class:`tzrange` object provides the same functionality, but is
+    specified using :class:`relativedelta.relativedelta` objects. rather than
+    strings.
+
+    :param s:
+        A time zone string in ``TZ`` variable format. This can be a
+        :class:`bytes` (2.x: :class:`str`), :class:`str` (2.x:
+        :class:`unicode`) or a stream emitting unicode characters
+        (e.g. :class:`StringIO`).
+
+    :param posix_offset:
+        Optional. If set to ``True``, interpret strings such as ``GMT+3`` or
+        ``UTC+3`` as being 3 hours *behind* UTC rather than ahead, per the
+        POSIX standard.
+
+    .. caution::
+
+        Prior to version 2.7.0, this function also supported time zones
+        in the format:
+
+            * ``EST5EDT,4,0,6,7200,10,0,26,7200,3600``
+            * ``EST5EDT,4,1,0,7200,10,-1,0,7200,3600``
+
+        This format is non-standard and has been deprecated; this function
+        will raise a :class:`DeprecatedTZFormatWarning` until
+        support is removed in a future version.
+
+    .. _`GNU C Library: TZ Variable`:
+        https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html
+    """
+    def __init__(self, s, posix_offset=False):
+        global parser
+        from dateutil.parser import _parser as parser
+
+        self._s = s
+
+        res = parser._parsetz(s)
+        if res is None or res.any_unused_tokens:
+            raise ValueError("unknown string format")
+
+        # Here we break the compatibility with the TZ variable handling.
+        # GMT-3 actually *means* the timezone -3.
+        if res.stdabbr in ("GMT", "UTC") and not posix_offset:
+            res.stdoffset *= -1
+
+        # We must initialize it first, since _delta() needs
+        # _std_offset and _dst_offset set. Use False in start/end
+        # to avoid building it two times.
+        tzrange.__init__(self, res.stdabbr, res.stdoffset,
+                         res.dstabbr, res.dstoffset,
+                         start=False, end=False)
+
+        if not res.dstabbr:
+            self._start_delta = None
+            self._end_delta = None
+        else:
+            self._start_delta = self._delta(res.start)
+            if self._start_delta:
+                self._end_delta = self._delta(res.end, isend=1)
+
+        self.hasdst = bool(self._start_delta)
+
+    def _delta(self, x, isend=0):
+        from dateutil import relativedelta
+        kwargs = {}
+        if x.month is not None:
+            kwargs["month"] = x.month
+            if x.weekday is not None:
+                kwargs["weekday"] = relativedelta.weekday(x.weekday, x.week)
+                if x.week > 0:
+                    kwargs["day"] = 1
+                else:
+                    kwargs["day"] = 31
+            elif x.day:
+                kwargs["day"] = x.day
+        elif x.yday is not None:
+            kwargs["yearday"] = x.yday
+        elif x.jyday is not None:
+            kwargs["nlyearday"] = x.jyday
+        if not kwargs:
+            # Default is to start on first sunday of april, and end
+            # on last sunday of october.
+            if not isend:
+                kwargs["month"] = 4
+                kwargs["day"] = 1
+                kwargs["weekday"] = relativedelta.SU(+1)
+            else:
+                kwargs["month"] = 10
+                kwargs["day"] = 31
+                kwargs["weekday"] = relativedelta.SU(-1)
+        if x.time is not None:
+            kwargs["seconds"] = x.time
+        else:
+            # Default is 2AM.
+            kwargs["seconds"] = 7200
+        if isend:
+            # Convert to standard time, to follow the documented way
+            # of working with the extra hour. See the documentation
+            # of the tzinfo class.
+            delta = self._dst_offset - self._std_offset
+            kwargs["seconds"] -= delta.seconds + delta.days * 86400
+        return relativedelta.relativedelta(**kwargs)
+
+    def __repr__(self):
+        return "%s(%s)" % (self.__class__.__name__, repr(self._s))
+
+
+class _tzicalvtzcomp(object):
+    def __init__(self, tzoffsetfrom, tzoffsetto, isdst,
+                 tzname=None, rrule=None):
+        self.tzoffsetfrom = datetime.timedelta(seconds=tzoffsetfrom)
+        self.tzoffsetto = datetime.timedelta(seconds=tzoffsetto)
+        self.tzoffsetdiff = self.tzoffsetto - self.tzoffsetfrom
+        self.isdst = isdst
+        self.tzname = tzname
+        self.rrule = rrule
+
+
+class _tzicalvtz(_tzinfo):
+    def __init__(self, tzid, comps=[]):
+        super(_tzicalvtz, self).__init__()
+
+        self._tzid = tzid
+        self._comps = comps
+        self._cachedate = []
+        self._cachecomp = []
+        self._cache_lock = _thread.allocate_lock()
+
+    def _find_comp(self, dt):
+        if len(self._comps) == 1:
+            return self._comps[0]
+
+        dt = dt.replace(tzinfo=None)
+
+        try:
+            with self._cache_lock:
+                return self._cachecomp[self._cachedate.index(
+                    (dt, self._fold(dt)))]
+        except ValueError:
+            pass
+
+        lastcompdt = None
+        lastcomp = None
+
+        for comp in self._comps:
+            compdt = self._find_compdt(comp, dt)
+
+            if compdt and (not lastcompdt or lastcompdt < compdt):
+                lastcompdt = compdt
+                lastcomp = comp
+
+        if not lastcomp:
+            # RFC says nothing about what to do when a given
+            # time is before the first onset date. We'll look for the
+            # first standard component, or the first component, if
+            # none is found.
+            for comp in self._comps:
+                if not comp.isdst:
+                    lastcomp = comp
+                    break
+            else:
+                lastcomp = comp[0]
+
+        with self._cache_lock:
+            self._cachedate.insert(0, (dt, self._fold(dt)))
+            self._cachecomp.insert(0, lastcomp)
+
+            if len(self._cachedate) > 10:
+                self._cachedate.pop()
+                self._cachecomp.pop()
+
+        return lastcomp
+
+    def _find_compdt(self, comp, dt):
+        if comp.tzoffsetdiff < ZERO and self._fold(dt):
+            dt -= comp.tzoffsetdiff
+
+        compdt = comp.rrule.before(dt, inc=True)
+
+        return compdt
+
+    def utcoffset(self, dt):
+        if dt is None:
+            return None
+
+        return self._find_comp(dt).tzoffsetto
+
+    def dst(self, dt):
+        comp = self._find_comp(dt)
+        if comp.isdst:
+            return comp.tzoffsetdiff
+        else:
+            return ZERO
+
+    @tzname_in_python2
+    def tzname(self, dt):
+        return self._find_comp(dt).tzname
+
+    def __repr__(self):
+        return "" % repr(self._tzid)
+
+    __reduce__ = object.__reduce__
+
+
+class tzical(object):
+    """
+    This object is designed to parse an iCalendar-style ``VTIMEZONE`` structure
+    as set out in `RFC 5545`_ Section 4.6.5 into one or more `tzinfo` objects.
+
+    :param `fileobj`:
+        A file or stream in iCalendar format, which should be UTF-8 encoded
+        with CRLF endings.
+
+    .. _`RFC 5545`: https://tools.ietf.org/html/rfc5545
+    """
+    def __init__(self, fileobj):
+        global rrule
+        from dateutil import rrule
+
+        if isinstance(fileobj, string_types):
+            self._s = fileobj
+            # ical should be encoded in UTF-8 with CRLF
+            fileobj = open(fileobj, 'r')
+        else:
+            self._s = getattr(fileobj, 'name', repr(fileobj))
+            fileobj = _nullcontext(fileobj)
+
+        self._vtz = {}
+
+        with fileobj as fobj:
+            self._parse_rfc(fobj.read())
+
+    def keys(self):
+        """
+        Retrieves the available time zones as a list.
+        """
+        return list(self._vtz.keys())
+
+    def get(self, tzid=None):
+        """
+        Retrieve a :py:class:`datetime.tzinfo` object by its ``tzid``.
+
+        :param tzid:
+            If there is exactly one time zone available, omitting ``tzid``
+            or passing :py:const:`None` value returns it. Otherwise a valid
+            key (which can be retrieved from :func:`keys`) is required.
+
+        :raises ValueError:
+            Raised if ``tzid`` is not specified but there are either more
+            or fewer than 1 zone defined.
+
+        :returns:
+            Returns either a :py:class:`datetime.tzinfo` object representing
+            the relevant time zone or :py:const:`None` if the ``tzid`` was
+            not found.
+        """
+        if tzid is None:
+            if len(self._vtz) == 0:
+                raise ValueError("no timezones defined")
+            elif len(self._vtz) > 1:
+                raise ValueError("more than one timezone available")
+            tzid = next(iter(self._vtz))
+
+        return self._vtz.get(tzid)
+
+    def _parse_offset(self, s):
+        s = s.strip()
+        if not s:
+            raise ValueError("empty offset")
+        if s[0] in ('+', '-'):
+            signal = (-1, +1)[s[0] == '+']
+            s = s[1:]
+        else:
+            signal = +1
+        if len(s) == 4:
+            return (int(s[:2]) * 3600 + int(s[2:]) * 60) * signal
+        elif len(s) == 6:
+            return (int(s[:2]) * 3600 + int(s[2:4]) * 60 + int(s[4:])) * signal
+        else:
+            raise ValueError("invalid offset: " + s)
+
+    def _parse_rfc(self, s):
+        lines = s.splitlines()
+        if not lines:
+            raise ValueError("empty string")
+
+        # Unfold
+        i = 0
+        while i < len(lines):
+            line = lines[i].rstrip()
+            if not line:
+                del lines[i]
+            elif i > 0 and line[0] == " ":
+                lines[i-1] += line[1:]
+                del lines[i]
+            else:
+                i += 1
+
+        tzid = None
+        comps = []
+        invtz = False
+        comptype = None
+        for line in lines:
+            if not line:
+                continue
+            name, value = line.split(':', 1)
+            parms = name.split(';')
+            if not parms:
+                raise ValueError("empty property name")
+            name = parms[0].upper()
+            parms = parms[1:]
+            if invtz:
+                if name == "BEGIN":
+                    if value in ("STANDARD", "DAYLIGHT"):
+                        # Process component
+                        pass
+                    else:
+                        raise ValueError("unknown component: "+value)
+                    comptype = value
+                    founddtstart = False
+                    tzoffsetfrom = None
+                    tzoffsetto = None
+                    rrulelines = []
+                    tzname = None
+                elif name == "END":
+                    if value == "VTIMEZONE":
+                        if comptype:
+                            raise ValueError("component not closed: "+comptype)
+                        if not tzid:
+                            raise ValueError("mandatory TZID not found")
+                        if not comps:
+                            raise ValueError(
+                                "at least one component is needed")
+                        # Process vtimezone
+                        self._vtz[tzid] = _tzicalvtz(tzid, comps)
+                        invtz = False
+                    elif value == comptype:
+                        if not founddtstart:
+                            raise ValueError("mandatory DTSTART not found")
+                        if tzoffsetfrom is None:
+                            raise ValueError(
+                                "mandatory TZOFFSETFROM not found")
+                        if tzoffsetto is None:
+                            raise ValueError(
+                                "mandatory TZOFFSETFROM not found")
+                        # Process component
+                        rr = None
+                        if rrulelines:
+                            rr = rrule.rrulestr("\n".join(rrulelines),
+                                                compatible=True,
+                                                ignoretz=True,
+                                                cache=True)
+                        comp = _tzicalvtzcomp(tzoffsetfrom, tzoffsetto,
+                                              (comptype == "DAYLIGHT"),
+                                              tzname, rr)
+                        comps.append(comp)
+                        comptype = None
+                    else:
+                        raise ValueError("invalid component end: "+value)
+                elif comptype:
+                    if name == "DTSTART":
+                        # DTSTART in VTIMEZONE takes a subset of valid RRULE
+                        # values under RFC 5545.
+                        for parm in parms:
+                            if parm != 'VALUE=DATE-TIME':
+                                msg = ('Unsupported DTSTART param in ' +
+                                       'VTIMEZONE: ' + parm)
+                                raise ValueError(msg)
+                        rrulelines.append(line)
+                        founddtstart = True
+                    elif name in ("RRULE", "RDATE", "EXRULE", "EXDATE"):
+                        rrulelines.append(line)
+                    elif name == "TZOFFSETFROM":
+                        if parms:
+                            raise ValueError(
+                                "unsupported %s parm: %s " % (name, parms[0]))
+                        tzoffsetfrom = self._parse_offset(value)
+                    elif name == "TZOFFSETTO":
+                        if parms:
+                            raise ValueError(
+                                "unsupported TZOFFSETTO parm: "+parms[0])
+                        tzoffsetto = self._parse_offset(value)
+                    elif name == "TZNAME":
+                        if parms:
+                            raise ValueError(
+                                "unsupported TZNAME parm: "+parms[0])
+                        tzname = value
+                    elif name == "COMMENT":
+                        pass
+                    else:
+                        raise ValueError("unsupported property: "+name)
+                else:
+                    if name == "TZID":
+                        if parms:
+                            raise ValueError(
+                                "unsupported TZID parm: "+parms[0])
+                        tzid = value
+                    elif name in ("TZURL", "LAST-MODIFIED", "COMMENT"):
+                        pass
+                    else:
+                        raise ValueError("unsupported property: "+name)
+            elif name == "BEGIN" and value == "VTIMEZONE":
+                tzid = None
+                comps = []
+                invtz = True
+
+    def __repr__(self):
+        return "%s(%s)" % (self.__class__.__name__, repr(self._s))
+
+
+if sys.platform != "win32":
+    TZFILES = ["/etc/localtime", "localtime"]
+    TZPATHS = ["/usr/share/zoneinfo",
+               "/usr/lib/zoneinfo",
+               "/usr/share/lib/zoneinfo",
+               "/etc/zoneinfo"]
+else:
+    TZFILES = []
+    TZPATHS = []
+
+
+def __get_gettz():
+    tzlocal_classes = (tzlocal,)
+    if tzwinlocal is not None:
+        tzlocal_classes += (tzwinlocal,)
+
+    class GettzFunc(object):
+        """
+        Retrieve a time zone object from a string representation
+
+        This function is intended to retrieve the :py:class:`tzinfo` subclass
+        that best represents the time zone that would be used if a POSIX
+        `TZ variable`_ were set to the same value.
+
+        If no argument or an empty string is passed to ``gettz``, local time
+        is returned:
+
+        .. code-block:: python3
+
+            >>> gettz()
+            tzfile('/etc/localtime')
+
+        This function is also the preferred way to map IANA tz database keys
+        to :class:`tzfile` objects:
+
+        .. code-block:: python3
+
+            >>> gettz('Pacific/Kiritimati')
+            tzfile('/usr/share/zoneinfo/Pacific/Kiritimati')
+
+        On Windows, the standard is extended to include the Windows-specific
+        zone names provided by the operating system:
+
+        .. code-block:: python3
+
+            >>> gettz('Egypt Standard Time')
+            tzwin('Egypt Standard Time')
+
+        Passing a GNU ``TZ`` style string time zone specification returns a
+        :class:`tzstr` object:
+
+        .. code-block:: python3
+
+            >>> gettz('AEST-10AEDT-11,M10.1.0/2,M4.1.0/3')
+            tzstr('AEST-10AEDT-11,M10.1.0/2,M4.1.0/3')
+
+        :param name:
+            A time zone name (IANA, or, on Windows, Windows keys), location of
+            a ``tzfile(5)`` zoneinfo file or ``TZ`` variable style time zone
+            specifier. An empty string, no argument or ``None`` is interpreted
+            as local time.
+
+        :return:
+            Returns an instance of one of ``dateutil``'s :py:class:`tzinfo`
+            subclasses.
+
+        .. versionchanged:: 2.7.0
+
+            After version 2.7.0, any two calls to ``gettz`` using the same
+            input strings will return the same object:
+
+            .. code-block:: python3
+
+                >>> tz.gettz('America/Chicago') is tz.gettz('America/Chicago')
+                True
+
+            In addition to improving performance, this ensures that
+            `"same zone" semantics`_ are used for datetimes in the same zone.
+
+
+        .. _`TZ variable`:
+            https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html
+
+        .. _`"same zone" semantics`:
+            https://blog.ganssle.io/articles/2018/02/aware-datetime-arithmetic.html
+        """
+        def __init__(self):
+
+            self.__instances = weakref.WeakValueDictionary()
+            self.__strong_cache_size = 8
+            self.__strong_cache = OrderedDict()
+            self._cache_lock = _thread.allocate_lock()
+
+        def __call__(self, name=None):
+            with self._cache_lock:
+                rv = self.__instances.get(name, None)
+
+                if rv is None:
+                    rv = self.nocache(name=name)
+                    if not (name is None
+                            or isinstance(rv, tzlocal_classes)
+                            or rv is None):
+                        # tzlocal is slightly more complicated than the other
+                        # time zone providers because it depends on environment
+                        # at construction time, so don't cache that.
+                        #
+                        # We also cannot store weak references to None, so we
+                        # will also not store that.
+                        self.__instances[name] = rv
+                    else:
+                        # No need for strong caching, return immediately
+                        return rv
+
+                self.__strong_cache[name] = self.__strong_cache.pop(name, rv)
+
+                if len(self.__strong_cache) > self.__strong_cache_size:
+                    self.__strong_cache.popitem(last=False)
+
+            return rv
+
+        def set_cache_size(self, size):
+            with self._cache_lock:
+                self.__strong_cache_size = size
+                while len(self.__strong_cache) > size:
+                    self.__strong_cache.popitem(last=False)
+
+        def cache_clear(self):
+            with self._cache_lock:
+                self.__instances = weakref.WeakValueDictionary()
+                self.__strong_cache.clear()
+
+        @staticmethod
+        def nocache(name=None):
+            """A non-cached version of gettz"""
+            tz = None
+            if not name:
+                try:
+                    name = os.environ["TZ"]
+                except KeyError:
+                    pass
+            if name is None or name in ("", ":"):
+                for filepath in TZFILES:
+                    if not os.path.isabs(filepath):
+                        filename = filepath
+                        for path in TZPATHS:
+                            filepath = os.path.join(path, filename)
+                            if os.path.isfile(filepath):
+                                break
+                        else:
+                            continue
+                    if os.path.isfile(filepath):
+                        try:
+                            tz = tzfile(filepath)
+                            break
+                        except (IOError, OSError, ValueError):
+                            pass
+                else:
+                    tz = tzlocal()
+            else:
+                try:
+                    if name.startswith(":"):
+                        name = name[1:]
+                except TypeError as e:
+                    if isinstance(name, bytes):
+                        new_msg = "gettz argument should be str, not bytes"
+                        six.raise_from(TypeError(new_msg), e)
+                    else:
+                        raise
+                if os.path.isabs(name):
+                    if os.path.isfile(name):
+                        tz = tzfile(name)
+                    else:
+                        tz = None
+                else:
+                    for path in TZPATHS:
+                        filepath = os.path.join(path, name)
+                        if not os.path.isfile(filepath):
+                            filepath = filepath.replace(' ', '_')
+                            if not os.path.isfile(filepath):
+                                continue
+                        try:
+                            tz = tzfile(filepath)
+                            break
+                        except (IOError, OSError, ValueError):
+                            pass
+                    else:
+                        tz = None
+                        if tzwin is not None:
+                            try:
+                                tz = tzwin(name)
+                            except (WindowsError, UnicodeEncodeError):
+                                # UnicodeEncodeError is for Python 2.7 compat
+                                tz = None
+
+                        if not tz:
+                            from dateutil.zoneinfo import get_zonefile_instance
+                            tz = get_zonefile_instance().get(name)
+
+                        if not tz:
+                            for c in name:
+                                # name is not a tzstr unless it has at least
+                                # one offset. For short values of "name", an
+                                # explicit for loop seems to be the fastest way
+                                # To determine if a string contains a digit
+                                if c in "0123456789":
+                                    try:
+                                        tz = tzstr(name)
+                                    except ValueError:
+                                        pass
+                                    break
+                            else:
+                                if name in ("GMT", "UTC"):
+                                    tz = UTC
+                                elif name in time.tzname:
+                                    tz = tzlocal()
+            return tz
+
+    return GettzFunc()
+
+
+gettz = __get_gettz()
+del __get_gettz
+
+
+def datetime_exists(dt, tz=None):
+    """
+    Given a datetime and a time zone, determine whether or not a given datetime
+    would fall in a gap.
+
+    :param dt:
+        A :class:`datetime.datetime` (whose time zone will be ignored if ``tz``
+        is provided.)
+
+    :param tz:
+        A :class:`datetime.tzinfo` with support for the ``fold`` attribute. If
+        ``None`` or not provided, the datetime's own time zone will be used.
+
+    :return:
+        Returns a boolean value whether or not the "wall time" exists in
+        ``tz``.
+
+    .. versionadded:: 2.7.0
+    """
+    if tz is None:
+        if dt.tzinfo is None:
+            raise ValueError('Datetime is naive and no time zone provided.')
+        tz = dt.tzinfo
+
+    dt = dt.replace(tzinfo=None)
+
+    # This is essentially a test of whether or not the datetime can survive
+    # a round trip to UTC.
+    dt_rt = dt.replace(tzinfo=tz).astimezone(UTC).astimezone(tz)
+    dt_rt = dt_rt.replace(tzinfo=None)
+
+    return dt == dt_rt
+
+
+def datetime_ambiguous(dt, tz=None):
+    """
+    Given a datetime and a time zone, determine whether or not a given datetime
+    is ambiguous (i.e if there are two times differentiated only by their DST
+    status).
+
+    :param dt:
+        A :class:`datetime.datetime` (whose time zone will be ignored if ``tz``
+        is provided.)
+
+    :param tz:
+        A :class:`datetime.tzinfo` with support for the ``fold`` attribute. If
+        ``None`` or not provided, the datetime's own time zone will be used.
+
+    :return:
+        Returns a boolean value whether or not the "wall time" is ambiguous in
+        ``tz``.
+
+    .. versionadded:: 2.6.0
+    """
+    if tz is None:
+        if dt.tzinfo is None:
+            raise ValueError('Datetime is naive and no time zone provided.')
+
+        tz = dt.tzinfo
+
+    # If a time zone defines its own "is_ambiguous" function, we'll use that.
+    is_ambiguous_fn = getattr(tz, 'is_ambiguous', None)
+    if is_ambiguous_fn is not None:
+        try:
+            return tz.is_ambiguous(dt)
+        except Exception:
+            pass
+
+    # If it doesn't come out and tell us it's ambiguous, we'll just check if
+    # the fold attribute has any effect on this particular date and time.
+    dt = dt.replace(tzinfo=tz)
+    wall_0 = enfold(dt, fold=0)
+    wall_1 = enfold(dt, fold=1)
+
+    same_offset = wall_0.utcoffset() == wall_1.utcoffset()
+    same_dst = wall_0.dst() == wall_1.dst()
+
+    return not (same_offset and same_dst)
+
+
+def resolve_imaginary(dt):
+    """
+    Given a datetime that may be imaginary, return an existing datetime.
+
+    This function assumes that an imaginary datetime represents what the
+    wall time would be in a zone had the offset transition not occurred, so
+    it will always fall forward by the transition's change in offset.
+
+    .. doctest::
+
+        >>> from dateutil import tz
+        >>> from datetime import datetime
+        >>> NYC = tz.gettz('America/New_York')
+        >>> print(tz.resolve_imaginary(datetime(2017, 3, 12, 2, 30, tzinfo=NYC)))
+        2017-03-12 03:30:00-04:00
+
+        >>> KIR = tz.gettz('Pacific/Kiritimati')
+        >>> print(tz.resolve_imaginary(datetime(1995, 1, 1, 12, 30, tzinfo=KIR)))
+        1995-01-02 12:30:00+14:00
+
+    As a note, :func:`datetime.astimezone` is guaranteed to produce a valid,
+    existing datetime, so a round-trip to and from UTC is sufficient to get
+    an extant datetime, however, this generally "falls back" to an earlier time
+    rather than falling forward to the STD side (though no guarantees are made
+    about this behavior).
+
+    :param dt:
+        A :class:`datetime.datetime` which may or may not exist.
+
+    :return:
+        Returns an existing :class:`datetime.datetime`. If ``dt`` was not
+        imaginary, the datetime returned is guaranteed to be the same object
+        passed to the function.
+
+    .. versionadded:: 2.7.0
+    """
+    if dt.tzinfo is not None and not datetime_exists(dt):
+
+        curr_offset = (dt + datetime.timedelta(hours=24)).utcoffset()
+        old_offset = (dt - datetime.timedelta(hours=24)).utcoffset()
+
+        dt += curr_offset - old_offset
+
+    return dt
+
+
+def _datetime_to_timestamp(dt):
+    """
+    Convert a :class:`datetime.datetime` object to an epoch timestamp in
+    seconds since January 1, 1970, ignoring the time zone.
+    """
+    return (dt.replace(tzinfo=None) - EPOCH).total_seconds()
+
+
+if sys.version_info >= (3, 6):
+    def _get_supported_offset(second_offset):
+        return second_offset
+else:
+    def _get_supported_offset(second_offset):
+        # For python pre-3.6, round to full-minutes if that's not the case.
+        # Python's datetime doesn't accept sub-minute timezones. Check
+        # http://python.org/sf/1447945 or https://bugs.python.org/issue5288
+        # for some information.
+        old_offset = second_offset
+        calculated_offset = 60 * ((second_offset + 30) // 60)
+        return calculated_offset
+
+
+try:
+    # Python 3.7 feature
+    from contextlib import nullcontext as _nullcontext
+except ImportError:
+    class _nullcontext(object):
+        """
+        Class for wrapping contexts so that they are passed through in a
+        with statement.
+        """
+        def __init__(self, context):
+            self.context = context
+
+        def __enter__(self):
+            return self.context
+
+        def __exit__(*args, **kwargs):
+            pass
+
+# vim:ts=4:sw=4:et
diff --git a/crates/hm-dsl-engine/vendor/dateutil/tz/win.py b/crates/hm-dsl-engine/vendor/dateutil/tz/win.py
new file mode 100644
index 00000000..cde07ba7
--- /dev/null
+++ b/crates/hm-dsl-engine/vendor/dateutil/tz/win.py
@@ -0,0 +1,370 @@
+# -*- coding: utf-8 -*-
+"""
+This module provides an interface to the native time zone data on Windows,
+including :py:class:`datetime.tzinfo` implementations.
+
+Attempting to import this module on a non-Windows platform will raise an
+:py:obj:`ImportError`.
+"""
+# This code was originally contributed by Jeffrey Harris.
+import datetime
+import struct
+
+from six.moves import winreg
+from six import text_type
+
+try:
+    import ctypes
+    from ctypes import wintypes
+except ValueError:
+    # ValueError is raised on non-Windows systems for some horrible reason.
+    raise ImportError("Running tzwin on non-Windows system")
+
+from ._common import tzrangebase
+
+__all__ = ["tzwin", "tzwinlocal", "tzres"]
+
+ONEWEEK = datetime.timedelta(7)
+
+TZKEYNAMENT = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones"
+TZKEYNAME9X = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Time Zones"
+TZLOCALKEYNAME = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation"
+
+
+def _settzkeyname():
+    handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
+    try:
+        winreg.OpenKey(handle, TZKEYNAMENT).Close()
+        TZKEYNAME = TZKEYNAMENT
+    except WindowsError:
+        TZKEYNAME = TZKEYNAME9X
+    handle.Close()
+    return TZKEYNAME
+
+
+TZKEYNAME = _settzkeyname()
+
+
+class tzres(object):
+    """
+    Class for accessing ``tzres.dll``, which contains timezone name related
+    resources.
+
+    .. versionadded:: 2.5.0
+    """
+    p_wchar = ctypes.POINTER(wintypes.WCHAR)        # Pointer to a wide char
+
+    def __init__(self, tzres_loc='tzres.dll'):
+        # Load the user32 DLL so we can load strings from tzres
+        user32 = ctypes.WinDLL('user32')
+
+        # Specify the LoadStringW function
+        user32.LoadStringW.argtypes = (wintypes.HINSTANCE,
+                                       wintypes.UINT,
+                                       wintypes.LPWSTR,
+                                       ctypes.c_int)
+
+        self.LoadStringW = user32.LoadStringW
+        self._tzres = ctypes.WinDLL(tzres_loc)
+        self.tzres_loc = tzres_loc
+
+    def load_name(self, offset):
+        """
+        Load a timezone name from a DLL offset (integer).
+
+        >>> from dateutil.tzwin import tzres
+        >>> tzr = tzres()
+        >>> print(tzr.load_name(112))
+        'Eastern Standard Time'
+
+        :param offset:
+            A positive integer value referring to a string from the tzres dll.
+
+        .. note::
+
+            Offsets found in the registry are generally of the form
+            ``@tzres.dll,-114``. The offset in this case is 114, not -114.
+
+        """
+        resource = self.p_wchar()
+        lpBuffer = ctypes.cast(ctypes.byref(resource), wintypes.LPWSTR)
+        nchar = self.LoadStringW(self._tzres._handle, offset, lpBuffer, 0)
+        return resource[:nchar]
+
+    def name_from_string(self, tzname_str):
+        """
+        Parse strings as returned from the Windows registry into the time zone
+        name as defined in the registry.
+
+        >>> from dateutil.tzwin import tzres
+        >>> tzr = tzres()
+        >>> print(tzr.name_from_string('@tzres.dll,-251'))
+        'Dateline Daylight Time'
+        >>> print(tzr.name_from_string('Eastern Standard Time'))
+        'Eastern Standard Time'
+
+        :param tzname_str:
+            A timezone name string as returned from a Windows registry key.
+
+        :return:
+            Returns the localized timezone string from tzres.dll if the string
+            is of the form `@tzres.dll,-offset`, else returns the input string.
+        """
+        if not tzname_str.startswith('@'):
+            return tzname_str
+
+        name_splt = tzname_str.split(',-')
+        try:
+            offset = int(name_splt[1])
+        except:
+            raise ValueError("Malformed timezone string.")
+
+        return self.load_name(offset)
+
+
+class tzwinbase(tzrangebase):
+    """tzinfo class based on win32's timezones available in the registry."""
+    def __init__(self):
+        raise NotImplementedError('tzwinbase is an abstract base class')
+
+    def __eq__(self, other):
+        # Compare on all relevant dimensions, including name.
+        if not isinstance(other, tzwinbase):
+            return NotImplemented
+
+        return  (self._std_offset == other._std_offset and
+                 self._dst_offset == other._dst_offset and
+                 self._stddayofweek == other._stddayofweek and
+                 self._dstdayofweek == other._dstdayofweek and
+                 self._stdweeknumber == other._stdweeknumber and
+                 self._dstweeknumber == other._dstweeknumber and
+                 self._stdhour == other._stdhour and
+                 self._dsthour == other._dsthour and
+                 self._stdminute == other._stdminute and
+                 self._dstminute == other._dstminute and
+                 self._std_abbr == other._std_abbr and
+                 self._dst_abbr == other._dst_abbr)
+
+    @staticmethod
+    def list():
+        """Return a list of all time zones known to the system."""
+        with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
+            with winreg.OpenKey(handle, TZKEYNAME) as tzkey:
+                result = [winreg.EnumKey(tzkey, i)
+                          for i in range(winreg.QueryInfoKey(tzkey)[0])]
+        return result
+
+    def display(self):
+        """
+        Return the display name of the time zone.
+        """
+        return self._display
+
+    def transitions(self, year):
+        """
+        For a given year, get the DST on and off transition times, expressed
+        always on the standard time side. For zones with no transitions, this
+        function returns ``None``.
+
+        :param year:
+            The year whose transitions you would like to query.
+
+        :return:
+            Returns a :class:`tuple` of :class:`datetime.datetime` objects,
+            ``(dston, dstoff)`` for zones with an annual DST transition, or
+            ``None`` for fixed offset zones.
+        """
+
+        if not self.hasdst:
+            return None
+
+        dston = picknthweekday(year, self._dstmonth, self._dstdayofweek,
+                               self._dsthour, self._dstminute,
+                               self._dstweeknumber)
+
+        dstoff = picknthweekday(year, self._stdmonth, self._stddayofweek,
+                                self._stdhour, self._stdminute,
+                                self._stdweeknumber)
+
+        # Ambiguous dates default to the STD side
+        dstoff -= self._dst_base_offset
+
+        return dston, dstoff
+
+    def _get_hasdst(self):
+        return self._dstmonth != 0
+
+    @property
+    def _dst_base_offset(self):
+        return self._dst_base_offset_
+
+
+class tzwin(tzwinbase):
+    """
+    Time zone object created from the zone info in the Windows registry
+
+    These are similar to :py:class:`dateutil.tz.tzrange` objects in that
+    the time zone data is provided in the format of a single offset rule
+    for either 0 or 2 time zone transitions per year.
+
+    :param: name
+        The name of a Windows time zone key, e.g. "Eastern Standard Time".
+        The full list of keys can be retrieved with :func:`tzwin.list`.
+    """
+
+    def __init__(self, name):
+        self._name = name
+
+        with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
+            tzkeyname = text_type("{kn}\\{name}").format(kn=TZKEYNAME, name=name)
+            with winreg.OpenKey(handle, tzkeyname) as tzkey:
+                keydict = valuestodict(tzkey)
+
+        self._std_abbr = keydict["Std"]
+        self._dst_abbr = keydict["Dlt"]
+
+        self._display = keydict["Display"]
+
+        # See http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm
+        tup = struct.unpack("=3l16h", keydict["TZI"])
+        stdoffset = -tup[0]-tup[1]          # Bias + StandardBias * -1
+        dstoffset = stdoffset-tup[2]        # + DaylightBias * -1
+        self._std_offset = datetime.timedelta(minutes=stdoffset)
+        self._dst_offset = datetime.timedelta(minutes=dstoffset)
+
+        # for the meaning see the win32 TIME_ZONE_INFORMATION structure docs
+        # http://msdn.microsoft.com/en-us/library/windows/desktop/ms725481(v=vs.85).aspx
+        (self._stdmonth,
+         self._stddayofweek,   # Sunday = 0
+         self._stdweeknumber,  # Last = 5
+         self._stdhour,
+         self._stdminute) = tup[4:9]
+
+        (self._dstmonth,
+         self._dstdayofweek,   # Sunday = 0
+         self._dstweeknumber,  # Last = 5
+         self._dsthour,
+         self._dstminute) = tup[12:17]
+
+        self._dst_base_offset_ = self._dst_offset - self._std_offset
+        self.hasdst = self._get_hasdst()
+
+    def __repr__(self):
+        return "tzwin(%s)" % repr(self._name)
+
+    def __reduce__(self):
+        return (self.__class__, (self._name,))
+
+
+class tzwinlocal(tzwinbase):
+    """
+    Class representing the local time zone information in the Windows registry
+
+    While :class:`dateutil.tz.tzlocal` makes system calls (via the :mod:`time`
+    module) to retrieve time zone information, ``tzwinlocal`` retrieves the
+    rules directly from the Windows registry and creates an object like
+    :class:`dateutil.tz.tzwin`.
+
+    Because Windows does not have an equivalent of :func:`time.tzset`, on
+    Windows, :class:`dateutil.tz.tzlocal` instances will always reflect the
+    time zone settings *at the time that the process was started*, meaning
+    changes to the machine's time zone settings during the run of a program
+    on Windows will **not** be reflected by :class:`dateutil.tz.tzlocal`.
+    Because ``tzwinlocal`` reads the registry directly, it is unaffected by
+    this issue.
+    """
+    def __init__(self):
+        with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
+            with winreg.OpenKey(handle, TZLOCALKEYNAME) as tzlocalkey:
+                keydict = valuestodict(tzlocalkey)
+
+            self._std_abbr = keydict["StandardName"]
+            self._dst_abbr = keydict["DaylightName"]
+
+            try:
+                tzkeyname = text_type('{kn}\\{sn}').format(kn=TZKEYNAME,
+                                                          sn=self._std_abbr)
+                with winreg.OpenKey(handle, tzkeyname) as tzkey:
+                    _keydict = valuestodict(tzkey)
+                    self._display = _keydict["Display"]
+            except OSError:
+                self._display = None
+
+        stdoffset = -keydict["Bias"]-keydict["StandardBias"]
+        dstoffset = stdoffset-keydict["DaylightBias"]
+
+        self._std_offset = datetime.timedelta(minutes=stdoffset)
+        self._dst_offset = datetime.timedelta(minutes=dstoffset)
+
+        # For reasons unclear, in this particular key, the day of week has been
+        # moved to the END of the SYSTEMTIME structure.
+        tup = struct.unpack("=8h", keydict["StandardStart"])
+
+        (self._stdmonth,
+         self._stdweeknumber,  # Last = 5
+         self._stdhour,
+         self._stdminute) = tup[1:5]
+
+        self._stddayofweek = tup[7]
+
+        tup = struct.unpack("=8h", keydict["DaylightStart"])
+
+        (self._dstmonth,
+         self._dstweeknumber,  # Last = 5
+         self._dsthour,
+         self._dstminute) = tup[1:5]
+
+        self._dstdayofweek = tup[7]
+
+        self._dst_base_offset_ = self._dst_offset - self._std_offset
+        self.hasdst = self._get_hasdst()
+
+    def __repr__(self):
+        return "tzwinlocal()"
+
+    def __str__(self):
+        # str will return the standard name, not the daylight name.
+        return "tzwinlocal(%s)" % repr(self._std_abbr)
+
+    def __reduce__(self):
+        return (self.__class__, ())
+
+
+def picknthweekday(year, month, dayofweek, hour, minute, whichweek):
+    """ dayofweek == 0 means Sunday, whichweek 5 means last instance """
+    first = datetime.datetime(year, month, 1, hour, minute)
+
+    # This will work if dayofweek is ISO weekday (1-7) or Microsoft-style (0-6),
+    # Because 7 % 7 = 0
+    weekdayone = first.replace(day=((dayofweek - first.isoweekday()) % 7) + 1)
+    wd = weekdayone + ((whichweek - 1) * ONEWEEK)
+    if (wd.month != month):
+        wd -= ONEWEEK
+
+    return wd
+
+
+def valuestodict(key):
+    """Convert a registry key's values to a dictionary."""
+    dout = {}
+    size = winreg.QueryInfoKey(key)[1]
+    tz_res = None
+
+    for i in range(size):
+        key_name, value, dtype = winreg.EnumValue(key, i)
+        if dtype == winreg.REG_DWORD or dtype == winreg.REG_DWORD_LITTLE_ENDIAN:
+            # If it's a DWORD (32-bit integer), it's stored as unsigned - convert
+            # that to a proper signed integer
+            if value & (1 << 31):
+                value = value - (1 << 32)
+        elif dtype == winreg.REG_SZ:
+            # If it's a reference to the tzres DLL, load the actual string
+            if value.startswith('@tzres'):
+                tz_res = tz_res or tzres()
+                value = tz_res.name_from_string(value)
+
+            value = value.rstrip('\x00')    # Remove trailing nulls
+
+        dout[key_name] = value
+
+    return dout
diff --git a/crates/hm-dsl-engine/vendor/dateutil/tzwin.py b/crates/hm-dsl-engine/vendor/dateutil/tzwin.py
new file mode 100644
index 00000000..cebc673e
--- /dev/null
+++ b/crates/hm-dsl-engine/vendor/dateutil/tzwin.py
@@ -0,0 +1,2 @@
+# tzwin has moved to dateutil.tz.win
+from .tz.win import *
diff --git a/crates/hm-dsl-engine/vendor/dateutil/utils.py b/crates/hm-dsl-engine/vendor/dateutil/utils.py
new file mode 100644
index 00000000..dd2d245a
--- /dev/null
+++ b/crates/hm-dsl-engine/vendor/dateutil/utils.py
@@ -0,0 +1,71 @@
+# -*- coding: utf-8 -*-
+"""
+This module offers general convenience and utility functions for dealing with
+datetimes.
+
+.. versionadded:: 2.7.0
+"""
+from __future__ import unicode_literals
+
+from datetime import datetime, time
+
+
+def today(tzinfo=None):
+    """
+    Returns a :py:class:`datetime` representing the current day at midnight
+
+    :param tzinfo:
+        The time zone to attach (also used to determine the current day).
+
+    :return:
+        A :py:class:`datetime.datetime` object representing the current day
+        at midnight.
+    """
+
+    dt = datetime.now(tzinfo)
+    return datetime.combine(dt.date(), time(0, tzinfo=tzinfo))
+
+
+def default_tzinfo(dt, tzinfo):
+    """
+    Sets the ``tzinfo`` parameter on naive datetimes only
+
+    This is useful for example when you are provided a datetime that may have
+    either an implicit or explicit time zone, such as when parsing a time zone
+    string.
+
+    .. doctest::
+
+        >>> from dateutil.tz import tzoffset
+        >>> from dateutil.parser import parse
+        >>> from dateutil.utils import default_tzinfo
+        >>> dflt_tz = tzoffset("EST", -18000)
+        >>> print(default_tzinfo(parse('2014-01-01 12:30 UTC'), dflt_tz))
+        2014-01-01 12:30:00+00:00
+        >>> print(default_tzinfo(parse('2014-01-01 12:30'), dflt_tz))
+        2014-01-01 12:30:00-05:00
+
+    :param dt:
+        The datetime on which to replace the time zone
+
+    :param tzinfo:
+        The :py:class:`datetime.tzinfo` subclass instance to assign to
+        ``dt`` if (and only if) it is naive.
+
+    :return:
+        Returns an aware :py:class:`datetime.datetime`.
+    """
+    if dt.tzinfo is not None:
+        return dt
+    else:
+        return dt.replace(tzinfo=tzinfo)
+
+
+def within_delta(dt1, dt2, delta):
+    """
+    Useful for comparing two datetimes that may have a negligible difference
+    to be considered equal.
+    """
+    delta = abs(delta)
+    difference = dt1 - dt2
+    return -delta <= difference <= delta
diff --git a/crates/hm-dsl-engine/vendor/dateutil/zoneinfo/__init__.py b/crates/hm-dsl-engine/vendor/dateutil/zoneinfo/__init__.py
new file mode 100644
index 00000000..34f11ad6
--- /dev/null
+++ b/crates/hm-dsl-engine/vendor/dateutil/zoneinfo/__init__.py
@@ -0,0 +1,167 @@
+# -*- coding: utf-8 -*-
+import warnings
+import json
+
+from tarfile import TarFile
+from pkgutil import get_data
+from io import BytesIO
+
+from dateutil.tz import tzfile as _tzfile
+
+__all__ = ["get_zonefile_instance", "gettz", "gettz_db_metadata"]
+
+ZONEFILENAME = "dateutil-zoneinfo.tar.gz"
+METADATA_FN = 'METADATA'
+
+
+class tzfile(_tzfile):
+    def __reduce__(self):
+        return (gettz, (self._filename,))
+
+
+def getzoneinfofile_stream():
+    try:
+        return BytesIO(get_data(__name__, ZONEFILENAME))
+    except IOError as e:  # TODO  switch to FileNotFoundError?
+        warnings.warn("I/O error({0}): {1}".format(e.errno, e.strerror))
+        return None
+
+
+class ZoneInfoFile(object):
+    def __init__(self, zonefile_stream=None):
+        if zonefile_stream is not None:
+            with TarFile.open(fileobj=zonefile_stream) as tf:
+                self.zones = {zf.name: tzfile(tf.extractfile(zf), filename=zf.name)
+                              for zf in tf.getmembers()
+                              if zf.isfile() and zf.name != METADATA_FN}
+                # deal with links: They'll point to their parent object. Less
+                # waste of memory
+                links = {zl.name: self.zones[zl.linkname]
+                         for zl in tf.getmembers() if
+                         zl.islnk() or zl.issym()}
+                self.zones.update(links)
+                try:
+                    metadata_json = tf.extractfile(tf.getmember(METADATA_FN))
+                    metadata_str = metadata_json.read().decode('UTF-8')
+                    self.metadata = json.loads(metadata_str)
+                except KeyError:
+                    # no metadata in tar file
+                    self.metadata = None
+        else:
+            self.zones = {}
+            self.metadata = None
+
+    def get(self, name, default=None):
+        """
+        Wrapper for :func:`ZoneInfoFile.zones.get`. This is a convenience method
+        for retrieving zones from the zone dictionary.
+
+        :param name:
+            The name of the zone to retrieve. (Generally IANA zone names)
+
+        :param default:
+            The value to return in the event of a missing key.
+
+        .. versionadded:: 2.6.0
+
+        """
+        return self.zones.get(name, default)
+
+
+# The current API has gettz as a module function, although in fact it taps into
+# a stateful class. So as a workaround for now, without changing the API, we
+# will create a new "global" class instance the first time a user requests a
+# timezone. Ugly, but adheres to the api.
+#
+# TODO: Remove after deprecation period.
+_CLASS_ZONE_INSTANCE = []
+
+
+def get_zonefile_instance(new_instance=False):
+    """
+    This is a convenience function which provides a :class:`ZoneInfoFile`
+    instance using the data provided by the ``dateutil`` package. By default, it
+    caches a single instance of the ZoneInfoFile object and returns that.
+
+    :param new_instance:
+        If ``True``, a new instance of :class:`ZoneInfoFile` is instantiated and
+        used as the cached instance for the next call. Otherwise, new instances
+        are created only as necessary.
+
+    :return:
+        Returns a :class:`ZoneInfoFile` object.
+
+    .. versionadded:: 2.6
+    """
+    if new_instance:
+        zif = None
+    else:
+        zif = getattr(get_zonefile_instance, '_cached_instance', None)
+
+    if zif is None:
+        zif = ZoneInfoFile(getzoneinfofile_stream())
+
+        get_zonefile_instance._cached_instance = zif
+
+    return zif
+
+
+def gettz(name):
+    """
+    This retrieves a time zone from the local zoneinfo tarball that is packaged
+    with dateutil.
+
+    :param name:
+        An IANA-style time zone name, as found in the zoneinfo file.
+
+    :return:
+        Returns a :class:`dateutil.tz.tzfile` time zone object.
+
+    .. warning::
+        It is generally inadvisable to use this function, and it is only
+        provided for API compatibility with earlier versions. This is *not*
+        equivalent to ``dateutil.tz.gettz()``, which selects an appropriate
+        time zone based on the inputs, favoring system zoneinfo. This is ONLY
+        for accessing the dateutil-specific zoneinfo (which may be out of
+        date compared to the system zoneinfo).
+
+    .. deprecated:: 2.6
+        If you need to use a specific zoneinfofile over the system zoneinfo,
+        instantiate a :class:`dateutil.zoneinfo.ZoneInfoFile` object and call
+        :func:`dateutil.zoneinfo.ZoneInfoFile.get(name)` instead.
+
+        Use :func:`get_zonefile_instance` to retrieve an instance of the
+        dateutil-provided zoneinfo.
+    """
+    warnings.warn("zoneinfo.gettz() will be removed in future versions, "
+                  "to use the dateutil-provided zoneinfo files, instantiate a "
+                  "ZoneInfoFile object and use ZoneInfoFile.zones.get() "
+                  "instead. See the documentation for details.",
+                  DeprecationWarning)
+
+    if len(_CLASS_ZONE_INSTANCE) == 0:
+        _CLASS_ZONE_INSTANCE.append(ZoneInfoFile(getzoneinfofile_stream()))
+    return _CLASS_ZONE_INSTANCE[0].zones.get(name)
+
+
+def gettz_db_metadata():
+    """ Get the zonefile metadata
+
+    See `zonefile_metadata`_
+
+    :returns:
+        A dictionary with the database metadata
+
+    .. deprecated:: 2.6
+        See deprecation warning in :func:`zoneinfo.gettz`. To get metadata,
+        query the attribute ``zoneinfo.ZoneInfoFile.metadata``.
+    """
+    warnings.warn("zoneinfo.gettz_db_metadata() will be removed in future "
+                  "versions, to use the dateutil-provided zoneinfo files, "
+                  "ZoneInfoFile object and query the 'metadata' attribute "
+                  "instead. See the documentation for details.",
+                  DeprecationWarning)
+
+    if len(_CLASS_ZONE_INSTANCE) == 0:
+        _CLASS_ZONE_INSTANCE.append(ZoneInfoFile(getzoneinfofile_stream()))
+    return _CLASS_ZONE_INSTANCE[0].metadata
diff --git a/crates/hm-dsl-engine/vendor/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz b/crates/hm-dsl-engine/vendor/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz
new file mode 100644
index 0000000000000000000000000000000000000000..1461f8c862df8c3eae75e0ea376788b5b2602269
GIT binary patch
literal 156400
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 literal 0 HcmV?d00001 diff --git a/crates/hm-dsl-engine/vendor/dateutil/zoneinfo/rebuild.py b/crates/hm-dsl-engine/vendor/dateutil/zoneinfo/rebuild.py new file mode 100644 index 00000000..684c6586 --- /dev/null +++ b/crates/hm-dsl-engine/vendor/dateutil/zoneinfo/rebuild.py @@ -0,0 +1,75 @@ +import logging +import os +import tempfile +import shutil +import json +from subprocess import check_call, check_output +from tarfile import TarFile + +from dateutil.zoneinfo import METADATA_FN, ZONEFILENAME + + +def rebuild(filename, tag=None, format="gz", zonegroups=[], metadata=None): + """Rebuild the internal timezone info in dateutil/zoneinfo/zoneinfo*tar* + + filename is the timezone tarball from ``ftp.iana.org/tz``. + + """ + tmpdir = tempfile.mkdtemp() + zonedir = os.path.join(tmpdir, "zoneinfo") + moduledir = os.path.dirname(__file__) + try: + with TarFile.open(filename) as tf: + for name in zonegroups: + tf.extract(name, tmpdir) + filepaths = [os.path.join(tmpdir, n) for n in zonegroups] + + _run_zic(zonedir, filepaths) + + # write metadata file + with open(os.path.join(zonedir, METADATA_FN), 'w') as f: + json.dump(metadata, f, indent=4, sort_keys=True) + target = os.path.join(moduledir, ZONEFILENAME) + with TarFile.open(target, "w:%s" % format) as tf: + for entry in os.listdir(zonedir): + entrypath = os.path.join(zonedir, entry) + tf.add(entrypath, entry) + finally: + shutil.rmtree(tmpdir) + + +def _run_zic(zonedir, filepaths): + """Calls the ``zic`` compiler in a compatible way to get a "fat" binary. + + Recent versions of ``zic`` default to ``-b slim``, while older versions + don't even have the ``-b`` option (but default to "fat" binaries). The + current version of dateutil does not support Version 2+ TZif files, which + causes problems when used in conjunction with "slim" binaries, so this + function is used to ensure that we always get a "fat" binary. + """ + + try: + help_text = check_output(["zic", "--help"]) + except OSError as e: + _print_on_nosuchfile(e) + raise + + if b"-b " in help_text: + bloat_args = ["-b", "fat"] + else: + bloat_args = [] + + check_call(["zic"] + bloat_args + ["-d", zonedir] + filepaths) + + +def _print_on_nosuchfile(e): + """Print helpful troubleshooting message + + e is an exception raised by subprocess.check_call() + + """ + if e.errno == 2: + logging.error( + "Could not find zic. Perhaps you need to install " + "libc-bin or some other package that provides it, " + "or it's not in your PATH?") diff --git a/crates/hm-dsl-engine/vendor/six.py b/crates/hm-dsl-engine/vendor/six.py new file mode 100644 index 00000000..3de5969b --- /dev/null +++ b/crates/hm-dsl-engine/vendor/six.py @@ -0,0 +1,1003 @@ +# Copyright (c) 2010-2024 Benjamin Peterson +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Utilities for writing code that runs on Python 2 and 3""" + +from __future__ import absolute_import + +import functools +import itertools +import operator +import sys +import types + +__author__ = "Benjamin Peterson " +__version__ = "1.17.0" + + +# Useful for very coarse version differentiation. +PY2 = sys.version_info[0] == 2 +PY3 = sys.version_info[0] == 3 +PY34 = sys.version_info[0:2] >= (3, 4) + +if PY3: + string_types = str, + integer_types = int, + class_types = type, + text_type = str + binary_type = bytes + + MAXSIZE = sys.maxsize +else: + string_types = basestring, + integer_types = (int, long) + class_types = (type, types.ClassType) + text_type = unicode + binary_type = str + + if sys.platform.startswith("java"): + # Jython always uses 32 bits. + MAXSIZE = int((1 << 31) - 1) + else: + # It's possible to have sizeof(long) != sizeof(Py_ssize_t). + class X(object): + + def __len__(self): + return 1 << 31 + try: + len(X()) + except OverflowError: + # 32-bit + MAXSIZE = int((1 << 31) - 1) + else: + # 64-bit + MAXSIZE = int((1 << 63) - 1) + del X + +if PY34: + from importlib.util import spec_from_loader +else: + spec_from_loader = None + + +def _add_doc(func, doc): + """Add documentation to a function.""" + func.__doc__ = doc + + +def _import_module(name): + """Import module, returning the module after the last dot.""" + __import__(name) + return sys.modules[name] + + +class _LazyDescr(object): + + def __init__(self, name): + self.name = name + + def __get__(self, obj, tp): + result = self._resolve() + setattr(obj, self.name, result) # Invokes __set__. + try: + # This is a bit ugly, but it avoids running this again by + # removing this descriptor. + delattr(obj.__class__, self.name) + except AttributeError: + pass + return result + + +class MovedModule(_LazyDescr): + + def __init__(self, name, old, new=None): + super(MovedModule, self).__init__(name) + if PY3: + if new is None: + new = name + self.mod = new + else: + self.mod = old + + def _resolve(self): + return _import_module(self.mod) + + def __getattr__(self, attr): + _module = self._resolve() + value = getattr(_module, attr) + setattr(self, attr, value) + return value + + +class _LazyModule(types.ModuleType): + + def __init__(self, name): + super(_LazyModule, self).__init__(name) + self.__doc__ = self.__class__.__doc__ + + def __dir__(self): + attrs = ["__doc__", "__name__"] + attrs += [attr.name for attr in self._moved_attributes] + return attrs + + # Subclasses should override this + _moved_attributes = [] + + +class MovedAttribute(_LazyDescr): + + def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None): + super(MovedAttribute, self).__init__(name) + if PY3: + if new_mod is None: + new_mod = name + self.mod = new_mod + if new_attr is None: + if old_attr is None: + new_attr = name + else: + new_attr = old_attr + self.attr = new_attr + else: + self.mod = old_mod + if old_attr is None: + old_attr = name + self.attr = old_attr + + def _resolve(self): + module = _import_module(self.mod) + return getattr(module, self.attr) + + +class _SixMetaPathImporter(object): + + """ + A meta path importer to import six.moves and its submodules. + + This class implements a PEP302 finder and loader. It should be compatible + with Python 2.5 and all existing versions of Python3 + """ + + def __init__(self, six_module_name): + self.name = six_module_name + self.known_modules = {} + + def _add_module(self, mod, *fullnames): + for fullname in fullnames: + self.known_modules[self.name + "." + fullname] = mod + + def _get_module(self, fullname): + return self.known_modules[self.name + "." + fullname] + + def find_module(self, fullname, path=None): + if fullname in self.known_modules: + return self + return None + + def find_spec(self, fullname, path, target=None): + if fullname in self.known_modules: + return spec_from_loader(fullname, self) + return None + + def __get_module(self, fullname): + try: + return self.known_modules[fullname] + except KeyError: + raise ImportError("This loader does not know module " + fullname) + + def load_module(self, fullname): + try: + # in case of a reload + return sys.modules[fullname] + except KeyError: + pass + mod = self.__get_module(fullname) + if isinstance(mod, MovedModule): + mod = mod._resolve() + else: + mod.__loader__ = self + sys.modules[fullname] = mod + return mod + + def is_package(self, fullname): + """ + Return true, if the named module is a package. + + We need this method to get correct spec objects with + Python 3.4 (see PEP451) + """ + return hasattr(self.__get_module(fullname), "__path__") + + def get_code(self, fullname): + """Return None + + Required, if is_package is implemented""" + self.__get_module(fullname) # eventually raises ImportError + return None + get_source = get_code # same as get_code + + def create_module(self, spec): + return self.load_module(spec.name) + + def exec_module(self, module): + pass + +_importer = _SixMetaPathImporter(__name__) + + +class _MovedItems(_LazyModule): + + """Lazy loading of moved objects""" + __path__ = [] # mark as package + + +_moved_attributes = [ + MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"), + MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), + MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"), + MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), + MovedAttribute("intern", "__builtin__", "sys"), + MovedAttribute("map", "itertools", "builtins", "imap", "map"), + MovedAttribute("getcwd", "os", "os", "getcwdu", "getcwd"), + MovedAttribute("getcwdb", "os", "os", "getcwd", "getcwdb"), + MovedAttribute("getoutput", "commands", "subprocess"), + MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("reload_module", "__builtin__", "importlib" if PY34 else "imp", "reload"), + MovedAttribute("reduce", "__builtin__", "functools"), + MovedAttribute("shlex_quote", "pipes", "shlex", "quote"), + MovedAttribute("StringIO", "StringIO", "io"), + MovedAttribute("UserDict", "UserDict", "collections", "IterableUserDict", "UserDict"), + MovedAttribute("UserList", "UserList", "collections"), + MovedAttribute("UserString", "UserString", "collections"), + MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), + MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"), + MovedModule("builtins", "__builtin__"), + MovedModule("configparser", "ConfigParser"), + MovedModule("collections_abc", "collections", "collections.abc" if sys.version_info >= (3, 3) else "collections"), + MovedModule("copyreg", "copy_reg"), + MovedModule("dbm_gnu", "gdbm", "dbm.gnu"), + MovedModule("dbm_ndbm", "dbm", "dbm.ndbm"), + MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread" if sys.version_info < (3, 9) else "_thread"), + MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), + MovedModule("http_cookies", "Cookie", "http.cookies"), + MovedModule("html_entities", "htmlentitydefs", "html.entities"), + MovedModule("html_parser", "HTMLParser", "html.parser"), + MovedModule("http_client", "httplib", "http.client"), + MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"), + MovedModule("email_mime_image", "email.MIMEImage", "email.mime.image"), + MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"), + MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"), + MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"), + MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), + MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"), + MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"), + MovedModule("cPickle", "cPickle", "pickle"), + MovedModule("queue", "Queue"), + MovedModule("reprlib", "repr"), + MovedModule("socketserver", "SocketServer"), + MovedModule("_thread", "thread", "_thread"), + MovedModule("tkinter", "Tkinter"), + MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"), + MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"), + MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"), + MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), + MovedModule("tkinter_tix", "Tix", "tkinter.tix"), + MovedModule("tkinter_ttk", "ttk", "tkinter.ttk"), + MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"), + MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"), + MovedModule("tkinter_colorchooser", "tkColorChooser", + "tkinter.colorchooser"), + MovedModule("tkinter_commondialog", "tkCommonDialog", + "tkinter.commondialog"), + MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"), + MovedModule("tkinter_font", "tkFont", "tkinter.font"), + MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"), + MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", + "tkinter.simpledialog"), + MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"), + MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"), + MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"), + MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"), + MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"), + MovedModule("xmlrpc_server", "SimpleXMLRPCServer", "xmlrpc.server"), +] +# Add windows specific modules. +if sys.platform == "win32": + _moved_attributes += [ + MovedModule("winreg", "_winreg"), + ] + +for attr in _moved_attributes: + setattr(_MovedItems, attr.name, attr) + if isinstance(attr, MovedModule): + _importer._add_module(attr, "moves." + attr.name) +del attr + +_MovedItems._moved_attributes = _moved_attributes + +moves = _MovedItems(__name__ + ".moves") +_importer._add_module(moves, "moves") + + +class Module_six_moves_urllib_parse(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_parse""" + + +_urllib_parse_moved_attributes = [ + MovedAttribute("ParseResult", "urlparse", "urllib.parse"), + MovedAttribute("SplitResult", "urlparse", "urllib.parse"), + MovedAttribute("parse_qs", "urlparse", "urllib.parse"), + MovedAttribute("parse_qsl", "urlparse", "urllib.parse"), + MovedAttribute("urldefrag", "urlparse", "urllib.parse"), + MovedAttribute("urljoin", "urlparse", "urllib.parse"), + MovedAttribute("urlparse", "urlparse", "urllib.parse"), + MovedAttribute("urlsplit", "urlparse", "urllib.parse"), + MovedAttribute("urlunparse", "urlparse", "urllib.parse"), + MovedAttribute("urlunsplit", "urlparse", "urllib.parse"), + MovedAttribute("quote", "urllib", "urllib.parse"), + MovedAttribute("quote_plus", "urllib", "urllib.parse"), + MovedAttribute("unquote", "urllib", "urllib.parse"), + MovedAttribute("unquote_plus", "urllib", "urllib.parse"), + MovedAttribute("unquote_to_bytes", "urllib", "urllib.parse", "unquote", "unquote_to_bytes"), + MovedAttribute("urlencode", "urllib", "urllib.parse"), + MovedAttribute("splitquery", "urllib", "urllib.parse"), + MovedAttribute("splittag", "urllib", "urllib.parse"), + MovedAttribute("splituser", "urllib", "urllib.parse"), + MovedAttribute("splitvalue", "urllib", "urllib.parse"), + MovedAttribute("uses_fragment", "urlparse", "urllib.parse"), + MovedAttribute("uses_netloc", "urlparse", "urllib.parse"), + MovedAttribute("uses_params", "urlparse", "urllib.parse"), + MovedAttribute("uses_query", "urlparse", "urllib.parse"), + MovedAttribute("uses_relative", "urlparse", "urllib.parse"), +] +for attr in _urllib_parse_moved_attributes: + setattr(Module_six_moves_urllib_parse, attr.name, attr) +del attr + +Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes + +_importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"), + "moves.urllib_parse", "moves.urllib.parse") + + +class Module_six_moves_urllib_error(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_error""" + + +_urllib_error_moved_attributes = [ + MovedAttribute("URLError", "urllib2", "urllib.error"), + MovedAttribute("HTTPError", "urllib2", "urllib.error"), + MovedAttribute("ContentTooShortError", "urllib", "urllib.error"), +] +for attr in _urllib_error_moved_attributes: + setattr(Module_six_moves_urllib_error, attr.name, attr) +del attr + +Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes + +_importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"), + "moves.urllib_error", "moves.urllib.error") + + +class Module_six_moves_urllib_request(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_request""" + + +_urllib_request_moved_attributes = [ + MovedAttribute("urlopen", "urllib2", "urllib.request"), + MovedAttribute("install_opener", "urllib2", "urllib.request"), + MovedAttribute("build_opener", "urllib2", "urllib.request"), + MovedAttribute("pathname2url", "urllib", "urllib.request"), + MovedAttribute("url2pathname", "urllib", "urllib.request"), + MovedAttribute("getproxies", "urllib", "urllib.request"), + MovedAttribute("Request", "urllib2", "urllib.request"), + MovedAttribute("OpenerDirector", "urllib2", "urllib.request"), + MovedAttribute("HTTPDefaultErrorHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPRedirectHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPCookieProcessor", "urllib2", "urllib.request"), + MovedAttribute("ProxyHandler", "urllib2", "urllib.request"), + MovedAttribute("BaseHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPPasswordMgr", "urllib2", "urllib.request"), + MovedAttribute("HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"), + MovedAttribute("AbstractBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("ProxyBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("AbstractDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("ProxyDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPSHandler", "urllib2", "urllib.request"), + MovedAttribute("FileHandler", "urllib2", "urllib.request"), + MovedAttribute("FTPHandler", "urllib2", "urllib.request"), + MovedAttribute("CacheFTPHandler", "urllib2", "urllib.request"), + MovedAttribute("UnknownHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPErrorProcessor", "urllib2", "urllib.request"), + MovedAttribute("urlretrieve", "urllib", "urllib.request"), + MovedAttribute("urlcleanup", "urllib", "urllib.request"), + MovedAttribute("proxy_bypass", "urllib", "urllib.request"), + MovedAttribute("parse_http_list", "urllib2", "urllib.request"), + MovedAttribute("parse_keqv_list", "urllib2", "urllib.request"), +] +if sys.version_info[:2] < (3, 14): + _urllib_request_moved_attributes.extend( + [ + MovedAttribute("URLopener", "urllib", "urllib.request"), + MovedAttribute("FancyURLopener", "urllib", "urllib.request"), + ] + ) +for attr in _urllib_request_moved_attributes: + setattr(Module_six_moves_urllib_request, attr.name, attr) +del attr + +Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes + +_importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"), + "moves.urllib_request", "moves.urllib.request") + + +class Module_six_moves_urllib_response(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_response""" + + +_urllib_response_moved_attributes = [ + MovedAttribute("addbase", "urllib", "urllib.response"), + MovedAttribute("addclosehook", "urllib", "urllib.response"), + MovedAttribute("addinfo", "urllib", "urllib.response"), + MovedAttribute("addinfourl", "urllib", "urllib.response"), +] +for attr in _urllib_response_moved_attributes: + setattr(Module_six_moves_urllib_response, attr.name, attr) +del attr + +Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes + +_importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"), + "moves.urllib_response", "moves.urllib.response") + + +class Module_six_moves_urllib_robotparser(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_robotparser""" + + +_urllib_robotparser_moved_attributes = [ + MovedAttribute("RobotFileParser", "robotparser", "urllib.robotparser"), +] +for attr in _urllib_robotparser_moved_attributes: + setattr(Module_six_moves_urllib_robotparser, attr.name, attr) +del attr + +Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes + +_importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"), + "moves.urllib_robotparser", "moves.urllib.robotparser") + + +class Module_six_moves_urllib(types.ModuleType): + + """Create a six.moves.urllib namespace that resembles the Python 3 namespace""" + __path__ = [] # mark as package + parse = _importer._get_module("moves.urllib_parse") + error = _importer._get_module("moves.urllib_error") + request = _importer._get_module("moves.urllib_request") + response = _importer._get_module("moves.urllib_response") + robotparser = _importer._get_module("moves.urllib_robotparser") + + def __dir__(self): + return ['parse', 'error', 'request', 'response', 'robotparser'] + +_importer._add_module(Module_six_moves_urllib(__name__ + ".moves.urllib"), + "moves.urllib") + + +def add_move(move): + """Add an item to six.moves.""" + setattr(_MovedItems, move.name, move) + + +def remove_move(name): + """Remove item from six.moves.""" + try: + delattr(_MovedItems, name) + except AttributeError: + try: + del moves.__dict__[name] + except KeyError: + raise AttributeError("no such move, %r" % (name,)) + + +if PY3: + _meth_func = "__func__" + _meth_self = "__self__" + + _func_closure = "__closure__" + _func_code = "__code__" + _func_defaults = "__defaults__" + _func_globals = "__globals__" +else: + _meth_func = "im_func" + _meth_self = "im_self" + + _func_closure = "func_closure" + _func_code = "func_code" + _func_defaults = "func_defaults" + _func_globals = "func_globals" + + +try: + advance_iterator = next +except NameError: + def advance_iterator(it): + return it.next() +next = advance_iterator + + +try: + callable = callable +except NameError: + def callable(obj): + return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) + + +if PY3: + def get_unbound_function(unbound): + return unbound + + create_bound_method = types.MethodType + + def create_unbound_method(func, cls): + return func + + Iterator = object +else: + def get_unbound_function(unbound): + return unbound.im_func + + def create_bound_method(func, obj): + return types.MethodType(func, obj, obj.__class__) + + def create_unbound_method(func, cls): + return types.MethodType(func, None, cls) + + class Iterator(object): + + def next(self): + return type(self).__next__(self) + + callable = callable +_add_doc(get_unbound_function, + """Get the function out of a possibly unbound function""") + + +get_method_function = operator.attrgetter(_meth_func) +get_method_self = operator.attrgetter(_meth_self) +get_function_closure = operator.attrgetter(_func_closure) +get_function_code = operator.attrgetter(_func_code) +get_function_defaults = operator.attrgetter(_func_defaults) +get_function_globals = operator.attrgetter(_func_globals) + + +if PY3: + def iterkeys(d, **kw): + return iter(d.keys(**kw)) + + def itervalues(d, **kw): + return iter(d.values(**kw)) + + def iteritems(d, **kw): + return iter(d.items(**kw)) + + def iterlists(d, **kw): + return iter(d.lists(**kw)) + + viewkeys = operator.methodcaller("keys") + + viewvalues = operator.methodcaller("values") + + viewitems = operator.methodcaller("items") +else: + def iterkeys(d, **kw): + return d.iterkeys(**kw) + + def itervalues(d, **kw): + return d.itervalues(**kw) + + def iteritems(d, **kw): + return d.iteritems(**kw) + + def iterlists(d, **kw): + return d.iterlists(**kw) + + viewkeys = operator.methodcaller("viewkeys") + + viewvalues = operator.methodcaller("viewvalues") + + viewitems = operator.methodcaller("viewitems") + +_add_doc(iterkeys, "Return an iterator over the keys of a dictionary.") +_add_doc(itervalues, "Return an iterator over the values of a dictionary.") +_add_doc(iteritems, + "Return an iterator over the (key, value) pairs of a dictionary.") +_add_doc(iterlists, + "Return an iterator over the (key, [values]) pairs of a dictionary.") + + +if PY3: + def b(s): + return s.encode("latin-1") + + def u(s): + return s + unichr = chr + import struct + int2byte = struct.Struct(">B").pack + del struct + byte2int = operator.itemgetter(0) + indexbytes = operator.getitem + iterbytes = iter + import io + StringIO = io.StringIO + BytesIO = io.BytesIO + del io + _assertCountEqual = "assertCountEqual" + if sys.version_info[1] <= 1: + _assertRaisesRegex = "assertRaisesRegexp" + _assertRegex = "assertRegexpMatches" + _assertNotRegex = "assertNotRegexpMatches" + else: + _assertRaisesRegex = "assertRaisesRegex" + _assertRegex = "assertRegex" + _assertNotRegex = "assertNotRegex" +else: + def b(s): + return s + # Workaround for standalone backslash + + def u(s): + return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape") + unichr = unichr + int2byte = chr + + def byte2int(bs): + return ord(bs[0]) + + def indexbytes(buf, i): + return ord(buf[i]) + iterbytes = functools.partial(itertools.imap, ord) + import StringIO + StringIO = BytesIO = StringIO.StringIO + _assertCountEqual = "assertItemsEqual" + _assertRaisesRegex = "assertRaisesRegexp" + _assertRegex = "assertRegexpMatches" + _assertNotRegex = "assertNotRegexpMatches" +_add_doc(b, """Byte literal""") +_add_doc(u, """Text literal""") + + +def assertCountEqual(self, *args, **kwargs): + return getattr(self, _assertCountEqual)(*args, **kwargs) + + +def assertRaisesRegex(self, *args, **kwargs): + return getattr(self, _assertRaisesRegex)(*args, **kwargs) + + +def assertRegex(self, *args, **kwargs): + return getattr(self, _assertRegex)(*args, **kwargs) + + +def assertNotRegex(self, *args, **kwargs): + return getattr(self, _assertNotRegex)(*args, **kwargs) + + +if PY3: + exec_ = getattr(moves.builtins, "exec") + + def reraise(tp, value, tb=None): + try: + if value is None: + value = tp() + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + finally: + value = None + tb = None + +else: + def exec_(_code_, _globs_=None, _locs_=None): + """Execute code in a namespace.""" + if _globs_ is None: + frame = sys._getframe(1) + _globs_ = frame.f_globals + if _locs_ is None: + _locs_ = frame.f_locals + del frame + elif _locs_ is None: + _locs_ = _globs_ + exec("""exec _code_ in _globs_, _locs_""") + + exec_("""def reraise(tp, value, tb=None): + try: + raise tp, value, tb + finally: + tb = None +""") + + +if sys.version_info[:2] > (3,): + exec_("""def raise_from(value, from_value): + try: + raise value from from_value + finally: + value = None +""") +else: + def raise_from(value, from_value): + raise value + + +print_ = getattr(moves.builtins, "print", None) +if print_ is None: + def print_(*args, **kwargs): + """The new-style print function for Python 2.4 and 2.5.""" + fp = kwargs.pop("file", sys.stdout) + if fp is None: + return + + def write(data): + if not isinstance(data, basestring): + data = str(data) + # If the file has an encoding, encode unicode with it. + if (isinstance(fp, file) and + isinstance(data, unicode) and + fp.encoding is not None): + errors = getattr(fp, "errors", None) + if errors is None: + errors = "strict" + data = data.encode(fp.encoding, errors) + fp.write(data) + want_unicode = False + sep = kwargs.pop("sep", None) + if sep is not None: + if isinstance(sep, unicode): + want_unicode = True + elif not isinstance(sep, str): + raise TypeError("sep must be None or a string") + end = kwargs.pop("end", None) + if end is not None: + if isinstance(end, unicode): + want_unicode = True + elif not isinstance(end, str): + raise TypeError("end must be None or a string") + if kwargs: + raise TypeError("invalid keyword arguments to print()") + if not want_unicode: + for arg in args: + if isinstance(arg, unicode): + want_unicode = True + break + if want_unicode: + newline = unicode("\n") + space = unicode(" ") + else: + newline = "\n" + space = " " + if sep is None: + sep = space + if end is None: + end = newline + for i, arg in enumerate(args): + if i: + write(sep) + write(arg) + write(end) +if sys.version_info[:2] < (3, 3): + _print = print_ + + def print_(*args, **kwargs): + fp = kwargs.get("file", sys.stdout) + flush = kwargs.pop("flush", False) + _print(*args, **kwargs) + if flush and fp is not None: + fp.flush() + +_add_doc(reraise, """Reraise an exception.""") + +if sys.version_info[0:2] < (3, 4): + # This does exactly the same what the :func:`py3:functools.update_wrapper` + # function does on Python versions after 3.2. It sets the ``__wrapped__`` + # attribute on ``wrapper`` object and it doesn't raise an error if any of + # the attributes mentioned in ``assigned`` and ``updated`` are missing on + # ``wrapped`` object. + def _update_wrapper(wrapper, wrapped, + assigned=functools.WRAPPER_ASSIGNMENTS, + updated=functools.WRAPPER_UPDATES): + for attr in assigned: + try: + value = getattr(wrapped, attr) + except AttributeError: + continue + else: + setattr(wrapper, attr, value) + for attr in updated: + getattr(wrapper, attr).update(getattr(wrapped, attr, {})) + wrapper.__wrapped__ = wrapped + return wrapper + _update_wrapper.__doc__ = functools.update_wrapper.__doc__ + + def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS, + updated=functools.WRAPPER_UPDATES): + return functools.partial(_update_wrapper, wrapped=wrapped, + assigned=assigned, updated=updated) + wraps.__doc__ = functools.wraps.__doc__ + +else: + wraps = functools.wraps + + +def with_metaclass(meta, *bases): + """Create a base class with a metaclass.""" + # This requires a bit of explanation: the basic idea is to make a dummy + # metaclass for one level of class instantiation that replaces itself with + # the actual metaclass. + class metaclass(type): + + def __new__(cls, name, this_bases, d): + if sys.version_info[:2] >= (3, 7): + # This version introduced PEP 560 that requires a bit + # of extra care (we mimic what is done by __build_class__). + resolved_bases = types.resolve_bases(bases) + if resolved_bases is not bases: + d['__orig_bases__'] = bases + else: + resolved_bases = bases + return meta(name, resolved_bases, d) + + @classmethod + def __prepare__(cls, name, this_bases): + return meta.__prepare__(name, bases) + return type.__new__(metaclass, 'temporary_class', (), {}) + + +def add_metaclass(metaclass): + """Class decorator for creating a class with a metaclass.""" + def wrapper(cls): + orig_vars = cls.__dict__.copy() + slots = orig_vars.get('__slots__') + if slots is not None: + if isinstance(slots, str): + slots = [slots] + for slots_var in slots: + orig_vars.pop(slots_var) + orig_vars.pop('__dict__', None) + orig_vars.pop('__weakref__', None) + if hasattr(cls, '__qualname__'): + orig_vars['__qualname__'] = cls.__qualname__ + return metaclass(cls.__name__, cls.__bases__, orig_vars) + return wrapper + + +def ensure_binary(s, encoding='utf-8', errors='strict'): + """Coerce **s** to six.binary_type. + + For Python 2: + - `unicode` -> encoded to `str` + - `str` -> `str` + + For Python 3: + - `str` -> encoded to `bytes` + - `bytes` -> `bytes` + """ + if isinstance(s, binary_type): + return s + if isinstance(s, text_type): + return s.encode(encoding, errors) + raise TypeError("not expecting type '%s'" % type(s)) + + +def ensure_str(s, encoding='utf-8', errors='strict'): + """Coerce *s* to `str`. + + For Python 2: + - `unicode` -> encoded to `str` + - `str` -> `str` + + For Python 3: + - `str` -> `str` + - `bytes` -> decoded to `str` + """ + # Optimization: Fast return for the common case. + if type(s) is str: + return s + if PY2 and isinstance(s, text_type): + return s.encode(encoding, errors) + elif PY3 and isinstance(s, binary_type): + return s.decode(encoding, errors) + elif not isinstance(s, (text_type, binary_type)): + raise TypeError("not expecting type '%s'" % type(s)) + return s + + +def ensure_text(s, encoding='utf-8', errors='strict'): + """Coerce *s* to six.text_type. + + For Python 2: + - `unicode` -> `unicode` + - `str` -> `unicode` + + For Python 3: + - `str` -> `str` + - `bytes` -> decoded to `str` + """ + if isinstance(s, binary_type): + return s.decode(encoding, errors) + elif isinstance(s, text_type): + return s + else: + raise TypeError("not expecting type '%s'" % type(s)) + + +def python_2_unicode_compatible(klass): + """ + A class decorator that defines __unicode__ and __str__ methods under Python 2. + Under Python 3 it does nothing. + + To support Python 2 and 3 with a single code base, define a __str__ method + returning text and apply this decorator to the class. + """ + if PY2: + if '__str__' not in klass.__dict__: + raise ValueError("@python_2_unicode_compatible cannot be applied " + "to %s because it doesn't define __str__()." % + klass.__name__) + klass.__unicode__ = klass.__str__ + klass.__str__ = lambda self: self.__unicode__().encode('utf-8') + return klass + + +# Complete the moves implementation. +# This code is at the end of this module to speed up module loading. +# Turn this module into a package. +__path__ = [] # required for PEP 302 and PEP 451 +__package__ = __name__ # see PEP 366 @ReservedAssignment +if globals().get("__spec__") is not None: + __spec__.submodule_search_locations = [] # PEP 451 @UndefinedVariable +# Remove other six meta path importers, since they cause problems. This can +# happen if six is removed from sys.modules and then reloaded. (Setuptools does +# this for some reason.) +if sys.meta_path: + for i, importer in enumerate(sys.meta_path): + # Here's some real nastiness: Another "instance" of the six module might + # be floating around. Therefore, we can't use isinstance() to check for + # the six meta path importer, since the other six instance will have + # inserted an importer with different class. + if (type(importer).__name__ == "_SixMetaPathImporter" and + importer.name == __name__): + del sys.meta_path[i] + break + del i, importer +# Finally, add the importer to the meta path import hook. +sys.meta_path.append(_importer) From 555706c421f37f9a2a6d693785dea33f710361f5 Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Sun, 24 May 2026 11:09:01 -0700 Subject: [PATCH 06/44] =?UTF-8?q?feat(dsl-engine):=20build-time=20TS?= =?UTF-8?q?=E2=86=92JS=20bundle=20with=20SHA-256=20polyfill?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/hm-dsl-engine/build.rs | 104 ++++++++++++ .../hm-dsl-engine/embedded/sha256-polyfill.js | 160 ++++++++++++++++++ 2 files changed, 264 insertions(+) create mode 100644 crates/hm-dsl-engine/build.rs create mode 100644 crates/hm-dsl-engine/embedded/sha256-polyfill.js diff --git a/crates/hm-dsl-engine/build.rs b/crates/hm-dsl-engine/build.rs new file mode 100644 index 00000000..6abeca4a --- /dev/null +++ b/crates/hm-dsl-engine/build.rs @@ -0,0 +1,104 @@ +use std::env; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; + +fn main() { + // Only run when embedded-typescript is enabled. + let ts_enabled = env::var("CARGO_FEATURE_EMBEDDED_TYPESCRIPT").is_ok(); + if !ts_enabled { + return; + } + + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + let workspace_root = manifest_dir.join("../..").canonicalize().unwrap(); + + let ts_src_dir = workspace_root.join("dsls/harmont-ts/src"); + let polyfill_path = manifest_dir.join("embedded/sha256-polyfill.js"); + + // Re-run if source files change. + println!("cargo:rerun-if-changed={}", ts_src_dir.display()); + println!("cargo:rerun-if-changed={}", polyfill_path.display()); + + let bundle_out = out_dir.join("harmont-bundle.js"); + + let polyfill = fs::read_to_string(&polyfill_path) + .expect("failed to read sha256-polyfill.js"); + + // Try to find esbuild. + let esbuild_bin = find_esbuild(&workspace_root); + + let Some(esbuild) = esbuild_bin else { + eprintln!( + "cargo:warning=esbuild not found; writing stub harmont-bundle.js. \ + Install esbuild or run `npm install` in dsls/harmont-ts/ for a real bundle." + ); + fs::write( + &bundle_out, + "// stub: esbuild was not available at build time\nvar harmont = {};\n", + ) + .unwrap(); + return; + }; + + // Create a crypto shim that delegates to globalThis.createHash (provided + // by the SHA-256 polyfill prepended to the bundle). + let shim_path = out_dir.join("_crypto_shim.ts"); + fs::write( + &shim_path, + "export function createHash(algo: string) {\n \ + return (globalThis as any).createHash(algo);\n}\n", + ) + .unwrap(); + + // Run esbuild to bundle the TS DSL into a single IIFE. + let entry = ts_src_dir.join("index.ts"); + let raw_bundle_path = out_dir.join("harmont-raw.js"); + + let status = Command::new(&esbuild) + .arg(entry.to_str().unwrap()) + .arg("--bundle") + .arg("--format=iife") + .arg("--global-name=harmont") + .arg("--platform=neutral") + .arg(format!("--alias:node:crypto={}", shim_path.display())) + .arg(format!("--outfile={}", raw_bundle_path.display())) + .status() + .expect("failed to run esbuild"); + + if !status.success() { + panic!("esbuild failed with status {status}"); + } + + let raw_bundle = + fs::read_to_string(&raw_bundle_path).expect("failed to read esbuild output"); + + // Prepend polyfill to the bundle. + let combined = format!( + "// === SHA-256 polyfill ===\n{polyfill}\n// === harmont-ts bundle ===\n{raw_bundle}" + ); + fs::write(&bundle_out, combined).unwrap(); + + println!("cargo:warning=harmont-ts bundle written to {}", bundle_out.display()); +} + +/// Locate an esbuild binary. Checks the local node_modules first, then PATH. +fn find_esbuild(workspace_root: &Path) -> Option { + // 1. Local node_modules binary in dsls/harmont-ts/ + let local = workspace_root.join("dsls/harmont-ts/node_modules/.bin/esbuild"); + if local.exists() { + return Some(local); + } + + // 2. System PATH + let which = Command::new("which").arg("esbuild").output().ok()?; + if which.status.success() { + let path = String::from_utf8_lossy(&which.stdout).trim().to_string(); + if !path.is_empty() { + return Some(PathBuf::from(path)); + } + } + + None +} diff --git a/crates/hm-dsl-engine/embedded/sha256-polyfill.js b/crates/hm-dsl-engine/embedded/sha256-polyfill.js new file mode 100644 index 00000000..e47968f3 --- /dev/null +++ b/crates/hm-dsl-engine/embedded/sha256-polyfill.js @@ -0,0 +1,160 @@ +// FIPS 180-4 SHA-256 polyfill for QuickJS (no Web Crypto, no Node.js APIs). +// Exposes globalThis.createHash("sha256") matching the Node.js subset used by harmont-ts. +(function () { + "use strict"; + + var K = [ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, + 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, + 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, + 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, + 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, + 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, + 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, + 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, + 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, + ]; + + function utf8ToBytes(str) { + var bytes = []; + for (var i = 0; i < str.length; i++) { + var c = str.charCodeAt(i); + if (c < 0x80) { + bytes.push(c); + } else if (c < 0x800) { + bytes.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f)); + } else if (c >= 0xd800 && c <= 0xdbff && i + 1 < str.length) { + var next = str.charCodeAt(i + 1); + if (next >= 0xdc00 && next <= 0xdfff) { + var cp = ((c - 0xd800) << 10) + (next - 0xdc00) + 0x10000; + bytes.push( + 0xf0 | (cp >> 18), + 0x80 | ((cp >> 12) & 0x3f), + 0x80 | ((cp >> 6) & 0x3f), + 0x80 | (cp & 0x3f) + ); + i++; + } else { + bytes.push(0xef, 0xbf, 0xbd); + } + } else if (c >= 0xdc00 && c <= 0xdfff) { + bytes.push(0xef, 0xbf, 0xbd); + } else { + bytes.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f)); + } + } + return bytes; + } + + function rotr(x, n) { return ((x >>> n) | (x << (32 - n))) >>> 0; } + + function sha256(msgBytes) { + var h0 = 0x6a09e667, h1 = 0xbb67ae85, h2 = 0x3c6ef372, h3 = 0xa54ff53a; + var h4 = 0x510e527f, h5 = 0x9b05688c, h6 = 0x1f83d9ab, h7 = 0x5be0cd19; + + var msgLen = msgBytes.length; + var bitLen = msgLen * 8; + + // Padding: append 0x80, then zeros, then 64-bit big-endian length. + msgBytes.push(0x80); + while (msgBytes.length % 64 !== 56) { + msgBytes.push(0); + } + // Append bit length as 64-bit big-endian (we only support < 2^32 bits). + for (var i = 56; i >= 0; i -= 8) { + if (i >= 32) { + msgBytes.push(0); + } else { + msgBytes.push((bitLen >>> i) & 0xff); + } + } + + var W = new Array(64); + + for (var offset = 0; offset < msgBytes.length; offset += 64) { + for (var t = 0; t < 16; t++) { + var base = offset + t * 4; + W[t] = ((msgBytes[base] << 24) | (msgBytes[base + 1] << 16) | + (msgBytes[base + 2] << 8) | msgBytes[base + 3]) >>> 0; + } + for (var t = 16; t < 64; t++) { + var s0 = rotr(W[t - 15], 7) ^ rotr(W[t - 15], 18) ^ (W[t - 15] >>> 3); + var s1 = rotr(W[t - 2], 17) ^ rotr(W[t - 2], 19) ^ (W[t - 2] >>> 10); + W[t] = (W[t - 16] + s0 + W[t - 7] + s1) >>> 0; + } + + var a = h0, b = h1, c = h2, d = h3; + var e = h4, f = h5, g = h6, h = h7; + + for (var t = 0; t < 64; t++) { + var S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25); + var ch = (e & f) ^ (~e & g); + var temp1 = (h + S1 + ch + K[t] + W[t]) >>> 0; + var S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22); + var maj = (a & b) ^ (a & c) ^ (b & c); + var temp2 = (S0 + maj) >>> 0; + + h = g; g = f; f = e; + e = (d + temp1) >>> 0; + d = c; c = b; b = a; + a = (temp1 + temp2) >>> 0; + } + + h0 = (h0 + a) >>> 0; h1 = (h1 + b) >>> 0; + h2 = (h2 + c) >>> 0; h3 = (h3 + d) >>> 0; + h4 = (h4 + e) >>> 0; h5 = (h5 + f) >>> 0; + h6 = (h6 + g) >>> 0; h7 = (h7 + h) >>> 0; + } + + return [h0, h1, h2, h3, h4, h5, h6, h7]; + } + + function toHex(words) { + var hex = ""; + for (var i = 0; i < words.length; i++) { + for (var j = 24; j >= 0; j -= 8) { + var byte = (words[i] >>> j) & 0xff; + hex += (byte < 16 ? "0" : "") + byte.toString(16); + } + } + return hex; + } + + function createHash(algo) { + if (algo !== "sha256") { + throw new Error("Unsupported algorithm: " + algo + " (only sha256 is available)"); + } + var buffer = []; + return { + update: function (data, _encoding) { + if (typeof data === "string") { + var bytes = utf8ToBytes(data); + for (var i = 0; i < bytes.length; i++) { + buffer.push(bytes[i]); + } + } else { + for (var i = 0; i < data.length; i++) { + buffer.push(data[i]); + } + } + return this; + }, + digest: function (encoding) { + var words = sha256(buffer); + if (encoding === "hex") { + return toHex(words); + } + throw new Error("Unsupported encoding: " + encoding + " (only hex is available)"); + }, + }; + } + + globalThis.createHash = createHash; +})(); From 5b147e931b9dc9f7bca23dc46676157966d4bd55 Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Sun, 24 May 2026 13:09:06 -0700 Subject: [PATCH 07/44] =?UTF-8?q?feat(dsl-engine):=20TS=E2=86=92JS=20prepr?= =?UTF-8?q?ocessing=20via=20oxc=20+=20import=20rewriting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two-stage pipeline: first rewrites harmont imports to globalThis destructuring at the text level, then strips TypeScript types via oxc_transformer (parse → semantic → transform → codegen). Uses only_remove_type_imports to preserve non-harmont value imports. Adds oxc_codegen and oxc_semantic as optional deps under the embedded-typescript feature. --- Cargo.lock | 64 +++++++ crates/hm-dsl-engine/Cargo.toml | 4 +- crates/hm-dsl-engine/src/lib.rs | 4 +- crates/hm-dsl-engine/src/ts_preprocess.rs | 208 ++++++++++++++++++++++ 4 files changed, 278 insertions(+), 2 deletions(-) create mode 100644 crates/hm-dsl-engine/src/ts_preprocess.rs diff --git a/Cargo.lock b/Cargo.lock index 4ad47c5d..8e35dbe0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -308,6 +308,16 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + [[package]] name = "bincode" version = "2.0.1" @@ -1594,7 +1604,9 @@ dependencies = [ "hex", "include_dir", "oxc_allocator", + "oxc_codegen", "oxc_parser", + "oxc_semantic", "oxc_span", "oxc_transformer", "reqwest", @@ -2166,6 +2178,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json-escape-simd" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35e770254dd7802184595b1d30da2a15cb72569e2aca2b177aef8d22eac8a693" + [[package]] name = "lazy_static" version = "1.5.0" @@ -2513,6 +2531,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + [[package]] name = "owo-colors" version = "4.3.0" @@ -2619,6 +2643,27 @@ dependencies = [ "oxc_syntax", ] +[[package]] +name = "oxc_codegen" +version = "0.100.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6092d9125daf10b955e30cccd766bb1c16817850c7ab9314b077e4a6b69619" +dependencies = [ + "bitflags", + "cow-utils", + "dragonbox_ecma", + "itoa", + "oxc_allocator", + "oxc_ast", + "oxc_data_structures", + "oxc_index", + "oxc_semantic", + "oxc_sourcemap", + "oxc_span", + "oxc_syntax", + "rustc-hash", +] + [[package]] name = "oxc_compat" version = "0.100.0" @@ -2743,6 +2788,19 @@ dependencies = [ "self_cell", ] +[[package]] +name = "oxc_sourcemap" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d378eb8bad20e89d66276aebab51f6a5408571092cac94abdd3eabb773713d6" +dependencies = [ + "base64-simd", + "json-escape-simd", + "rustc-hash", + "serde", + "serde_json", +] + [[package]] name = "oxc_span" version = "0.100.0" @@ -4460,6 +4518,12 @@ version = "0.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + [[package]] name = "vte" version = "0.14.1" diff --git a/crates/hm-dsl-engine/Cargo.toml b/crates/hm-dsl-engine/Cargo.toml index fe198640..7917f794 100644 --- a/crates/hm-dsl-engine/Cargo.toml +++ b/crates/hm-dsl-engine/Cargo.toml @@ -13,7 +13,7 @@ path = "src/lib.rs" [features] default = ["embedded-python", "embedded-typescript"] embedded-python = ["dep:wasmtime", "dep:wasmtime-wasi", "dep:include_dir"] -embedded-typescript = ["dep:wasmtime", "dep:wasmtime-wasi", "dep:oxc_transformer", "dep:oxc_allocator", "dep:oxc_parser", "dep:oxc_span"] +embedded-typescript = ["dep:wasmtime", "dep:wasmtime-wasi", "dep:oxc_transformer", "dep:oxc_allocator", "dep:oxc_parser", "dep:oxc_span", "dep:oxc_codegen", "dep:oxc_semantic"] [dependencies] # Always-on @@ -36,6 +36,8 @@ oxc_transformer = { version = "0.100", optional = true } oxc_allocator = { version = "0.100", optional = true } oxc_parser = { version = "0.100", optional = true } oxc_span = { version = "0.100", optional = true } +oxc_codegen = { version = "0.100", optional = true } +oxc_semantic = { version = "0.100", optional = true } include_dir = { version = "0.7", optional = true } [dev-dependencies] diff --git a/crates/hm-dsl-engine/src/lib.rs b/crates/hm-dsl-engine/src/lib.rs index bb2fe56f..aa99e956 100644 --- a/crates/hm-dsl-engine/src/lib.rs +++ b/crates/hm-dsl-engine/src/lib.rs @@ -10,7 +10,9 @@ pub mod detect; // Feature-gated modules — added as engines are implemented: // mod python_engine; (embedded-python) // mod js_engine; (embedded-typescript) -// mod ts_preprocess; (embedded-typescript) + +#[cfg(feature = "embedded-typescript")] +pub mod ts_preprocess; #[cfg(feature = "embedded-python")] pub mod embedded_sources; diff --git a/crates/hm-dsl-engine/src/ts_preprocess.rs b/crates/hm-dsl-engine/src/ts_preprocess.rs new file mode 100644 index 00000000..f0944590 --- /dev/null +++ b/crates/hm-dsl-engine/src/ts_preprocess.rs @@ -0,0 +1,208 @@ +//! TypeScript-to-JavaScript preprocessing. +//! +//! Two-stage pipeline: +//! 1. **Strip types** via `oxc_transformer` — parse TS, transform to JS, codegen. +//! 2. **Rewrite imports** — line-by-line text transform that rewrites +//! `import { X } from 'harmont'` → `const { X } = globalThis.harmont;` +//! and strips `import type ...` lines. + +use std::path::Path; + +use oxc_allocator::Allocator; +use oxc_codegen::Codegen; +use oxc_parser::Parser; +use oxc_semantic::SemanticBuilder; +use oxc_span::SourceType; +use oxc_transformer::{TransformOptions, Transformer, TypeScriptOptions}; + +/// Preprocess TypeScript source into plain JavaScript suitable for QuickJS. +/// +/// Strips all TypeScript type syntax and rewrites `harmont` imports to +/// `globalThis.harmont` destructuring. +/// +/// Import rewriting happens first (text level) so the transformer never sees +/// ESM `import` declarations for `harmont` modules. +pub fn preprocess_ts(source: &str) -> anyhow::Result { + let rewritten = rewrite_imports(source); + let js = strip_types(&rewritten)?; + Ok(js) +} + +/// Parse TypeScript, strip type annotations via oxc, and codegen back to JS. +fn strip_types(source: &str) -> anyhow::Result { + let allocator = Allocator::default(); + let source_type = SourceType::default().with_typescript(true).with_module(true); + + // 1. Parse + let parsed = Parser::new(&allocator, source, source_type).parse(); + if parsed.panicked { + anyhow::bail!("oxc parser panicked on TypeScript input"); + } + + // 2. Semantic analysis (required for Scoping used by transformer and codegen) + // Build semantic, extract scoping, then drop the borrow on `program`. + let scoping = { + let sem_ret = SemanticBuilder::new().build(&parsed.program); + sem_ret.semantic.into_scoping() + }; + + // 3. Transform (strip TS types only — preserve value imports) + let mut program = parsed.program; + let options = TransformOptions { + typescript: TypeScriptOptions { + only_remove_type_imports: true, + ..TypeScriptOptions::default() + }, + ..TransformOptions::default() + }; + + let transformer = Transformer::new(&allocator, Path::new("pipeline.ts"), &options); + let ret = transformer.build_with_scoping(scoping, &mut program); + + if !ret.errors.is_empty() { + let msgs: Vec = ret.errors.iter().map(ToString::to_string).collect(); + anyhow::bail!("oxc transform errors: {}", msgs.join("; ")); + } + + // 4. Codegen + let codegen_ret = Codegen::new().build(&program); + Ok(codegen_ret.code) +} + +/// Rewrite `harmont` imports to `globalThis.harmont` destructuring. +/// +/// - `import { X } from 'harmont';` → `const { X } = globalThis.harmont;` +/// - `import { X } from 'harmont/foo';` → `const { X } = globalThis.harmont.foo;` +/// - `import type { ... } from 'harmont';` → removed (oxc may have already done this) +/// - Other imports are left unchanged. +fn rewrite_imports(source: &str) -> String { + let mut output = String::with_capacity(source.len()); + for line in source.lines() { + let trimmed = line.trim(); + + // Skip type-only imports (should already be gone after strip_types, + // but handle it defensively). + if trimmed.starts_with("import type ") { + continue; + } + + // Match: import { ... } from 'harmont...' or import { ... } from "harmont..." + if let Some(rewritten) = try_rewrite_harmont_import(trimmed) { + output.push_str(&rewritten); + output.push('\n'); + continue; + } + + output.push_str(line); + output.push('\n'); + } + output +} + +/// Try to rewrite a single `import { ... } from 'harmont...'` line. +/// Returns `None` if this is not a harmont import. +fn try_rewrite_harmont_import(line: &str) -> Option { + // Must start with `import` + if !line.starts_with("import ") { + return None; + } + + // Extract the binding part between { and } + let open = line.find('{')?; + let close = line.find('}')?; + let bindings = &line[open..=close]; // e.g. "{ sh, pipeline }" + + // Find the module specifier — look for `from` followed by a string literal + let from_idx = line.find(" from ")?; + let after_from = &line[from_idx + 6..]; // after "from " + let after_from = after_from.trim(); + + // Extract the module path from the string literal + let (quote_char, rest) = if after_from.starts_with('\'') { + ('\'', &after_from[1..]) + } else if after_from.starts_with('"') { + ('"', &after_from[1..]) + } else { + return None; + }; + + let end_quote = rest.find(quote_char)?; + let module_path = &rest[..end_quote]; + + // Only rewrite harmont imports + if module_path != "harmont" && !module_path.starts_with("harmont/") { + return None; + } + + // Build the globalThis expression + // 'harmont' -> globalThis.harmont + // 'harmont/foo' -> globalThis.harmont.foo + // 'harmont/foo/bar' -> globalThis.harmont.foo.bar + let global_path = module_path.replace('/', "."); + let global_expr = format!("globalThis.{global_path}"); + + Some(format!("const {bindings} = {global_expr};")) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn strips_type_annotation() { + let input = "const x: string = 'hi';\n"; + let output = preprocess_ts(input).unwrap(); + assert!(!output.contains(": string"), "output: {output}"); + assert!(output.contains("hi"), "output: {output}"); + } + + #[test] + fn rewrites_harmont_import() { + let input = "import { sh, pipeline } from 'harmont';\n"; + let output = preprocess_ts(input).unwrap(); + assert!(output.contains("globalThis.harmont"), "output: {output}"); + assert!(!output.contains("import")); + } + + #[test] + fn removes_type_only_import() { + let input = "import type { Step } from 'harmont';\nconst x = 1;\n"; + let output = preprocess_ts(input).unwrap(); + assert!(!output.contains("import type"), "output: {output}"); + assert!(output.contains("1")); + } + + #[test] + fn rewrites_subpath_import() { + let input = "import { npm } from 'harmont/toolchains';\n"; + let output = preprocess_ts(input).unwrap(); + assert!( + output.contains("globalThis.harmont.toolchains"), + "output: {output}" + ); + } + + #[test] + fn leaves_non_harmont_imports_alone() { + let input = "import { foo } from './local';\n"; + let output = preprocess_ts(input).unwrap(); + assert!(output.contains("./local"), "output: {output}"); + } + + #[test] + fn full_pipeline_file() { + let input = r#"import { sh, pipeline } from 'harmont'; +import type { Step } from 'harmont'; + +const base: Step = sh('echo hi'); + +export default [ + { slug: 'ci', pipeline: pipeline(base) } +]; +"#; + let output = preprocess_ts(input).unwrap(); + assert!(!output.contains("import type"), "type import present"); + assert!(output.contains("globalThis.harmont"), "missing destructure"); + assert!(!output.contains(": Step"), "type annotation present"); + } +} From 597e829bd8180b82ab083b6c67c9f150be7b892d Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Sun, 24 May 2026 13:11:07 -0700 Subject: [PATCH 08/44] feat(dsl-engine): system-interpreter fallback (python3 subprocess) --- crates/hm-dsl-engine/src/fallback.rs | 217 +++++++++++++++++++++++++++ crates/hm-dsl-engine/src/lib.rs | 1 + 2 files changed, 218 insertions(+) create mode 100644 crates/hm-dsl-engine/src/fallback.rs diff --git a/crates/hm-dsl-engine/src/fallback.rs b/crates/hm-dsl-engine/src/fallback.rs new file mode 100644 index 00000000..63bf9727 --- /dev/null +++ b/crates/hm-dsl-engine/src/fallback.rs @@ -0,0 +1,217 @@ +//! Subprocess-based DSL engines that shell out to system `python3` or `node`. +//! +//! These serve as always-available fallbacks when the embedded (WASI) engines +//! are compiled out or otherwise unavailable. + +use std::path::{Path, PathBuf}; +use std::process::Stdio; + +use anyhow::{Context, Result, bail}; +use async_trait::async_trait; +use tokio::process::Command; + +use crate::{DslEngine, PipelineMeta}; + +// --------------------------------------------------------------------------- +// Shared Python script constants +// --------------------------------------------------------------------------- + +/// Python one-liner that discovers every `@hm.pipeline` registration in +/// `.harmont/*.py` and prints `[{slug, name}, ...]` JSON to stdout. +const LIST_PIPELINES_SCRIPT: &str = "\ +import importlib.util, json, pathlib +import harmont as hm +for p in sorted(pathlib.Path('.harmont').glob('*.py')): + spec = importlib.util.spec_from_file_location(f'_harmont_{p.stem}', p) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) +envelope = json.loads(hm.dump_registry_json()) +print(json.dumps([{'slug': p['slug'], 'name': p['name']} for p in envelope['pipelines']])) +"; + +/// Python script that renders a single pipeline (slug passed as `sys.argv[1]`) +/// to its JSON IR definition and prints it to stdout. +const RENDER_PIPELINE_SCRIPT: &str = "\ +import importlib.util, json, pathlib, sys +import harmont as hm +slug = sys.argv[1] +for p in sorted(pathlib.Path('.harmont').glob('*.py')): + spec = importlib.util.spec_from_file_location(f'_harmont_{p.stem}', p) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) +envelope = json.loads(hm.dump_registry_json()) +match = next((p for p in envelope['pipelines'] if p['slug'] == slug), None) +if match is None: + avail = ', '.join(p['slug'] for p in envelope['pipelines']) or '(none)' + print(f'error: pipeline {slug!r} not found\\n -> available: {avail}', file=sys.stderr) + sys.exit(2) +print(json.dumps(match['definition'])) +"; + +// =========================================================================== +// SystemPythonEngine +// =========================================================================== + +/// Subprocess engine that delegates to the system `python3`. +/// +/// This mirrors the behaviour in `crates/hm/src/commands/run/render.rs` and +/// is the fallback when the `embedded-python` feature is not compiled in. +#[derive(Debug)] +pub struct SystemPythonEngine { + cidsl_py: PathBuf, +} + +impl SystemPythonEngine { + /// Discover the `cidsl/py` package path. + /// + /// Honors `HARMONT_CIDSL_PY` if set; otherwise walks up from the current + /// executable looking for a sibling `cidsl/py` directory. + /// + /// # Errors + /// + /// Returns an error only if `std::env::current_exe` fails. + pub fn discover() -> Result { + let cidsl_py = if let Some(p) = std::env::var_os("HARMONT_CIDSL_PY") { + PathBuf::from(p) + } else { + let exe = std::env::current_exe().context("locating cli executable")?; + exe.ancestors() + .find_map(|d| { + let candidate = d.join("cidsl/py"); + candidate.exists().then_some(candidate) + }) + .unwrap_or_else(|| PathBuf::from("cidsl/py")) + }; + Ok(Self { cidsl_py }) + } + + /// Build the `PYTHONPATH` value for a given project root. + fn pythonpath(&self, project_dir: &Path) -> String { + format!( + "{}:{}", + self.cidsl_py.display(), + project_dir.join(".harmont").display() + ) + } + + /// Spawn `python3 -c