diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index b6e6ed107..56ed3da33 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,10 +27,10 @@ jobs: steps: - uses: actions/checkout@v6 - - name: Prepare Python 3.10 + - name: Prepare Python 3.14 uses: actions/setup-python@v6 with: - python-version: '3.10' + python-version: '3.14' - name: Prepare pipenv run: | diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f3495887b..e21d66e99 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -51,10 +51,10 @@ jobs: fetch-depth: 0 submodules: recursive - - name: Prepare Python 3.10 + - name: Prepare Python 3.14 uses: actions/setup-python@v6 with: - python-version: '3.10' + python-version: '3.14' cache: 'pipenv' - name: Prepare pipenv @@ -84,7 +84,7 @@ jobs: dotnet-version: 10.0.x - name: Prepare msbuild - uses: microsoft/setup-msbuild@v2 + uses: microsoft/setup-msbuild@v3 - name: Prepare git if: (github.repository == env.MainRepo) @@ -135,7 +135,7 @@ jobs: - name: Sign files with Trusted Signing (DLLs and EXEs) if: (github.repository == env.MainRepo) - uses: azure/trusted-signing-action@v1.0.0 + uses: azure/trusted-signing-action@v1.2.0 with: azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }} azure-client-id: ${{ secrets.AZURE_CLIENT_ID }} @@ -145,7 +145,7 @@ jobs: certificate-profile-name: ${{ secrets.AZURE_CERT_PROFILE_NAME }} files-folder: bin/ - files-folder-filter: pyrevit*.exe,pyrevit*.dll, pyRevit*.dll + files-folder-filter: pyrevit*.exe,pyrevit*.dll,pyRevit*.dll files-folder-recurse: true file-digest: SHA256 timestamp-rfc3161: http://timestamp.acs.microsoft.com @@ -157,7 +157,7 @@ jobs: - name: Sign files with Trusted Signing (installers) if: (github.repository == env.MainRepo) - uses: azure/trusted-signing-action@v1.0.0 + uses: azure/trusted-signing-action@v1.2.0 with: azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }} azure-client-id: ${{ secrets.AZURE_CLIENT_ID }} @@ -174,7 +174,7 @@ jobs: timestamp-digest: SHA256 - name: Upload Installers - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: pyrevit-installers path: | diff --git a/.gitignore b/.gitignore index ffb331b4b..f8d901780 100644 --- a/.gitignore +++ b/.gitignore @@ -67,4 +67,5 @@ bin/**/*.xml *.dylib # claude -.claude \ No newline at end of file +.claude +.vscode/settings.json diff --git a/Pipfile b/Pipfile index 00a290b7c..54670411c 100644 --- a/Pipfile +++ b/Pipfile @@ -5,7 +5,7 @@ verify_ssl = true [dev-packages] mypy = "*" -pylint = "==4.0.4" +pylint = "==4.0.5" [packages] docopt = "*" @@ -13,7 +13,7 @@ requests = "*" pygount = "*" pyyaml = ">=5.4" black = "*" -setuptools = "==80.10.2" +setuptools = "==82.0.1" mkdocs = "*" mkdocstrings = "*" mkdocstrings-python = "*" @@ -26,10 +26,10 @@ mkdocs-material = "*" ruff = "*" [requires] -python_version = "3.10" +python_version = "3.14" [pipenv] -allow_prereleases = true +allow_prereleases = false [scripts] pyrevit = "python ./dev/pyrevit.py" diff --git a/Pipfile.lock b/Pipfile.lock index f34a98b48..0d9f85fe2 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,11 +1,11 @@ { "_meta": { "hash": { - "sha256": "8dc4b70f3df824ac74388b44cbaf686796fc966097b1241ea7d7e713151a65f5" + "sha256": "2b58c8506f0011488f916b49be763691a56516b735f798fea32b123f0dad88f4" }, "pipfile-spec": 6, "requires": { - "python_version": "3.10" + "python_version": "3.14" }, "sources": [ { @@ -18,66 +18,66 @@ "default": { "babel": { "hashes": [ - "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", - "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2" + "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", + "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35" ], "markers": "python_version >= '3.8'", - "version": "==2.17.0" + "version": "==2.18.0" }, "backrefs": { "hashes": [ - "sha256:13eafbc9ccd5222e9c1f0bec563e6d2a6d21514962f11e7fc79872fd56cbc853", - "sha256:2a2ccb96302337ce61ee4717ceacfbf26ba4efb1d55af86564b8bbaeda39cac1", - "sha256:3bba1749aafe1db9b915f00e0dd166cba613b6f788ffd63060ac3485dc9be231", - "sha256:4c9d3dc1e2e558965202c012304f33d4e0e477e1c103663fd2c3cc9bb18b0d05", - "sha256:a9e99b8a4867852cad177a6430e31b0f6e495d65f8c6c134b68c14c3c95bf4b0", - "sha256:c64698c8d2269343d88947c0735cb4b78745bd3ba590e10313fbf3f78c34da5a", - "sha256:e82bba3875ee4430f4de4b6db19429a27275d95a5f3773c57e9e18abc23fd2b7" + "sha256:08aa7fae530c6b2361d7bdcbda1a7c454e330cc9dbcd03f5c23205e430e5c3be", + "sha256:0fdc7b012420b6b144410342caeb8adc54c6866cf12064abc9bb211302e496f8", + "sha256:12df81596ab511f783b7d87c043ce26bc5b0288cf3bb03610fe76b8189282b2b", + "sha256:664e33cd88c6840b7625b826ecf2555f32d491800900f5a541f772c485f7cda7", + "sha256:c3f4b9cb2af8cda0d87ab4f57800b57b95428488477be164dd2b47be54db0c90", + "sha256:e5f805ae09819caa1aa0623b4a83790e7028604aa2b8c73ba602c4454e665de7", + "sha256:f44ff4d48808b243b6c0cdc6231e22195c32f77046018141556c66f8bab72a49" ], "markers": "python_version >= '3.9'", - "version": "==6.1" + "version": "==6.2" }, "black": { "hashes": [ - "sha256:101540cb2a77c680f4f80e628ae98bd2bd8812fb9d72ade4f8995c5ff019e82c", - "sha256:1054e8e47ebd686e078c0bb0eaf31e6ce69c966058d122f2c0c950311f9f3ede", - "sha256:1de0f7d01cc894066a1153b738145b194414cc6eeaad8ef4397ac9abacf40f6b", - "sha256:2b807c240b64609cb0e80d2200a35b23c7df82259f80bef1b2c96eb422b4aac9", - "sha256:3cee1487a9e4c640dc7467aaa543d6c0097c391dc8ac74eb313f2fbf9d7a7cb5", - "sha256:53c62883b3f999f14e5d30b5a79bd437236658ad45b2f853906c7cbe79de00af", - "sha256:5e8e75dabb6eb83d064b0db46392b25cabb6e784ea624219736e8985a6b3675d", - "sha256:643d27fb5facc167c0b1b59d0315f2674a6e950341aed0fc05cf307d22bf4954", - "sha256:66912475200b67ef5a0ab665011964bf924745103f51977a78b4fb92a9fc1bf0", - "sha256:6eeca41e70b5f5c84f2f913af857cf2ce17410847e1d54642e658e078da6544f", - "sha256:6f3977a16e347f1b115662be07daa93137259c711e526402aa444d7a88fdc9d4", - "sha256:7ed300200918147c963c87700ccf9966dceaefbbb7277450a8d646fc5646bf24", - "sha256:91a68ae46bf07868963671e4d05611b179c2313301bd756a89ad4e3b3db2325b", - "sha256:9459ad0d6cd483eacad4c6566b0f8e42af5e8b583cee917d90ffaa3778420a0a", - "sha256:9dc8c71656a79ca49b8d3e2ce8103210c9481c57798b48deeb3a8bb02db5f115", - "sha256:a19915ec61f3a8746e8b10adbac4a577c6ba9851fa4a9e9fbfbcf319887a5791", - "sha256:b22b3810451abe359a964cc88121d57f7bce482b53a066de0f1584988ca36e79", - "sha256:ba1d768fbfb6930fc93b0ecc32a43d8861ded16f47a40f14afa9bb04ab93d304", - "sha256:be5e2fe860b9bd9edbf676d5b60a9282994c03fbbd40fe8f5e75d194f96064ca", - "sha256:c5b7713daea9bf943f79f8c3b46f361cc5229e0e604dcef6a8bb6d1c37d9df89", - "sha256:ca699710dece84e3ebf6e92ee15f5b8f72870ef984bf944a57a777a48357c168", - "sha256:d294ac3340eef9c9eb5d29288e96dc719ff269a88e27b396340459dd85da4c58", - "sha256:d62d14ca31c92adf561ebb2e5f2741bf8dea28aef6deb400d49cca011d186c68", - "sha256:dd39eef053e58e60204f2cdf059e2442e2eb08f15989eefe259870f89614c8b6", - "sha256:eb07665d9a907a1a645ee41a0df8a25ffac8ad9c26cdb557b7b88eeeeec934e0", - "sha256:f016baaadc423dc960cdddf9acae679e71ee02c4c341f78f3179d7e4819c095f", - "sha256:fb1dafbbaa3b1ee8b4550a84425aac8874e5f390200f5502cf3aee4a2acb2f14" + "sha256:0126ae5b7c09957da2bdbd91a9ba1207453feada9e9fe51992848658c6c8e01c", + "sha256:0f76ff19ec5297dd8e66eb64deda23631e642c9393ab592826fd4bdc97a4bce7", + "sha256:28ef38aee69e4b12fda8dba75e21f9b4f979b490c8ac0baa7cb505369ac9e1ff", + "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b", + "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07", + "sha256:2d6bfaf7fd0993b420bed691f20f9492d53ce9a2bcccea4b797d34e947318a78", + "sha256:41cd2012d35b47d589cb8a16faf8a32ef7a336f56356babd9fcf70939ad1897f", + "sha256:474c27574d6d7037c1bc875a81d9be0a9a4f9ee95e62800dab3cfaadbf75acd5", + "sha256:5602bdb96d52d2d0672f24f6ffe5218795736dd34807fd0fd55ccd6bf206168b", + "sha256:5e9d0d86df21f2e1677cc4bd090cd0e446278bcbbe49bf3659c308c3e402843e", + "sha256:5ed0ca58586c8d9a487352a96b15272b7fa55d139fc8496b519e78023a8dab0a", + "sha256:6c54a4a82e291a1fee5137371ab488866b7c86a3305af4026bdd4dc78642e1ac", + "sha256:6e131579c243c98f35bce64a7e08e87fb2d610544754675d4a0e73a070a5aa3a", + "sha256:855822d90f884905362f602880ed8b5df1b7e3ee7d0db2502d4388a954cc8c54", + "sha256:86a8b5035fce64f5dcd1b794cf8ec4d31fe458cf6ce3986a30deb434df82a1d2", + "sha256:8a33d657f3276328ce00e4d37fe70361e1ec7614da5d7b6e78de5426cb56332f", + "sha256:92c0ec1f2cc149551a2b7b47efc32c866406b6891b0ee4625e95967c8f4acfb1", + "sha256:9a5e9f45e5d5e1c5b5c29b3bd4265dcc90e8b92cf4534520896ed77f791f4da5", + "sha256:afc622538b430aa4c8c853f7f63bc582b3b8030fd8c80b70fb5fa5b834e575c2", + "sha256:b07fc0dab849d24a80a29cfab8d8a19187d1c4685d8a5e6385a5ce323c1f015f", + "sha256:b5e6f89631eb88a7302d416594a32faeee9fb8fb848290da9d0a5f2903519fc1", + "sha256:bf9bf162ed91a26f1adba8efda0b573bc6924ec1408a52cc6f82cb73ec2b142c", + "sha256:c7e72339f841b5a237ff14f7d3880ddd0fc7f98a1199e8c4327f9a4f478c1839", + "sha256:ddb113db38838eb9f043623ba274cfaf7d51d5b0c22ecb30afe58b1bb8322983", + "sha256:dfdd51fc3e64ea4f35873d1b3fb25326773d55d2329ff8449139ebaad7357efb", + "sha256:f1cd08e99d2f9317292a311dfe578fd2a24b15dbce97792f9c4d752275c1fa56", + "sha256:f89f2ab047c76a9c03f78d0d66ca519e389519902fa27e7a91117ef7611c0568" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==26.1.0" + "version": "==26.3.1" }, "certifi": { "hashes": [ - "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", - "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316" + "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", + "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7" ], "markers": "python_version >= '3.7'", - "version": "==2025.11.12" + "version": "==2026.2.25" }, "chardet": { "hashes": [ @@ -89,122 +89,138 @@ }, "charset-normalizer": { "hashes": [ - "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", - "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", - "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", - "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", - "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc", - "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", - "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63", - "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d", - "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", - "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", - "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", - "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505", - "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", - "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af", - "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", - "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318", - "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", - "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", - "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", - "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", - "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576", - "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", - "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1", - "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", - "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1", - "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", - "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", - "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", - "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88", - "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", - "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", - "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", - "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a", - "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", - "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", - "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", - "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", - "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", - "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7", - "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", - "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", - "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", - "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", - "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", - "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", - "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2", - "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", - "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", - "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", - "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", - "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf", - "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6", - "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", - "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", - "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa", - "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", - "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", - "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", - "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", - "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", - "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", - "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", - "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", - "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", - "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", - "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", - "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", - "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", - "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", - "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", - "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3", - "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9", - "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", - "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", - "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", - "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", - "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50", - "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf", - "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", - "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", - "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac", - "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", - "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", - "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c", - "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", - "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", - "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e", - "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4", - "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84", - "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", - "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", - "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", - "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", - "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", - "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", - "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", - "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", - "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", - "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074", - "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3", - "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", - "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", - "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", - "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d", - "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", - "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", - "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", - "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", - "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966", - "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", - "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3", - "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", - "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608" + "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e", + "sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c", + "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5", + "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815", + "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f", + "sha256:150b8ce8e830eb7ccb029ec9ca36022f756986aaaa7956aad6d9ec90089338c0", + "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484", + "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407", + "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", + "sha256:1cf0a70018692f85172348fe06d3a4b63f94ecb055e13a00c644d368eb82e5b8", + "sha256:1ed80ff870ca6de33f4d953fda4d55654b9a2b340ff39ab32fa3adbcd718f264", + "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815", + "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2", + "sha256:259695e2ccc253feb2a016303543d691825e920917e31f894ca1a687982b1de4", + "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579", + "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f", + "sha256:2bd9d128ef93637a5d7a6af25363cf5dec3fa21cf80e68055aad627f280e8afa", + "sha256:2e1d8ca8611099001949d1cdfaefc510cf0f212484fe7c565f735b68c78c3c95", + "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab", + "sha256:2f7fdd9b6e6c529d6a2501a2d36b240109e78a8ceaef5687cfcfa2bbe671d297", + "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a", + "sha256:31215157227939b4fb3d740cd23fe27be0439afef67b785a1eb78a3ae69cba9e", + "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84", + "sha256:3516bbb8d42169de9e61b8520cbeeeb716f12f4ecfe3fd30a9919aa16c806ca8", + "sha256:3778fd7d7cd04ae8f54651f4a7a0bd6e39a0cf20f801720a4c21d80e9b7ad6b0", + "sha256:39f5068d35621da2881271e5c3205125cc456f54e9030d3f723288c873a71bf9", + "sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f", + "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1", + "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843", + "sha256:4482481cb0572180b6fd976a4d5c72a30263e98564da68b86ec91f0fe35e8565", + "sha256:461598cd852bfa5a61b09cae2b1c02e2efcd166ee5516e243d540ac24bfa68a7", + "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c", + "sha256:48696db7f18afb80a068821504296eb0787d9ce239b91ca15059d1d3eaacf13b", + "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7", + "sha256:4d1d02209e06550bdaef34af58e041ad71b88e624f5d825519da3a3308e22687", + "sha256:4f41da960b196ea355357285ad1316a00099f22d0929fe168343b99b254729c9", + "sha256:517ad0e93394ac532745129ceabdf2696b609ec9f87863d337140317ebce1c14", + "sha256:51fb3c322c81d20567019778cb5a4a6f2dc1c200b886bc0d636238e364848c89", + "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f", + "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0", + "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9", + "sha256:54fae94be3d75f3e573c9a1b5402dc593de19377013c9a0e4285e3d402dd3a2a", + "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389", + "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0", + "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30", + "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd", + "sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e", + "sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9", + "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc", + "sha256:659a1e1b500fac8f2779dd9e1570464e012f43e580371470b45277a27baa7532", + "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d", + "sha256:69dd852c2f0ad631b8b60cfbe25a28c0058a894de5abb566619c205ce0550eae", + "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2", + "sha256:71be7e0e01753a89cf024abf7ecb6bca2c81738ead80d43004d9b5e3f1244e64", + "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f", + "sha256:74a2e659c7ecbc73562e2a15e05039f1e22c75b7c7618b4b574a3ea9118d1557", + "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e", + "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff", + "sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398", + "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db", + "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a", + "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43", + "sha256:802168e03fba8bbc5ce0d866d589e4b1ca751d06edee69f7f3a19c5a9fe6b597", + "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c", + "sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e", + "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2", + "sha256:8761ac29b6c81574724322a554605608a9960769ea83d2c73e396f3df896ad54", + "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e", + "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4", + "sha256:8bc5f0687d796c05b1e28ab0d38a50e6309906ee09375dd3aff6a9c09dd6e8f4", + "sha256:8bea55c4eef25b0b19a0337dc4e3f9a15b00d569c77211fa8cde38684f234fb7", + "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6", + "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5", + "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194", + "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", + "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f", + "sha256:97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316", + "sha256:9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e", + "sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73", + "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8", + "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923", + "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88", + "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f", + "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21", + "sha256:a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4", + "sha256:aa9cccf4a44b9b62d8ba8b4dd06c649ba683e4bf04eea606d2e94cfc2d6ff4d6", + "sha256:ab30e5e3e706e3063bc6de96b118688cb10396b70bb9864a430f67df98c61ecc", + "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2", + "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866", + "sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021", + "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2", + "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d", + "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8", + "sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de", + "sha256:bf625105bb9eef28a56a943fec8c8a98aeb80e7d7db99bd3c388137e6eb2d237", + "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4", + "sha256:c45a03a4c69820a399f1dda9e1d8fbf3562eda46e7720458180302021b08f778", + "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb", + "sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc", + "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602", + "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4", + "sha256:d08ec48f0a1c48d75d0356cea971921848fb620fdeba805b28f937e90691209f", + "sha256:d1a2ee9c1499fc8f86f4521f27a973c914b211ffa87322f4ee33bb35392da2c5", + "sha256:d5f5d1e9def3405f60e3ca8232d56f35c98fb7bf581efcc60051ebf53cb8b611", + "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8", + "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf", + "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d", + "sha256:dad6e0f2e481fffdcf776d10ebee25e0ef89f16d691f1e5dee4b586375fdc64b", + "sha256:dda86aba335c902b6149a02a55b38e96287157e609200811837678214ba2b1db", + "sha256:df01808ee470038c3f8dc4f48620df7225c49c2d6639e38f96e6d6ac6e6f7b0e", + "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077", + "sha256:e25369dc110d58ddf29b949377a93e0716d72a24f62bad72b2b39f155949c1fd", + "sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef", + "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e", + "sha256:e68c14b04827dd76dcbd1aeea9e604e3e4b78322d8faf2f8132c7138efa340a8", + "sha256:e8aeb10fcbe92767f0fa69ad5a72deca50d0dca07fbde97848997d778a50c9fe", + "sha256:e985a16ff513596f217cee86c21371b8cd011c0f6f056d0920aa2d926c544058", + "sha256:ecbbd45615a6885fe3240eb9db73b9e62518b611850fdf8ab08bd56de7ad2b17", + "sha256:ee4ec14bc1680d6b0afab9aea2ef27e26d2024f18b24a2d7155a52b60da7e833", + "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421", + "sha256:f0cdaecd4c953bfae0b6bb64910aaaca5a424ad9c72d85cb88417bb9814f7550", + "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff", + "sha256:f50498891691e0864dc3da965f340fada0771f6142a378083dc4608f4ea513e2", + "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc", + "sha256:f61aa92e4aad0be58eb6eb4e0c21acf32cf8065f4b2cae5665da756c4ceef982", + "sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d", + "sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed", + "sha256:f98059e4fcd3e3e4e2d632b7cf81c2faae96c43c60b569e9c621468082f1d104", + "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659" ], "markers": "python_version >= '3.7'", - "version": "==3.4.4" + "version": "==3.4.6" }, "click": { "hashes": [ @@ -247,19 +263,19 @@ }, "gitpython": { "hashes": [ - "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110", - "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269" + "sha256:400124c7d0ef4ea03f7310ac2fbf7151e09ff97f2a3288d64a440c584a29c37f", + "sha256:79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058" ], "markers": "python_version >= '3.7'", - "version": "==3.1.44" + "version": "==3.1.46" }, - "griffe": { + "griffelib": { "hashes": [ - "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3", - "sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea" + "sha256:3cf20b3bc470e83763ffbf236e0076b1211bac1bc67de13daf494640f2de707e", + "sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1" ], "markers": "python_version >= '3.10'", - "version": "==1.15.0" + "version": "==2.0.2" }, "idna": { "hashes": [ @@ -279,19 +295,19 @@ }, "markdown": { "hashes": [ - "sha256:1c19c10bd5c14ac948c53d0d762a04e2fa35a6d58a6b7b1e6bfcbe6fefc0001a", - "sha256:867d788939fe33e4b736426f5b9f651ad0c0ae0ecf89df0ca5d1176c70812fe3" + "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", + "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36" ], "markers": "python_version >= '3.10'", - "version": "==3.10.1" + "version": "==3.10.2" }, "markdown-it-py": { "hashes": [ - "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", - "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb" + "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", + "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3" ], - "markers": "python_version >= '3.8'", - "version": "==3.0.0" + "markers": "python_version >= '3.10'", + "version": "==4.0.0" }, "markupsafe": { "hashes": [ @@ -415,11 +431,11 @@ }, "mkdocs-autorefs": { "hashes": [ - "sha256:469d85eb3114801d08e9cc55d102b3ba65917a869b893403b8987b601cf55dc9", - "sha256:beee715b254455c4aa93b6ef3c67579c399ca092259cc41b7d9342573ff1fc75" + "sha256:834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089", + "sha256:d54a284f27a7346b9c38f1f852177940c222da508e66edc816a0fa55fc6da197" ], "markers": "python_version >= '3.9'", - "version": "==1.4.3" + "version": "==1.4.4" }, "mkdocs-exclude": { "hashes": [ @@ -430,38 +446,38 @@ }, "mkdocs-gen-files": { "hashes": [ - "sha256:52022dc14dcc0451e05e54a8f5d5e7760351b6701eff816d1e9739577ec5635e", - "sha256:815af15f3e2dbfda379629c1b95c02c8e6f232edf2a901186ea3b204ab1135b2" + "sha256:57d7ff2229e23d077e46d14a33db6d37c8823f6ce1a503c874c1764a71679763", + "sha256:b3182bfc6219e35b8d26658cb988368659d5d023aac30c2a819247558fc12189" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==0.6.0" + "version": "==0.6.1" }, "mkdocs-get-deps": { "hashes": [ - "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", - "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134" + "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", + "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650" ], - "markers": "python_version >= '3.8'", - "version": "==0.2.0" + "markers": "python_version >= '3.9'", + "version": "==0.2.2" }, "mkdocs-literate-nav": { "hashes": [ - "sha256:0a6489a26ec7598477b56fa112056a5e3a6c15729f0214bea8a4dbc55bd5f630", - "sha256:760e1708aa4be86af81a2b56e82c739d5a8388a0eab1517ecfd8e5aa40810a75" + "sha256:2c421561280fa9184f88cbf399bebbd4cc17ee507e978a31ce11fd6f3aabf233", + "sha256:edbaca22343f861fe4e34aac47d55a0c9955c640dbf02eea99fe631e914cf9ee" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==0.6.2" + "version": "==0.6.3" }, "mkdocs-material": { "hashes": [ - "sha256:3f6100937d7d731f87f1e3e3b021c97f7239666b9ba1151ab476cabb96c60d5c", - "sha256:89601b8f2c3e6c6ee0a918cc3566cb201d40bf37c3cd3c2067e26fadb8cce2b8" + "sha256:00bdde50574f776d328b1862fe65daeaf581ec309bd150f7bff345a098c64a69", + "sha256:71b84353921b8ea1ba84fe11c50912cc512da8fe0881038fcc9a0761c0e635ba" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==9.7.1" + "version": "==9.7.6" }, "mkdocs-material-extensions": { "hashes": [ @@ -473,30 +489,30 @@ }, "mkdocs-section-index": { "hashes": [ - "sha256:a82afbda633c82c5568f0e3b008176b9b365bf4bd8b6f919d6eff09ee146b9f8", - "sha256:bc27c0d0dc497c0ebaee1fc72839362aed77be7318b5ec0c30628f65918e4776" + "sha256:26f008f4860789e6c41dce868e3e1dcd1528f8cbc1db181416c5edc18f0f15a0", + "sha256:81a5948af0e974bfb474f40b45aeddbb621024ff132eb8ace8854b9db6b41812" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==0.3.10" + "version": "==0.3.11" }, "mkdocstrings": { "hashes": [ - "sha256:41897815a8026c3634fe5d51472c3a569f92ded0ad8c7a640550873eea3b6817", - "sha256:48edd0ccbcb9e30a3121684e165261a9d6af4d63385fc4f39a54a49ac3b32ea8" + "sha256:0d66d18430c2201dc7fe85134277382baaa15e6b30979f3f3bdbabd6dbdb6046", + "sha256:ab670f55040722b49bb45865b2e93b824450fb4aef638b00d7acb493a9020434" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==1.0.2" + "version": "==1.0.3" }, "mkdocstrings-python": { "hashes": [ - "sha256:66ecff45c5f8b71bf174e11d49afc845c2dfc7fc0ab17a86b6b337e0f24d8d90", - "sha256:843a562221e6a471fefdd4b45cc6c22d2607ccbad632879234fa9692e9cf7732" + "sha256:0b83513478bdfd803ff05aa43e9b1fca9dd22bcd9471f09ca6257f009bc5ee12", + "sha256:c518632751cc869439b31c9d3177678ad2bfa5c21b79b863956ad68fc92c13b8" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==2.0.1" + "version": "==2.0.3" }, "mypy-extensions": { "hashes": [ @@ -531,36 +547,44 @@ }, "platformdirs": { "hashes": [ - "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", - "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31" + "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", + "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868" ], "markers": "python_version >= '3.10'", - "version": "==4.5.1" + "version": "==4.9.4" + }, + "properdocs": { + "hashes": [ + "sha256:6fa0cfa2e01bf338f684892c8a506cf70ea88ae7f3479c933b6fa20168101cbd", + "sha256:adc7b16e562890af0e098a7e5b02e3a81c20894a87d6a28d345c9300de73c26e" + ], + "markers": "python_version >= '3.9'", + "version": "==1.6.7" }, "pygments": { "hashes": [ - "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", - "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b" + "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", + "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176" ], - "markers": "python_version >= '3.8'", - "version": "==2.19.2" + "markers": "python_version >= '3.9'", + "version": "==2.20.0" }, "pygount": { "hashes": [ - "sha256:2f4b064ac1689ba43bdf84bef244e7a45f27e315feae6fb19d1209c56a580361", - "sha256:974aaf7a77d7f93aefc220639a897364cdd88a77e5a102d7063f73f8b7100955" + "sha256:1ceffaab9a72357bcf28cb13b8b51c1399b8f9b67798008fdf3d27f8cd1d8c09", + "sha256:404068a91019a0d3e451dbdee4e78c39cc30a4700c2a10245337af64681a2376" ], "index": "pypi", - "markers": "python_version >= '3.9' and python_version < '4'", - "version": "==3.1.0" + "markers": "python_version >= '3.10' and python_version < '4'", + "version": "==3.1.1" }, "pymdown-extensions": { "hashes": [ - "sha256:24af7feacbca56504b313b7b418c4f5e1317bb5fea60f03d57be7fcc40912aa0", - "sha256:e7e39c865727338d434b55f1dd8da51febcffcaebd6e1a0b9c836243f660740a" + "sha256:5c0fd2a2bea14eb39af8ff284f1066d898ab2187d81b889b75d46d4348c01638", + "sha256:c3f55a5b8a1d0edf6699e35dcbea71d978d34ff3fa79f3d807b8a5b3fa90fbdc" ], "markers": "python_version >= '3.9'", - "version": "==10.20.1" + "version": "==10.21.2" }, "python-dateutil": { "hashes": [ @@ -706,157 +730,56 @@ "markers": "python_version >= '3.9'", "version": "==1.1" }, - "regex": { - "hashes": [ - "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c", - "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60", - "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d", - "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d", - "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67", - "sha256:072623554418a9911446278f16ecb398fb3b540147a7828c06e2011fa531e773", - "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", - "sha256:08986dce1339bc932923e7d1232ce9881499a0e02925f7402fb7c982515419ef", - "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", - "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", - "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", - "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114", - "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", - "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", - "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e", - "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3", - "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", - "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d", - "sha256:220902c3c5cc6af55d4fe19ead504de80eb91f786dc102fbd74894b1551f095e", - "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", - "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7", - "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f", - "sha256:2e34b51b650b23ed3354b5a07aab37034d9f923db2a40519139af34f485f77d0", - "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", - "sha256:3a51ccc315653ba012774efca4f23d1d2a8a8f278a6072e29c7147eee7da446b", - "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", - "sha256:40291b1b89ca6ad8d3f2b82782cc33807f1406cf68c8d440861da6304d8ffbbd", - "sha256:41758407fc32d5c3c5de163888068cfee69cb4c2be844e7ac517a52770f9af57", - "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34", - "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", - "sha256:50153825ee016b91549962f970d6a4442fa106832e14c918acd1c8e479916c4f", - "sha256:5056b185ca113c88e18223183aa1a50e66507769c9640a6ff75859619d73957b", - "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519", - "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", - "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", - "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638", - "sha256:5670bce7b200273eee1840ef307bfa07cda90b38ae56e9a6ebcc9f50da9c469b", - "sha256:5704e174f8ccab2026bd2f1ab6c510345ae8eac818b613d7d73e785f1310f839", - "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", - "sha256:5e7e351589da0850c125f1600a4c4ba3c722efefe16b297de54300f08d734fbf", - "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", - "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0", - "sha256:684d7a212682996d21ca12ef3c17353c021fe9de6049e19ac8481ec35574a70f", - "sha256:69ab78f848845569401469da20df3e081e6b5a11cb086de3eed1d48f5ed57c95", - "sha256:6f44ec28b1f858c98d3036ad5d7d0bfc568bdd7a74f9c24e25f41ef1ebfd81a4", - "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", - "sha256:764e71f22ab3b305e7f4c21f1a97e1526a25ebdd22513e251cf376760213da13", - "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", - "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", - "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008", - "sha256:86fddba590aad9208e2fa8b43b4c098bb0ec74f15718bb6a704e3c63e2cef3e9", - "sha256:89d75e7293d2b3e674db7d4d9b1bee7f8f3d1609428e293771d1a962617150cc", - "sha256:93c0b12d3d3bc25af4ebbf38f9ee780a487e8bf6954c115b9f015822d3bb8e48", - "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20", - "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89", - "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", - "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf", - "sha256:a03e02f48cd1abbd9f3b7e3586d97c8f7a9721c436f51a5245b3b9483044480b", - "sha256:a36fdf2af13c2b14738f6e973aba563623cb77d753bbbd8d414d18bfaa3105dd", - "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", - "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", - "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", - "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", - "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45", - "sha256:ad182d02e40de7459b73155deb8996bbd8e96852267879396fb274e8700190e3", - "sha256:b2837718570f95dd41675328e111345f9b7095d821bac435aac173ac80b19983", - "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e", - "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", - "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", - "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", - "sha256:ba9b72e5643641b7d41fa1f6d5abda2c9a263ae835b917348fc3c928182ad467", - "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", - "sha256:bb8f74f2f10dbf13a0be8de623ba4f9491faf58c24064f32b65679b021ed0001", - "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", - "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55", - "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9", - "sha256:cdf58d0e516ee426a48f7b2c03a332a4114420716d55769ff7108c37a09951bf", - "sha256:d1cee317bfc014c2419a76bcc87f071405e3966da434e03e13beb45f8aced1a6", - "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e", - "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde", - "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62", - "sha256:df951c5f4a1b1910f1a99ff42c473ff60f8225baa1cdd3539fe2819d9543e9df", - "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", - "sha256:ea1bfda2f7162605f6e8178223576856b3d791109f15ea99a9f95c16a7636fb5", - "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86", - "sha256:f056bf21105c2515c32372bbc057f43eb02aae2fda61052e2f7622c801f0b4e2", - "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2", - "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0", - "sha256:f654882311409afb1d780b940234208a252322c24a93b442ca714d119e68086c", - "sha256:f65557897fc977a44ab205ea871b690adaef6b9da6afda4790a2484b04293a5f", - "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", - "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2", - "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", - "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91" - ], - "markers": "python_version >= '3.8'", - "version": "==2024.11.6" - }, "requests": { "hashes": [ - "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", - "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf" + "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", + "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a" ], "index": "pypi", - "markers": "python_version >= '3.9'", - "version": "==2.32.5" + "markers": "python_version >= '3.10'", + "version": "==2.33.1" }, "rich": { "hashes": [ - "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", - "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725" + "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", + "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b" ], "markers": "python_full_version >= '3.8.0'", - "version": "==14.0.0" + "version": "==14.3.3" }, "ruff": { "hashes": [ - "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df", - "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de", - "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66", - "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906", - "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b", - "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b", - "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480", - "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b", - "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8", - "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd", - "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", - "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c", - "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed", - "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167", - "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974", - "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3", - "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412", - "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13", - "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e" + "sha256:04f79eff02a72db209d47d665ba7ebcad609d8918a134f86cb13dd132159fc89", + "sha256:0f29b989a55572fb885b77464cf24af05500806ab4edf9a0fd8977f9759d85b1", + "sha256:12e617fc01a95e5821648a6df341d80456bd627bfab8a829f7cfc26a14a4b4a3", + "sha256:2033f963c43949d51e6fdccd3946633c6b37c484f5f98c3035f49c27395a8ab8", + "sha256:432701303b26416d22ba696c39f2c6f12499b89093b61360abc34bcc9bf07762", + "sha256:6ee3ae5c65a42f273f126686353f2e08ff29927b7b7e203b711514370d500de3", + "sha256:75e5cd06b1cf3f47a3996cfc999226b19aa92e7cce682dcd62f80d7035f98f49", + "sha256:8d9a5b8ea13f26ae90838afc33f91b547e61b794865374f114f349e9036835fb", + "sha256:995f11f63597ee362130d1d5a327a87cb6f3f5eae3094c620bcc632329a4d26e", + "sha256:ac51d486bf457cdc985a412fb1801b2dfd1bd8838372fc55de64b1510eff4bec", + "sha256:bc1f0a51254ba21767bfa9a8b5013ca8149dcf38092e6a9eb704d876de94dc34", + "sha256:c2a33a529fb3cbc23a7124b5c6ff121e4d6228029cba374777bd7649cc8598b8", + "sha256:c9861eb959edab053c10ad62c278835ee69ca527b6dcd72b47d5c1e5648964f6", + "sha256:cbe05adeba76d58162762d6b239c9056f1a15a55bd4b346cfd21e26cd6ad7bc7", + "sha256:cf891fa8e3bb430c0e7fac93851a5978fc99c8fa2c053b57b118972866f8e5f2", + "sha256:d3e3d0b6ba8dca1b7ef9ab80a28e840a20070c4b62e56d675c24f366ef330570", + "sha256:d910ae974b7a06a33a057cb87d2a10792a3b2b3b35e33d2699fdf63ec8f6b17a", + "sha256:fdce027ada77baa448077ccc6ebb2fa9c3c62fd110d8659d601cf2f475858d94" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==0.14.14" + "version": "==0.15.8" }, "setuptools": { "hashes": [ - "sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70", - "sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173" + "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", + "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==80.10.2" + "version": "==82.0.1" }, "six": { "hashes": [ @@ -868,79 +791,17 @@ }, "smmap": { "hashes": [ - "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", - "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e" + "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c", + "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f" ], "markers": "python_version >= '3.7'", - "version": "==5.0.2" - }, - "tomli": { - "hashes": [ - "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", - "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", - "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", - "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", - "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", - "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", - "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", - "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", - "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", - "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", - "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", - "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", - "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", - "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", - "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", - "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", - "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", - "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", - "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", - "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", - "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", - "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", - "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", - "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", - "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", - "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", - "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", - "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", - "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", - "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", - "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", - "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", - "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", - "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", - "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", - "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", - "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", - "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", - "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", - "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", - "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", - "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", - "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", - "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", - "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", - "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", - "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087" - ], - "markers": "python_version >= '3.8'", - "version": "==2.4.0" - }, - "typing-extensions": { - "hashes": [ - "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", - "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548" - ], - "markers": "python_version >= '3.9'", - "version": "==4.15.0" + "version": "==5.0.3" }, "urllib3": { "hashes": [ "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4" ], - "index": "pypi", "markers": "python_version >= '3.9'", "version": "==2.6.3" }, @@ -984,118 +845,123 @@ "develop": { "astroid": { "hashes": [ - "sha256:ac8fb7ca1c08eb9afec91ccc23edbd8ac73bb22cbdd7da1d488d9fb8d6579070", - "sha256:d7546c00a12efc32650b19a2bb66a153883185d3179ab0d4868086f807338b9b" + "sha256:52f39653876c7dec3e3afd4c2696920e05c83832b9737afc21928f2d2eb7a753", + "sha256:986fed8bcf79fb82c78b18a53352a0b287a73817d6dbcfba3162da36667c49a0" ], "markers": "python_full_version >= '3.10.0'", - "version": "==4.0.2" - }, - "colorama": { - "hashes": [ - "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", - "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" - ], - "index": "pypi", - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", - "version": "==0.4.6" + "version": "==4.0.4" }, "dill": { "hashes": [ - "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0", - "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049" + "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", + "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa" ], - "markers": "python_version >= '3.8'", - "version": "==0.4.0" + "markers": "python_version >= '3.9'", + "version": "==0.4.1" }, "isort": { "hashes": [ - "sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1", - "sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187" + "sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d", + "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75" ], "markers": "python_full_version >= '3.10.0'", - "version": "==7.0.0" + "version": "==8.0.1" }, "librt": { "hashes": [ - "sha256:060bde69c3604f694bd8ae21a780fe8be46bb3dbb863642e8dfc75c931ca8eee", - "sha256:08153ea537609d11f774d2bfe84af39d50d5c9ca3a4d061d946e0c9d8bce04a1", - "sha256:0b4791202296ad51ac09a3ff58eb49d9da8e3a4009167a6d76ac418a974e5fd4", - "sha256:0b7f080ba30601dfa3e3deed3160352273e1b9bc92e652f51103c3e9298f7899", - "sha256:0e2bf8f91093fac43e3eaebacf777f12fd539dce9ec5af3efc6d8424e96ccd49", - "sha256:118716de5ad6726332db1801bc90fa6d94194cd2e07c1a7822cebf12c496714d", - "sha256:168e04663e126416ba712114050f413ac306759a1791d87b7c11d4428ba75760", - "sha256:191cbd42660446d67cf7a95ac7bfa60f49b8b3b0417c64f216284a1d86fc9335", - "sha256:263f4fae9eba277513357c871275b18d14de93fd49bf5e43dc60a97b81ad5eb8", - "sha256:265729b551a2dd329cc47b323a182fb7961af42abf21e913c9dd7d3331b2f3c2", - "sha256:26b46620e1e0e45af510d9848ea0915e7040605dd2ae94ebefb6c962cbb6f7ec", - "sha256:29c8d2fae11d4379ea207ba7fc69d43237e42cf8a9f90ec6e05993687e6d648b", - "sha256:2fcbf2e135c11f721193aa5f42ba112bb1046afafbffd407cbc81d8d735c74d0", - "sha256:39183abee670bc37b85f11e86c44a9cad1ed6efa48b580083e89ecee13dd9717", - "sha256:3919c9407faeeee35430ae135e3a78acd4ecaaaa73767529e2c15ca1d73ba325", - "sha256:3c98a8d0ac9e2a7cb8ff8c53e5d6e8d82bfb2839abf144fdeaaa832f2a12aa45", - "sha256:3dd58f7ce20360c6ce0c04f7bd9081c7f9c19fc6129a3c705d0c5a35439f201d", - "sha256:46aa91813c267c3f60db75d56419b42c0c0b9748ec2c568a0e3588e543fb4233", - "sha256:49c596cd18e90e58b7caa4d7ca7606049c1802125fcff96b8af73fa5c3870e4d", - "sha256:532ddc6a8a6ca341b1cd7f4d999043e4c71a212b26fe9fd2e7f1e8bb4e873544", - "sha256:54d2aef0b0f5056f130981ad45081b278602ff3657fe16c88529f5058038e802", - "sha256:553dc58987d1d853adda8aeadf4db8e29749f0b11877afcc429a9ad892818ae2", - "sha256:584cb3e605ec45ba350962cec853e17be0a25a772f21f09f1e422f7044ae2a7d", - "sha256:5cd3afd71e9bc146203b6c8141921e738364158d4aa7cdb9a874e2505163770f", - "sha256:63055d3dda433ebb314c9f1819942f16a19203c454508fdb2d167613f7017169", - "sha256:654fdc971c76348a73af5240d8e2529265b9a7ba6321e38dd5bae7b0d4ab3abe", - "sha256:6b7b58913d475911f6f33e8082f19dd9b120c4f4a5c911d07e395d67b81c6982", - "sha256:6e860909fea75baef941ee6436e0453612505883b9d0d87924d4fda27865b9a2", - "sha256:730be847daad773a3c898943cf67fb9845a3961d06fb79672ceb0a8cd8624cfa", - "sha256:732e0aa0385b59a1b2545159e781c792cc58ce9c134249233a7c7250a44684c4", - "sha256:75965c1f4efb7234ff52a58b729d245a21e87e4b6a26a0ec08052f02b16274e4", - "sha256:7a488908a470451338607650f1c064175094aedebf4a4fa37890682e30ce0b57", - "sha256:81056e01bba1394f1d92904ec61a4078f66df785316275edbaf51d90da8c6e26", - "sha256:82f3f088482e2229387eadf8215c03f7726d56f69cce8c0c40f0795aebc9b361", - "sha256:84d4a6b9efd6124f728558a18e79e7cc5c5d4efc09b2b846c910de7e564f5bad", - "sha256:85f485b7471571e99fab4f44eeb327dc0e1f814ada575f3fa85e698417d8a54e", - "sha256:8a3cfb15961e7333ea6ef033dc574af75153b5c230d5ad25fbcd55198f21e0cf", - "sha256:8bb7883c1e94ceb87c2bf81385266f032da09cd040e804cc002f2c9d6b842e2f", - "sha256:8dcae24de1bc9da93aa689cb6313c70e776d7cea2fcf26b9b6160fedfe6bd9af", - "sha256:9937574e6d842f359b8585903d04f5b4ab62277a091a93e02058158074dc52f2", - "sha256:9bbb8facc5375476d392990dd6a71f97e4cb42e2ac66f32e860f6e47299d5e89", - "sha256:9c08527055fbb03c641c15bbc5b79dd2942fb6a3bd8dabf141dd7e97eeea4904", - "sha256:9cffa3ef0af29687455161cb446eff059bf27607f95163d6a37e27bcb37180f6", - "sha256:9f85f9b5db87b0f52e53c68ad2a0c5a53e00afa439bd54a1723742a2b1021276", - "sha256:a82d5a0ee43aeae2116d7292c77cc8038f4841830ade8aa922e098933b468b9e", - "sha256:a9eacbf983319b26b5f340a2e0cd47ac1ee4725a7f3a72fd0f15063c934b69d6", - "sha256:ab4b0d3bee6f6ff7017e18e576ac7e41a06697d8dea4b8f3ab9e0c8e1300c409", - "sha256:af69d9e159575e877c7546d1ee817b4ae089aa221dd1117e20c24ad8dc8659c7", - "sha256:b1795c4b2789b458fa290059062c2f5a297ddb28c31e704d27e161386469691a", - "sha256:b591c094afd0ffda820e931148c9e48dc31a556dc5b2b9b3cc552fa710d858e4", - "sha256:b8e0fd344bad57026a8f4ccfaf406486c2fc991838050c2fef156170edc3b775", - "sha256:ba1077c562a046208a2dc6366227b3eeae8f2c2ab4b41eaf4fd2fa28cece4203", - "sha256:bb41f04046b4f22b1e7ba5ef513402cd2e3477ec610e5f92d38fe2bba383d419", - "sha256:c039bbf79a9a2498404d1ae7e29a6c175e63678d7a54013a97397c40aee026c5", - "sha256:c566a4672564c5d54d8ab65cdaae5a87ee14c1564c1a2ddc7a9f5811c750f023", - "sha256:cda8b025875946ffff5a9a7590bf9acde3eb02cb6200f06a2d3e691ef3d9955b", - "sha256:cdb001a1a0e4f41e613bca2c0fc147fc8a7396f53fc94201cbfd8ec7cd69ca4b", - "sha256:cdde31759bd8888f3ef0eebda80394a48961328a17c264dce8cc35f4b9cde35d", - "sha256:d2cc7d187e8c6e9b7bdbefa9697ce897a704ea7a7ce844f2b4e0e2aa07ae51d3", - "sha256:d7aa33153a5bb0bac783d2c57885889b1162823384e8313d47800a0e10d0070e", - "sha256:d7c72c8756eeb3aefb1b9e3dac7c37a4a25db63640cac0ab6fc18e91a0edf05a", - "sha256:dd810f2d39c526c42ea205e0addad5dc08ef853c625387806a29d07f9d150d9b", - "sha256:ddc0ab9dbc5f9ceaf2bf7a367bf01f2697660e908f6534800e88f43590b271db", - "sha256:ddc4a16207f88f9597b397fc1f60781266d13b13de922ff61c206547a29e4bbd", - "sha256:de4221a1181fa9c8c4b5f35506ed6f298948f44003d84d2a8b9885d7e01e6cfa", - "sha256:df2e210400b28e50994477ebf82f055698c79797b6ee47a1669d383ca33263e1", - "sha256:df3146d52465b3b6397d25d513f428cb421c18df65b7378667bb5f1e3cc45805", - "sha256:e47fc52602ffc374e69bf1b76536dc99f7f6dd876bd786c8213eaa3598be030a", - "sha256:e9e9c988b5ffde7be02180f864cbd17c0b0c1231c235748912ab2afa05789c25", - "sha256:ea1b60b86595a5dc1f57b44a801a1c4d8209c0a69518391d349973a4491408e6", - "sha256:edf6b465306215b19dbe6c3fb63cf374a8f3e1ad77f3b4c16544b83033bbb67b", - "sha256:f02c4337bf271c4f06637f5ff254fad2238c0b8e32a3a480ebb2fc5e26f754a5", - "sha256:f7f51ffe59f4556243d3cc82d827bde74765f594fa3ceb80ec4de0c13ccd3416", - "sha256:f952e1a78c480edee8fb43aa2bf2e84dcd46c917d44f8065b883079d3893e8fc", - "sha256:fb565b4219abc8ea2402e61c7ba648a62903831059ed3564fa1245cc245d58d7", - "sha256:fee15c2a190ef389f14928135c6fb2d25cd3fdb7887bfd9a7b444bbdc8c06b96" + "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6", + "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d", + "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440", + "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a", + "sha256:08eec3a1fc435f0d09c87b6bf1ec798986a3544f446b864e4099633a56fcd9ed", + "sha256:0bf69d79a23f4f40b8673a947a234baeeb133b5078b483b7297c5916539cf5d5", + "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04", + "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3", + "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d", + "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14", + "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972", + "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c", + "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78", + "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732", + "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c", + "sha256:22b46eabd76c1986ee7d231b0765ad387d7673bbd996aa0d0d054b38ac65d8f6", + "sha256:237796479f4d0637d6b9cbcb926ff424a97735e68ade6facf402df4ec93375ed", + "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1", + "sha256:2cc68eeeef5e906839c7bb0815748b5b0a974ec27125beefc0f942715785b551", + "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7", + "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382", + "sha256:3dff3d3ca8db20e783b1bc7de49c0a2ab0b8387f31236d6a026597d07fcd68ac", + "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a", + "sha256:439352ba9373f11cb8e1933da194dcc6206daf779ff8df0ed69c5e39113e6a99", + "sha256:4998009e7cb9e896569f4be7004f09d0ed70d386fa99d42b6d363f6d200501ac", + "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb", + "sha256:4beb04b8c66c6ae62f8c1e0b2f097c1ebad9295c929a8d5286c05eae7c2fc7dc", + "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7", + "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0", + "sha256:52c224e14614b750c0a6d97368e16804a98c684657c7518752c356834fff83bb", + "sha256:56e04c14b696300d47b3bc5f1d10a00e86ae978886d0cee14e5714fafb5df5d2", + "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7", + "sha256:5cdc0f588ff4b663ea96c26d2a230c525c6fc62b28314edaaaca8ed5af931ad0", + "sha256:5db05697c82b3a2ec53f6e72b2ed373132b0c2e05135f0696784e97d7f5d48e7", + "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363", + "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624", + "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9", + "sha256:64548cde61b692dc0dc379f4b5f59a2f582c2ebe7890d09c1ae3b9e66fa015b7", + "sha256:681dc2451d6d846794a828c16c22dc452d924e9f700a485b7ecb887a30aad1fd", + "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3", + "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f", + "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", + "sha256:747328be0c5b7075cde86a0e09d7a9196029800ba75a1689332348e998fb85c0", + "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb", + "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e", + "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc", + "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071", + "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730", + "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35", + "sha256:81fd938344fecb9373ba1b155968c8a329491d2ce38e7ddb76f30ffb938f12dc", + "sha256:82210adabbc331dbb65d7868b105185464ef13f56f7f76688565ad79f648b0fe", + "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", + "sha256:8f1125e6bbf2f1657d9a2f3ccc4a2c9b0c8b176965bb565dd4d86be67eddb4b6", + "sha256:8f4bb453f408137d7581be309b2fbc6868a80e7ef60c88e689078ee3a296ae71", + "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0", + "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d", + "sha256:97c2b54ff6717a7a563b72627990bec60d8029df17df423f0ed37d56a17a176b", + "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040", + "sha256:9e2c6f77b9ad48ce5603b83b7da9ee3e36b3ab425353f695cba13200c5d96596", + "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a", + "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee", + "sha256:a3b4350b13cc0e6f5bec8fa7caf29a8fb8cdc051a3bae45cfbfd7ce64f009965", + "sha256:aaab0e307e344cb28d800957ef3ec16605146ef0e59e059a60a176d19543d1b7", + "sha256:ac1e7817fd0ed3d14fd7c5df91daed84c48e4c2a11ee99c0547f9f62fdae13da", + "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9", + "sha256:b6d7ab1f01aa753188605b09a51faa44a3327400b00b8cce424c71910fc0a128", + "sha256:bacdb58d9939d95cc557b4dbaa86527c9db2ac1ed76a18bc8d26f6dc8647d851", + "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", + "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", + "sha256:c00e5c884f528c9932d278d5c9cbbea38a6b81eb62c02e06ae53751a83a4d52b", + "sha256:c25d9e338d5bed46c1632f851babf3d13c78f49a225462017cf5e11e845c5891", + "sha256:c336d61d2fe74a3195edc1646d53ff1cddd3a9600b09fa6ab75e5514ba4862a7", + "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994", + "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583", + "sha256:d0ee06b5b5291f609ddb37b9750985b27bc567791bc87c76a569b3feed8481ac", + "sha256:d480de377f5b687b6b1bc0c0407426da556e2a757633cc7e4d2e1a057aa688f3", + "sha256:d56bc4011975f7460bea7b33e1ff425d2f1adf419935ff6707273c77f8a4ada6", + "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921", + "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0", + "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79", + "sha256:e3f0a41487fd5fad7e760b9e8a90e251e27c2816fbc2cff36a22a0e6bcbbd9dd", + "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012", + "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023", + "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4", + "sha256:eb5656019db7c4deacf0c1a55a898c5bb8f989be904597fcb5232a2f4828fa05", + "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c", + "sha256:f0af2bd2bc204fa27f3d6711d0f360e6b8c684a035206257a81673ab924aa11e", + "sha256:f7cdf7f26c2286ffb02e46d7bac56c94655540b26347673bea15fa52a6af17e9", + "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b", + "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444" ], "markers": "python_version >= '3.9'", - "version": "==0.7.5" + "version": "==0.8.1" }, "mccabe": { "hashes": [ @@ -1107,48 +973,54 @@ }, "mypy": { "hashes": [ - "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", - "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", - "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", - "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", - "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", - "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", - "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", - "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", - "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", - "sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24", - "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", - "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", - "sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e", - "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", - "sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3", - "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", - "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", - "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", - "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", - "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", - "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", - "sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67", - "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", - "sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a", - "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", - "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", - "sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376", - "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", - "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", - "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", - "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", - "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", - "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", - "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", - "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", - "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", - "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", - "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e" + "sha256:002b613ae19f4ac7d18b7e168ffe1cb9013b37c57f7411984abbd3b817b0a214", + "sha256:00e047c74d3ec6e71a2eb88e9ea551a2edb90c21f993aefa9e0d2a898e0bb732", + "sha256:02cca0761c75b42a20a2757ae58713276605eb29a08dd8a6e092aa347c4115ca", + "sha256:0ecd63f75fdd30327e4ad8b5704bd6d91fc6c1b2e029f8ee14705e1207212489", + "sha256:0f42dfaab7ec1baff3b383ad7af562ab0de573c5f6edb44b2dab016082b89948", + "sha256:1973868d2adbb4584a3835780b27436f06d1dc606af5be09f187aaa25be1070f", + "sha256:26c8b52627b6552f47ff11adb4e1509605f094e29815323e487fc0053ebe93d1", + "sha256:2721f0ce49cb74a38f00c50da67cb7d36317b5eda38877a49614dc018e91c787", + "sha256:2fcedb16d456106e545b2bfd7ef9d24e70b38ec252d2a629823a4d07ebcdb69e", + "sha256:31b5dbb55293c1bd27c0fc813a0d2bb5ceef9d65ac5afa2e58f829dab7921fd5", + "sha256:34506397dbf40c15dc567635d18a21d33827e9ab29014fb83d292a8f4f8953b6", + "sha256:367e5c993ba34d5054d11937d0485ad6dfc60ba760fa326c01090fc256adf15c", + "sha256:379edf079ce44ac8d2805bcf9b3dd7340d4f97aad3a5e0ebabbf9d125b84b442", + "sha256:39362cdb4ba5f916e7976fccecaab1ba3a83e35f60fa68b64e9a70e221bb2436", + "sha256:4525e7010b1b38334516181c5b81e16180b8e149e6684cee5a727c78186b4e3b", + "sha256:47781555a7aa5fedcc2d16bcd72e0dc83eb272c10dd657f9fb3f9cc08e2e6abb", + "sha256:49d11c6f573a5a08f77fad13faff2139f6d0730ebed2cfa9b3d2702671dd7188", + "sha256:555493c44a4f5a1b58d611a43333e71a9981c6dbe26270377b6f8174126a0526", + "sha256:555658c611099455b2da507582ea20d2043dfdfe7f5ad0add472b1c6238b433f", + "sha256:697f102c5c1d526bdd761a69f17c6070f9892eebcb94b1a5963d679288c09e78", + "sha256:76a70bf840495729be47510856b978f1b0ec7d08f257ca38c9d932720bf6b43e", + "sha256:7d3243c406773185144527f83be0e0aefc7bf4601b0b2b956665608bf7c98a83", + "sha256:931a7630bba591593dcf6e97224a21ff80fb357e7982628d25e3c618e7f598ef", + "sha256:9804c3ad27f78e54e58b32e7cb532d128b43dbfb9f3f9f06262b821a0f6bd3f5", + "sha256:a17c5d0bdcca61ce24a35beb828a2d0d323d3fcf387d7512206888c900193367", + "sha256:a6e0641147cbfa7e4e94efdb95c2dab1aff8cfc159ded13e07f308ddccc8c48e", + "sha256:a79c1eba7ac4209f2d850f0edd0a2f8bba88cbfdfefe6fb76a19e9d4fe5e71a2", + "sha256:a9336b5e6712f4adaf5afc3203a99a40b379049104349d747eb3e5a3aa23ac2e", + "sha256:b20c8b0fd5877abdf402e79a3af987053de07e6fb208c18df6659f708b535134", + "sha256:b3a49064504be59e59da664c5e149edc1f26c67c4f8e8456f6ba6aba55033018", + "sha256:b503ab55a836136b619b5fc21c8803d810c5b87551af8600b72eecafb0059cb0", + "sha256:bd0212976dc57a5bfeede7c219e7cd66568a32c05c9129686dd487c059c1b88a", + "sha256:c70380fe5d64010f79fb863b9081c7004dd65225d2277333c219d93a10dad4dd", + "sha256:d99f515f95fd03a90875fdb2cca12ff074aa04490db4d190905851bdf8a549a8", + "sha256:e80cf77847d0d3e6e3111b7b25db32a7f8762fd4b9a3a72ce53fe16a2863b281", + "sha256:eb96c84efcc33f0b5e0e04beacf00129dd963b67226b01c00b9dfc8affb464c3", + "sha256:ebea00201737ad4391142808ed16e875add5c17f676e0912b387739f84991e13", + "sha256:efe8d70949c3023698c3fca1e94527e7e790a361ab8116f90d11221421cd8726", + "sha256:f13b3e41bce9d257eded794c0f12878af3129d80aacd8a3ee0dee51f3a978651", + "sha256:f194db59657c58593a3c47c6dfd7bad4ef4ac12dbc94d01b3a95521f78177e33", + "sha256:f49590891d2c2f8a9de15614e32e459a794bcba84693c2394291a2038bbaaa69", + "sha256:f75ff57defcd0f1d6e006d721ccdec6c88d4f6a7816eb92f1c4890d979d9ee62", + "sha256:f799d9db89fc00446f03281f84a221e50018fc40113a3ba9864b132895619ebe", + "sha256:f8426d4d75d68714abc17a4292d922f6ba2cfb984b72c2278c437f6dae797865" ], "index": "pypi", - "markers": "python_version >= '3.9'", - "version": "==1.19.1" + "markers": "python_version >= '3.10'", + "version": "==1.20.0" }, "mypy-extensions": { "hashes": [ @@ -1160,84 +1032,36 @@ }, "pathspec": { "hashes": [ - "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", - "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" + "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", + "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723" ], - "markers": "python_version >= '3.8'", - "version": "==0.12.1" + "markers": "python_version >= '3.9'", + "version": "==1.0.4" }, "platformdirs": { "hashes": [ - "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", - "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3" + "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", + "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868" ], "markers": "python_version >= '3.10'", - "version": "==4.5.0" + "version": "==4.9.4" }, "pylint": { "hashes": [ - "sha256:63e06a37d5922555ee2c20963eb42559918c20bd2b21244e4ef426e7c43b92e0", - "sha256:d9b71674e19b1c36d79265b5887bf8e55278cbe236c9e95d22dc82cf044fdbd2" + "sha256:00f51c9b14a3b3ae08cff6b2cdd43f28165c78b165b628692e428fb1f8dc2cf2", + "sha256:8cd6a618df75deb013bd7eb98327a95f02a6fb839205a6bbf5456ef96afb317c" ], "index": "pypi", "markers": "python_full_version >= '3.10.0'", - "version": "==4.0.4" - }, - "tomli": { - "hashes": [ - "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", - "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", - "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", - "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", - "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", - "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", - "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", - "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", - "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", - "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", - "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", - "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", - "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", - "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", - "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", - "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", - "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", - "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", - "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", - "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", - "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", - "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", - "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", - "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", - "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", - "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", - "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", - "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", - "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", - "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", - "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", - "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", - "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", - "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", - "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", - "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", - "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", - "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", - "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", - "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", - "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", - "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876" - ], - "markers": "python_version >= '3.8'", - "version": "==2.3.0" + "version": "==4.0.5" }, "tomlkit": { "hashes": [ - "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", - "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0" + "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", + "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064" ], - "markers": "python_version >= '3.8'", - "version": "==0.13.3" + "markers": "python_version >= '3.9'", + "version": "==0.14.0" }, "typing-extensions": { "hashes": [ diff --git a/bin/netcore/engines/IPY2712PR/pyRevitAssemblyBuilder.dll b/bin/netcore/engines/IPY2712PR/pyRevitAssemblyBuilder.dll index 4bde66485..02bc37eba 100644 Binary files a/bin/netcore/engines/IPY2712PR/pyRevitAssemblyBuilder.dll and b/bin/netcore/engines/IPY2712PR/pyRevitAssemblyBuilder.dll differ diff --git a/bin/netcore/engines/IPY2712PR/pyRevitExtensionParser.dll b/bin/netcore/engines/IPY2712PR/pyRevitExtensionParser.dll index b7f30a925..a0465d9c4 100644 Binary files a/bin/netcore/engines/IPY2712PR/pyRevitExtensionParser.dll and b/bin/netcore/engines/IPY2712PR/pyRevitExtensionParser.dll differ diff --git a/bin/netcore/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2025.dll b/bin/netcore/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2025.dll index 9a572a80d..ed536c3a3 100644 Binary files a/bin/netcore/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2025.dll and b/bin/netcore/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2025.dll differ diff --git a/bin/netcore/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2026.dll b/bin/netcore/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2026.dll index 188af6494..8b98b5f2f 100644 Binary files a/bin/netcore/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2026.dll and b/bin/netcore/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2026.dll differ diff --git a/bin/netcore/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2027.dll b/bin/netcore/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2027.dll index 39a53c65c..eeeb7e484 100644 Binary files a/bin/netcore/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2027.dll and b/bin/netcore/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2027.dll differ diff --git a/bin/netcore/engines/IPY2712PR/pyRevitLoader.dll b/bin/netcore/engines/IPY2712PR/pyRevitLoader.dll index 4757afb85..c115e6d6d 100644 Binary files a/bin/netcore/engines/IPY2712PR/pyRevitLoader.dll and b/bin/netcore/engines/IPY2712PR/pyRevitLoader.dll differ diff --git a/bin/netcore/engines/IPY2712PR/pyRevitRunner.dll b/bin/netcore/engines/IPY2712PR/pyRevitRunner.dll index e513eaebb..5dacf3f60 100644 Binary files a/bin/netcore/engines/IPY2712PR/pyRevitRunner.dll and b/bin/netcore/engines/IPY2712PR/pyRevitRunner.dll differ diff --git a/bin/netcore/engines/IPY342/pyRevitAssemblyBuilder.dll b/bin/netcore/engines/IPY342/pyRevitAssemblyBuilder.dll index 4bde66485..02bc37eba 100644 Binary files a/bin/netcore/engines/IPY342/pyRevitAssemblyBuilder.dll and b/bin/netcore/engines/IPY342/pyRevitAssemblyBuilder.dll differ diff --git a/bin/netcore/engines/IPY342/pyRevitExtensionParser.dll b/bin/netcore/engines/IPY342/pyRevitExtensionParser.dll index b7f30a925..a0465d9c4 100644 Binary files a/bin/netcore/engines/IPY342/pyRevitExtensionParser.dll and b/bin/netcore/engines/IPY342/pyRevitExtensionParser.dll differ diff --git a/bin/netcore/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2025.dll b/bin/netcore/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2025.dll index 1be0e13b4..dbb70e957 100644 Binary files a/bin/netcore/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2025.dll and b/bin/netcore/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2025.dll differ diff --git a/bin/netcore/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2026.dll b/bin/netcore/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2026.dll index 58e473335..bc99c7268 100644 Binary files a/bin/netcore/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2026.dll and b/bin/netcore/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2026.dll differ diff --git a/bin/netcore/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2027.dll b/bin/netcore/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2027.dll index e654dfaef..4d76f285a 100644 Binary files a/bin/netcore/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2027.dll and b/bin/netcore/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2027.dll differ diff --git a/bin/netcore/engines/IPY342/pyRevitLoader.dll b/bin/netcore/engines/IPY342/pyRevitLoader.dll index d08c53a8a..d810cfb86 100644 Binary files a/bin/netcore/engines/IPY342/pyRevitLoader.dll and b/bin/netcore/engines/IPY342/pyRevitLoader.dll differ diff --git a/bin/netcore/engines/IPY342/pyRevitRunner.dll b/bin/netcore/engines/IPY342/pyRevitRunner.dll index d1036a80c..da2e6bda1 100644 Binary files a/bin/netcore/engines/IPY342/pyRevitRunner.dll and b/bin/netcore/engines/IPY342/pyRevitRunner.dll differ diff --git a/bin/netcore/pyRevitLabs.Common.dll b/bin/netcore/pyRevitLabs.Common.dll index 1fd0ac65e..dfc79b760 100644 Binary files a/bin/netcore/pyRevitLabs.Common.dll and b/bin/netcore/pyRevitLabs.Common.dll differ diff --git a/bin/netcore/pyRevitLabs.CommonCLI.dll b/bin/netcore/pyRevitLabs.CommonCLI.dll index 0020cb299..76bfe66d0 100644 Binary files a/bin/netcore/pyRevitLabs.CommonCLI.dll and b/bin/netcore/pyRevitLabs.CommonCLI.dll differ diff --git a/bin/netcore/pyRevitLabs.CommonWPF.dll b/bin/netcore/pyRevitLabs.CommonWPF.dll index dbe3f4137..fc9f940bb 100644 Binary files a/bin/netcore/pyRevitLabs.CommonWPF.dll and b/bin/netcore/pyRevitLabs.CommonWPF.dll differ diff --git a/bin/netcore/pyRevitLabs.DeffrelDB.dll b/bin/netcore/pyRevitLabs.DeffrelDB.dll index d3b05d08a..cd98d49f3 100644 Binary files a/bin/netcore/pyRevitLabs.DeffrelDB.dll and b/bin/netcore/pyRevitLabs.DeffrelDB.dll differ diff --git a/bin/netcore/pyRevitLabs.Emojis.dll b/bin/netcore/pyRevitLabs.Emojis.dll index 7dce420e4..f3e93be86 100644 Binary files a/bin/netcore/pyRevitLabs.Emojis.dll and b/bin/netcore/pyRevitLabs.Emojis.dll differ diff --git a/bin/netcore/pyRevitLabs.Language.dll b/bin/netcore/pyRevitLabs.Language.dll index 6b307503f..124097474 100644 Binary files a/bin/netcore/pyRevitLabs.Language.dll and b/bin/netcore/pyRevitLabs.Language.dll differ diff --git a/bin/netcore/pyRevitLabs.NLog.dll b/bin/netcore/pyRevitLabs.NLog.dll index 4dc3c0c86..8e061298e 100644 Binary files a/bin/netcore/pyRevitLabs.NLog.dll and b/bin/netcore/pyRevitLabs.NLog.dll differ diff --git a/bin/netcore/pyRevitLabs.PyRevit.Runtime.Shared.dll b/bin/netcore/pyRevitLabs.PyRevit.Runtime.Shared.dll index 88b19d16e..8efd6e071 100644 Binary files a/bin/netcore/pyRevitLabs.PyRevit.Runtime.Shared.dll and b/bin/netcore/pyRevitLabs.PyRevit.Runtime.Shared.dll differ diff --git a/bin/netcore/pyRevitLabs.PyRevit.dll b/bin/netcore/pyRevitLabs.PyRevit.dll index 58bda67c1..4a19a1a2c 100644 Binary files a/bin/netcore/pyRevitLabs.PyRevit.dll and b/bin/netcore/pyRevitLabs.PyRevit.dll differ diff --git a/bin/netcore/pyRevitLabs.TargetApps.AutoCAD.dll b/bin/netcore/pyRevitLabs.TargetApps.AutoCAD.dll index 388254c18..586b9f75d 100644 Binary files a/bin/netcore/pyRevitLabs.TargetApps.AutoCAD.dll and b/bin/netcore/pyRevitLabs.TargetApps.AutoCAD.dll differ diff --git a/bin/netcore/pyRevitLabs.TargetApps.Navisworks.dll b/bin/netcore/pyRevitLabs.TargetApps.Navisworks.dll index 6af63b101..4342b3fec 100644 Binary files a/bin/netcore/pyRevitLabs.TargetApps.Navisworks.dll and b/bin/netcore/pyRevitLabs.TargetApps.Navisworks.dll differ diff --git a/bin/netcore/pyRevitLabs.TargetApps.Revit.dll b/bin/netcore/pyRevitLabs.TargetApps.Revit.dll index 9fd5b030a..2840a7569 100644 Binary files a/bin/netcore/pyRevitLabs.TargetApps.Revit.dll and b/bin/netcore/pyRevitLabs.TargetApps.Revit.dll differ diff --git a/bin/netcore/pyRevitLabs.UnitTests.dll b/bin/netcore/pyRevitLabs.UnitTests.dll index 18bcdddd0..648d939bf 100644 Binary files a/bin/netcore/pyRevitLabs.UnitTests.dll and b/bin/netcore/pyRevitLabs.UnitTests.dll differ diff --git a/bin/netfx/engines/IPY2712PR/pyRevitAssemblyBuilder.dll b/bin/netfx/engines/IPY2712PR/pyRevitAssemblyBuilder.dll index 35496b1ac..fb3ce2565 100644 Binary files a/bin/netfx/engines/IPY2712PR/pyRevitAssemblyBuilder.dll and b/bin/netfx/engines/IPY2712PR/pyRevitAssemblyBuilder.dll differ diff --git a/bin/netfx/engines/IPY2712PR/pyRevitExtensionParser.dll b/bin/netfx/engines/IPY2712PR/pyRevitExtensionParser.dll index 0091d46df..6af0a0d3e 100644 Binary files a/bin/netfx/engines/IPY2712PR/pyRevitExtensionParser.dll and b/bin/netfx/engines/IPY2712PR/pyRevitExtensionParser.dll differ diff --git a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2017.dll b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2017.dll index 25f012fd6..ea8195b2b 100644 Binary files a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2017.dll and b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2017.dll differ diff --git a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2018.dll b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2018.dll index 56c3419c4..e34edbcbc 100644 Binary files a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2018.dll and b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2018.dll differ diff --git a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2019.dll b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2019.dll index 930739924..a0c7afd55 100644 Binary files a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2019.dll and b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2019.dll differ diff --git a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2020.dll b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2020.dll index fbc592f6d..6772eddbb 100644 Binary files a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2020.dll and b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2020.dll differ diff --git a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2021.dll b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2021.dll index dc51f7847..b08b7d7f1 100644 Binary files a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2021.dll and b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2021.dll differ diff --git a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2022.dll b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2022.dll index ccb1c593e..c6051f21d 100644 Binary files a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2022.dll and b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2022.dll differ diff --git a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2023.dll b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2023.dll index 5622366c0..2a2529e74 100644 Binary files a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2023.dll and b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2023.dll differ diff --git a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2024.dll b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2024.dll index aa98d5cab..0a992aee4 100644 Binary files a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2024.dll and b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2024.dll differ diff --git a/bin/netfx/engines/IPY2712PR/pyRevitLoader.dll b/bin/netfx/engines/IPY2712PR/pyRevitLoader.dll index b8fe8eddf..2de0ce417 100644 Binary files a/bin/netfx/engines/IPY2712PR/pyRevitLoader.dll and b/bin/netfx/engines/IPY2712PR/pyRevitLoader.dll differ diff --git a/bin/netfx/engines/IPY2712PR/pyRevitRunner.dll b/bin/netfx/engines/IPY2712PR/pyRevitRunner.dll index df0db993a..311e03374 100644 Binary files a/bin/netfx/engines/IPY2712PR/pyRevitRunner.dll and b/bin/netfx/engines/IPY2712PR/pyRevitRunner.dll differ diff --git a/bin/netfx/engines/IPY342/pyRevitAssemblyBuilder.dll b/bin/netfx/engines/IPY342/pyRevitAssemblyBuilder.dll index 35496b1ac..fb3ce2565 100644 Binary files a/bin/netfx/engines/IPY342/pyRevitAssemblyBuilder.dll and b/bin/netfx/engines/IPY342/pyRevitAssemblyBuilder.dll differ diff --git a/bin/netfx/engines/IPY342/pyRevitExtensionParser.dll b/bin/netfx/engines/IPY342/pyRevitExtensionParser.dll index 0091d46df..6af0a0d3e 100644 Binary files a/bin/netfx/engines/IPY342/pyRevitExtensionParser.dll and b/bin/netfx/engines/IPY342/pyRevitExtensionParser.dll differ diff --git a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2017.dll b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2017.dll index 0545730d0..b1bba620c 100644 Binary files a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2017.dll and b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2017.dll differ diff --git a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2018.dll b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2018.dll index 150395af1..7d82c11d4 100644 Binary files a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2018.dll and b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2018.dll differ diff --git a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2019.dll b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2019.dll index 298219c86..6596df9ad 100644 Binary files a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2019.dll and b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2019.dll differ diff --git a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2020.dll b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2020.dll index 1a33bee73..22477c19f 100644 Binary files a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2020.dll and b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2020.dll differ diff --git a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2021.dll b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2021.dll index 2b56f2f41..a15f3badf 100644 Binary files a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2021.dll and b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2021.dll differ diff --git a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2022.dll b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2022.dll index 42e00aab7..bce8c3dca 100644 Binary files a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2022.dll and b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2022.dll differ diff --git a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2023.dll b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2023.dll index 43935867d..d1a56db99 100644 Binary files a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2023.dll and b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2023.dll differ diff --git a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2024.dll b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2024.dll index 972f7006a..dc26656a1 100644 Binary files a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2024.dll and b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2024.dll differ diff --git a/bin/netfx/engines/IPY342/pyRevitLoader.dll b/bin/netfx/engines/IPY342/pyRevitLoader.dll index 7d051340e..d22ff4e90 100644 Binary files a/bin/netfx/engines/IPY342/pyRevitLoader.dll and b/bin/netfx/engines/IPY342/pyRevitLoader.dll differ diff --git a/bin/netfx/engines/IPY342/pyRevitRunner.dll b/bin/netfx/engines/IPY342/pyRevitRunner.dll index d6c65a2b5..cf02d63c3 100644 Binary files a/bin/netfx/engines/IPY342/pyRevitRunner.dll and b/bin/netfx/engines/IPY342/pyRevitRunner.dll differ diff --git a/bin/netfx/pyRevitLabs.Common.dll b/bin/netfx/pyRevitLabs.Common.dll index 987ed9619..d5882143c 100644 Binary files a/bin/netfx/pyRevitLabs.Common.dll and b/bin/netfx/pyRevitLabs.Common.dll differ diff --git a/bin/netfx/pyRevitLabs.CommonCLI.dll b/bin/netfx/pyRevitLabs.CommonCLI.dll index 53a8a95cb..af7ebecd7 100644 Binary files a/bin/netfx/pyRevitLabs.CommonCLI.dll and b/bin/netfx/pyRevitLabs.CommonCLI.dll differ diff --git a/bin/netfx/pyRevitLabs.CommonWPF.dll b/bin/netfx/pyRevitLabs.CommonWPF.dll index f3e88b1ab..aea318e57 100644 Binary files a/bin/netfx/pyRevitLabs.CommonWPF.dll and b/bin/netfx/pyRevitLabs.CommonWPF.dll differ diff --git a/bin/netfx/pyRevitLabs.DeffrelDB.dll b/bin/netfx/pyRevitLabs.DeffrelDB.dll index b22af6fd0..9d5d103d8 100644 Binary files a/bin/netfx/pyRevitLabs.DeffrelDB.dll and b/bin/netfx/pyRevitLabs.DeffrelDB.dll differ diff --git a/bin/netfx/pyRevitLabs.Emojis.dll b/bin/netfx/pyRevitLabs.Emojis.dll index 25b68da41..5f9022b77 100644 Binary files a/bin/netfx/pyRevitLabs.Emojis.dll and b/bin/netfx/pyRevitLabs.Emojis.dll differ diff --git a/bin/netfx/pyRevitLabs.Language.dll b/bin/netfx/pyRevitLabs.Language.dll index 0d4472cd8..4dde85a2c 100644 Binary files a/bin/netfx/pyRevitLabs.Language.dll and b/bin/netfx/pyRevitLabs.Language.dll differ diff --git a/bin/netfx/pyRevitLabs.NLog.dll b/bin/netfx/pyRevitLabs.NLog.dll index ff6f3096b..08eb6d05c 100644 Binary files a/bin/netfx/pyRevitLabs.NLog.dll and b/bin/netfx/pyRevitLabs.NLog.dll differ diff --git a/bin/netfx/pyRevitLabs.PyRevit.Runtime.Shared.dll b/bin/netfx/pyRevitLabs.PyRevit.Runtime.Shared.dll index 153c73d81..c7d620e02 100644 Binary files a/bin/netfx/pyRevitLabs.PyRevit.Runtime.Shared.dll and b/bin/netfx/pyRevitLabs.PyRevit.Runtime.Shared.dll differ diff --git a/bin/netfx/pyRevitLabs.PyRevit.dll b/bin/netfx/pyRevitLabs.PyRevit.dll index b663e5d96..cada4799f 100644 Binary files a/bin/netfx/pyRevitLabs.PyRevit.dll and b/bin/netfx/pyRevitLabs.PyRevit.dll differ diff --git a/bin/netfx/pyRevitLabs.TargetApps.AutoCAD.dll b/bin/netfx/pyRevitLabs.TargetApps.AutoCAD.dll index 2946d30d3..2760c4abb 100644 Binary files a/bin/netfx/pyRevitLabs.TargetApps.AutoCAD.dll and b/bin/netfx/pyRevitLabs.TargetApps.AutoCAD.dll differ diff --git a/bin/netfx/pyRevitLabs.TargetApps.Navisworks.dll b/bin/netfx/pyRevitLabs.TargetApps.Navisworks.dll index 3788f95eb..cb7b9b2af 100644 Binary files a/bin/netfx/pyRevitLabs.TargetApps.Navisworks.dll and b/bin/netfx/pyRevitLabs.TargetApps.Navisworks.dll differ diff --git a/bin/netfx/pyRevitLabs.TargetApps.Revit.dll b/bin/netfx/pyRevitLabs.TargetApps.Revit.dll index abf36dbab..90d23d434 100644 Binary files a/bin/netfx/pyRevitLabs.TargetApps.Revit.dll and b/bin/netfx/pyRevitLabs.TargetApps.Revit.dll differ diff --git a/bin/netfx/pyRevitLabs.UnitTests.dll b/bin/netfx/pyRevitLabs.UnitTests.dll index 7d8427c8a..f0e620d75 100644 Binary files a/bin/netfx/pyRevitLabs.UnitTests.dll and b/bin/netfx/pyRevitLabs.UnitTests.dll differ diff --git a/bin/pyRevitLabs.Common.dll b/bin/pyRevitLabs.Common.dll index 1fd0ac65e..dfc79b760 100644 Binary files a/bin/pyRevitLabs.Common.dll and b/bin/pyRevitLabs.Common.dll differ diff --git a/bin/pyRevitLabs.CommonCLI.dll b/bin/pyRevitLabs.CommonCLI.dll index 0020cb299..76bfe66d0 100644 Binary files a/bin/pyRevitLabs.CommonCLI.dll and b/bin/pyRevitLabs.CommonCLI.dll differ diff --git a/bin/pyRevitLabs.CommonWPF.dll b/bin/pyRevitLabs.CommonWPF.dll index dbe3f4137..fc9f940bb 100644 Binary files a/bin/pyRevitLabs.CommonWPF.dll and b/bin/pyRevitLabs.CommonWPF.dll differ diff --git a/bin/pyRevitLabs.Language.dll b/bin/pyRevitLabs.Language.dll index 6b307503f..124097474 100644 Binary files a/bin/pyRevitLabs.Language.dll and b/bin/pyRevitLabs.Language.dll differ diff --git a/bin/pyRevitLabs.NLog.dll b/bin/pyRevitLabs.NLog.dll index 4dc3c0c86..8e061298e 100644 Binary files a/bin/pyRevitLabs.NLog.dll and b/bin/pyRevitLabs.NLog.dll differ diff --git a/bin/pyRevitLabs.PyRevit.dll b/bin/pyRevitLabs.PyRevit.dll index 58bda67c1..4a19a1a2c 100644 Binary files a/bin/pyRevitLabs.PyRevit.dll and b/bin/pyRevitLabs.PyRevit.dll differ diff --git a/bin/pyRevitLabs.TargetApps.Revit.dll b/bin/pyRevitLabs.TargetApps.Revit.dll index 9fd5b030a..2840a7569 100644 Binary files a/bin/pyRevitLabs.TargetApps.Revit.dll and b/bin/pyRevitLabs.TargetApps.Revit.dll differ diff --git a/bin/pyrevit-autocomplete.exe b/bin/pyrevit-autocomplete.exe index 5f530c045..63d35ac08 100644 Binary files a/bin/pyrevit-autocomplete.exe and b/bin/pyrevit-autocomplete.exe differ diff --git a/bin/pyrevit-doctor.dll b/bin/pyrevit-doctor.dll index 28693c18c..254e812de 100644 Binary files a/bin/pyrevit-doctor.dll and b/bin/pyrevit-doctor.dll differ diff --git a/bin/pyrevit-doctor.exe b/bin/pyrevit-doctor.exe index 5226ee47c..5e8f4202e 100644 Binary files a/bin/pyrevit-doctor.exe and b/bin/pyrevit-doctor.exe differ diff --git a/bin/pyrevit-hosts.json b/bin/pyrevit-hosts.json index e6a9c1027..101d0288c 100644 --- a/bin/pyrevit-hosts.json +++ b/bin/pyrevit-hosts.json @@ -1883,6 +1883,18 @@ "target": "x64", "version": "23.1.80.30" }, + { + "build": "20260220_1515", + "meta": { + "schema": "1.0", + "source": "https://help.autodesk.com/view/RVT/2023/ENU/?guid=RevitReleaseNotes_2023updates_2023_1_9_html" + }, + "notes": "https://help.autodesk.com/view/RVT/2023/ENU/?guid=RevitReleaseNotes_2023updates_2023_1_9_html", + "product": "Autodesk Revit", + "release": "2023.1.9", + "target": "x64", + "version": "23.1.90.15" + }, { "build": "20230308_1635", "meta": { @@ -2147,6 +2159,18 @@ "target": "x64", "version": "25.4.30.29" }, + { + "build": "20251111_1515", + "meta": { + "schema": "1.0", + "source": "https://help.autodesk.com/view/RVT/2025/ENU/?guid=RevitReleaseNotes_2025updates_2025_4_4_html" + }, + "notes": "https://help.autodesk.com/view/RVT/2025/ENU/?guid=RevitReleaseNotes_2025updates_2025_4_4_html", + "product": "Autodesk Revit", + "release": "2025.4.4", + "target": "x64", + "version": "25.4.41.14" + }, { "build": "20250227_1515", "meta": { diff --git a/bin/pyrevit.dll b/bin/pyrevit.dll index f38a48c66..87ad434c1 100644 Binary files a/bin/pyrevit.dll and b/bin/pyrevit.dll differ diff --git a/bin/pyrevit.exe b/bin/pyrevit.exe index 202eb9621..4a83e51fd 100644 Binary files a/bin/pyrevit.exe and b/bin/pyrevit.exe differ diff --git a/bin/pyrevit.runtimeconfig.json b/bin/pyrevit.runtimeconfig.json index 9e04ed2cc..910ec3c92 100644 --- a/bin/pyrevit.runtimeconfig.json +++ b/bin/pyrevit.runtimeconfig.json @@ -12,7 +12,6 @@ } ], "configProperties": { - "System.Reflection.Metadata.MetadataUpdater.IsSupported": false, "System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false, "CSWINRT_USE_WINDOWS_UI_XAML_PROJECTIONS": false } diff --git a/dev/Directory.Build.props b/dev/Directory.Build.props index ff7231398..da1eee1ac 100644 --- a/dev/Directory.Build.props +++ b/dev/Directory.Build.props @@ -17,7 +17,7 @@ - 6.1.0.26047+2349 + 6.2.0.26090+1754 Copyright © 2014-2025 pyRevitLabs.io diff --git a/dev/libs/netcore/pyRevitLabs.NLog.dll b/dev/libs/netcore/pyRevitLabs.NLog.dll index 4dc3c0c86..8e061298e 100644 Binary files a/dev/libs/netcore/pyRevitLabs.NLog.dll and b/dev/libs/netcore/pyRevitLabs.NLog.dll differ diff --git a/dev/libs/netfx/pyRevitLabs.NLog.dll b/dev/libs/netfx/pyRevitLabs.NLog.dll index ff6f3096b..08eb6d05c 100644 Binary files a/dev/libs/netfx/pyRevitLabs.NLog.dll and b/dev/libs/netfx/pyRevitLabs.NLog.dll differ diff --git a/dev/pyRevitLabs.PyRevit.Runtime/Directory.Build.targets b/dev/pyRevitLabs.PyRevit.Runtime/Directory.Build.targets index 063b4147a..799c10cf5 100644 --- a/dev/pyRevitLabs.PyRevit.Runtime/Directory.Build.targets +++ b/dev/pyRevitLabs.PyRevit.Runtime/Directory.Build.targets @@ -51,10 +51,10 @@ - + - - + + diff --git a/dev/pyRevitLabs.PyRevit.Runtime/EnvVariables.cs b/dev/pyRevitLabs.PyRevit.Runtime/EnvVariables.cs index a03e22f32..d73d7b3d3 100644 --- a/dev/pyRevitLabs.PyRevit.Runtime/EnvVariables.cs +++ b/dev/pyRevitLabs.PyRevit.Runtime/EnvVariables.cs @@ -169,5 +169,28 @@ public EnvDictionary() public void ResetEventHooks() { ((Dictionary>)_envData[EnvDictionaryKeys.Hooks]).Clear(); } + + /// + /// Seeds the AppDomain environment dictionary with session values supplied by the C# loader. + /// Called via reflection by EnvDictionarySeeder in pyRevitAssemblyBuilder (which has no + /// compile-time reference to IronPython), so the PythonDictionary is created here where + /// IronPython is already available. + /// + /// + /// Key/value pairs to store. Keys must match the string values of . + /// Values must be plain CLR primitives (string, bool, int) — IronPython coerces them correctly. + /// + public static void Seed(Dictionary values) { + var envData = AppDomain.CurrentDomain.GetData(DomainStorageKeys.EnvVarsDictKey) as PythonDictionary + ?? new PythonDictionary(); + + foreach (var kv in values) + envData[kv.Key] = kv.Value; + + if (!envData.Contains(EnvDictionaryKeys.Hooks)) + envData[EnvDictionaryKeys.Hooks] = new Dictionary>(); + + AppDomain.CurrentDomain.SetData(DomainStorageKeys.EnvVarsDictKey, envData); + } } } diff --git a/dev/pyRevitLabs.PyRevit.Runtime/EventHandling.cs b/dev/pyRevitLabs.PyRevit.Runtime/EventHandling.cs index a65b81162..2b5dbd134 100644 --- a/dev/pyRevitLabs.PyRevit.Runtime/EventHandling.cs +++ b/dev/pyRevitLabs.PyRevit.Runtime/EventHandling.cs @@ -278,10 +278,11 @@ private static void ActivateUpdaterListener() { if (updaterListener == null) { updaterListener = new UpdaterListener(); UpdaterRegistry.RegisterUpdater(updaterListener); - UpdaterRegistry.AddTrigger( - updaterListener.GetUpdaterId(), - new ElementCategoryFilter(BuiltInCategory.INVALID, inverted: true), - Element.GetChangeTypeAny()); + var updaterId = updaterListener.GetUpdaterId(); + var categoryFilter = new ElementCategoryFilter(BuiltInCategory.INVALID, inverted: true); + UpdaterRegistry.AddTrigger(updaterId, categoryFilter, Element.GetChangeTypeAny()); + UpdaterRegistry.AddTrigger(updaterId, categoryFilter, Element.GetChangeTypeElementAddition()); + UpdaterRegistry.AddTrigger(updaterId, categoryFilter, Element.GetChangeTypeElementDeletion()); } } @@ -1637,4 +1638,79 @@ public static void SetTabFlowDirection() { } } } + /// + /// Hides ribbon tabs by intercepting PropertyChanged on each target + /// RibbonTab data object. When Revit sets IsVisible=true during a + /// view switch, the callback fires synchronously and overrides it + /// back to false — before the WPF layout pass renders. + /// + /// Called from MinifyUI smartbutton via pyrevit.runtime.types. + /// + public static class RibbonTabVisibilityUtils + { + public static bool IsHidingTabs { get; private set; } + + private static HashSet _hiddenTabTitles = new HashSet(); + private static List _hookedTabs + = new List(); + + public static void StartHidingTabs(IEnumerable tabTitles) + { + StopHidingTabs(); + + _hiddenTabTitles = new HashSet(tabTitles); + if (_hiddenTabTitles.Count == 0) + return; + + IsHidingTabs = true; + + foreach (var tab in Autodesk.Windows.ComponentManager.Ribbon.Tabs) + { + if (_hiddenTabTitles.Contains(tab.Title)) + { + var inpc = tab as System.ComponentModel.INotifyPropertyChanged; + if (inpc != null) + { + inpc.PropertyChanged += OnTabPropertyChanged; + _hookedTabs.Add(tab); + } + tab.IsVisible = false; + } + } + } + + public static void StopHidingTabs() + { + foreach (var tab in _hookedTabs) + { + try + { + var inpc = tab as System.ComponentModel.INotifyPropertyChanged; + if (inpc != null) + inpc.PropertyChanged -= OnTabPropertyChanged; + tab.IsVisible = true; + } + catch { } + } + _hookedTabs.Clear(); + _hiddenTabTitles.Clear(); + IsHidingTabs = false; + } + + private static void OnTabPropertyChanged( + object sender, + System.ComponentModel.PropertyChangedEventArgs e) + { + if (!IsHidingTabs) return; + if (e.PropertyName != "IsVisible") return; + + var tab = sender as Autodesk.Windows.RibbonTab; + if (tab == null) return; + if (!_hiddenTabTitles.Contains(tab.Title)) return; + + if (tab.IsVisible) + tab.IsVisible = false; + } + } } + diff --git a/dev/pyRevitLabs/pyRevitCLI/pyRevitCLI.csproj b/dev/pyRevitLabs/pyRevitCLI/pyRevitCLI.csproj index 5c12e6273..fbbaff8a0 100644 --- a/dev/pyRevitLabs/pyRevitCLI/pyRevitCLI.csproj +++ b/dev/pyRevitLabs/pyRevitCLI/pyRevitCLI.csproj @@ -20,7 +20,7 @@ - + diff --git a/dev/pyRevitLabs/pyRevitCLIAutoComplete/go.mod b/dev/pyRevitLabs/pyRevitCLIAutoComplete/go.mod index 4ed7358da..9de64d003 100644 --- a/dev/pyRevitLabs/pyRevitCLIAutoComplete/go.mod +++ b/dev/pyRevitLabs/pyRevitCLIAutoComplete/go.mod @@ -2,7 +2,10 @@ module pyrevit-autocomplete go 1.17 -require github.com/posener/complete v1.2.3 +require ( + github.com/posener/complete v1.2.3 + github.com/posener/complete/v2 v2.1.0 +) require ( github.com/hashicorp/errwrap v1.1.0 // indirect diff --git a/dev/pyRevitLabs/pyRevitCLIAutoComplete/go.sum b/dev/pyRevitLabs/pyRevitCLIAutoComplete/go.sum index 3d8404254..409493294 100644 --- a/dev/pyRevitLabs/pyRevitCLIAutoComplete/go.sum +++ b/dev/pyRevitLabs/pyRevitCLIAutoComplete/go.sum @@ -10,6 +10,7 @@ github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/posener/complete/v2 v2.1.0/go.mod h1:AkzsSVGx4ysH/4OhZf57dr4yszGXgFmXsP/VNwlaW7U= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/dev/pyRevitLabs/pyRevitCLIAutoComplete/pyrevit-autocomplete.go b/dev/pyRevitLabs/pyRevitCLIAutoComplete/pyrevit-autocomplete.go index 1bf216641..d0e33fcd6 100644 --- a/dev/pyRevitLabs/pyRevitCLIAutoComplete/pyrevit-autocomplete.go +++ b/dev/pyRevitLabs/pyRevitCLIAutoComplete/pyrevit-autocomplete.go @@ -34,9 +34,9 @@ func main() { "env": complete.Command{ Sub: complete.Commands{}, Flags: complete.Flags{ - "--log": complete.PredictNothing, "--help": complete.PredictNothing, "--json": complete.PredictNothing, + "--log": complete.PredictNothing, }, }, "update": complete.Command{ @@ -48,13 +48,13 @@ func main() { "clone": complete.Command{ Sub: complete.Commands{}, Flags: complete.Flags{ - "--branch": complete.PredictNothing, - "--password": complete.PredictNothing, - "--token": complete.PredictNothing, - "--image": complete.PredictNothing, "--log": complete.PredictNothing, "--help": complete.PredictNothing, + "--image": complete.PredictNothing, + "--branch": complete.PredictNothing, + "--token": complete.PredictNothing, "--dest": complete.PredictNothing, + "--password": complete.PredictNothing, }, }, "clones": complete.Command{ @@ -119,16 +119,16 @@ func main() { "origin": complete.Command{ Sub: complete.Commands{}, Flags: complete.Flags{ - "--reset": complete.PredictNothing, "--log": complete.PredictNothing, + "--reset": complete.PredictNothing, }, }, "update": complete.Command{ Sub: complete.Commands{}, Flags: complete.Flags{ - "--password": complete.PredictNothing, - "--log": complete.PredictNothing, "--token": complete.PredictNothing, + "--log": complete.PredictNothing, + "--password": complete.PredictNothing, }, }, "deployments": complete.Command{ @@ -150,16 +150,16 @@ func main() { Sub: complete.Commands{}, Flags: complete.Flags{ "--installed": complete.PredictNothing, - "--allusers": complete.PredictNothing, "--attached": complete.PredictNothing, + "--allusers": complete.PredictNothing, }, }, }, Flags: complete.Flags{ + "--help": complete.PredictNothing, "--installed": complete.PredictNothing, - "--allusers": complete.PredictNothing, "--attached": complete.PredictNothing, - "--help": complete.PredictNothing, + "--allusers": complete.PredictNothing, }, }, "attached": complete.Command{ @@ -177,8 +177,8 @@ func main() { "detach": complete.Command{ Sub: complete.Commands{}, Flags: complete.Flags{ - "--log": complete.PredictNothing, "--help": complete.PredictNothing, + "--log": complete.PredictNothing, }, }, "extend": complete.Command{ @@ -186,28 +186,28 @@ func main() { "ui": complete.Command{ Sub: complete.Commands{}, Flags: complete.Flags{ - "--password": complete.PredictNothing, - "--log": complete.PredictNothing, "--token": complete.PredictNothing, "--dest": complete.PredictNothing, + "--log": complete.PredictNothing, + "--password": complete.PredictNothing, }, }, "lib": complete.Command{ Sub: complete.Commands{}, Flags: complete.Flags{ - "--password": complete.PredictNothing, - "--log": complete.PredictNothing, "--token": complete.PredictNothing, "--dest": complete.PredictNothing, + "--log": complete.PredictNothing, + "--password": complete.PredictNothing, }, }, }, Flags: complete.Flags{ - "--password": complete.PredictNothing, - "--token": complete.PredictNothing, "--log": complete.PredictNothing, "--help": complete.PredictNothing, + "--token": complete.PredictNothing, "--dest": complete.PredictNothing, + "--password": complete.PredictNothing, }, }, "extensions": complete.Command{ @@ -237,8 +237,8 @@ func main() { "origin": complete.Command{ Sub: complete.Commands{}, Flags: complete.Flags{ - "--reset": complete.PredictNothing, "--log": complete.PredictNothing, + "--reset": complete.PredictNothing, }, }, "paths": complete.Command{ @@ -246,8 +246,8 @@ func main() { "forget": complete.Command{ Sub: complete.Commands{}, Flags: complete.Flags{ - "--log": complete.PredictNothing, "--all": complete.PredictNothing, + "--log": complete.PredictNothing, }, }, "add": complete.Command{ @@ -258,8 +258,8 @@ func main() { }, }, Flags: complete.Flags{ - "--log": complete.PredictNothing, "--help": complete.PredictNothing, + "--log": complete.PredictNothing, }, }, "enable": complete.Command{ @@ -279,8 +279,8 @@ func main() { "forget": complete.Command{ Sub: complete.Commands{}, Flags: complete.Flags{ - "--log": complete.PredictNothing, "--all": complete.PredictNothing, + "--log": complete.PredictNothing, }, }, "add": complete.Command{ @@ -291,22 +291,22 @@ func main() { }, }, Flags: complete.Flags{ - "--log": complete.PredictNothing, "--help": complete.PredictNothing, + "--log": complete.PredictNothing, }, }, "update": complete.Command{ Sub: complete.Commands{}, Flags: complete.Flags{ - "--password": complete.PredictNothing, - "--log": complete.PredictNothing, "--token": complete.PredictNothing, + "--log": complete.PredictNothing, + "--password": complete.PredictNothing, }, }, }, Flags: complete.Flags{ - "--log": complete.PredictNothing, "--help": complete.PredictNothing, + "--log": complete.PredictNothing, }, }, "releases": complete.Command{ @@ -351,8 +351,8 @@ func main() { }, }, Flags: complete.Flags{ - "--pre": complete.PredictNothing, "--help": complete.PredictNothing, + "--pre": complete.PredictNothing, }, }, "revits": complete.Command{ @@ -373,9 +373,9 @@ func main() { }, }, Flags: complete.Flags{ + "--help": complete.PredictNothing, "--installed": complete.PredictNothing, "--supported": complete.PredictNothing, - "--help": complete.PredictNothing, }, }, "run": complete.Command{ @@ -386,12 +386,12 @@ func main() { }, }, Flags: complete.Flags{ - "--revit": complete.PredictNothing, - "--import": complete.PredictNothing, "--models": complete.PredictNothing, - "--allowdialogs": complete.PredictNothing, - "--purge": complete.PredictNothing, "--help": complete.PredictNothing, + "--import": complete.PredictNothing, + "--revit": complete.PredictNothing, + "--purge": complete.PredictNothing, + "--allowdialogs": complete.PredictNothing, }, }, "caches": complete.Command{ @@ -415,8 +415,8 @@ func main() { "config": complete.Command{ Sub: complete.Commands{}, Flags: complete.Flags{ - "--from": complete.PredictNothing, "--help": complete.PredictNothing, + "--from": complete.PredictNothing, }, }, "configs": complete.Command{ @@ -708,8 +708,8 @@ func main() { }, }, Flags: complete.Flags{ - "--log": complete.PredictNothing, "--help": complete.PredictNothing, + "--log": complete.PredictNothing, }, }, "telemetry": complete.Command{ @@ -778,8 +778,8 @@ func main() { }, }, Flags: complete.Flags{ - "--log": complete.PredictNothing, "--help": complete.PredictNothing, + "--log": complete.PredictNothing, }, }, "apptelemetry": complete.Command{ @@ -839,25 +839,25 @@ func main() { }, }, Flags: complete.Flags{ - "--log": complete.PredictNothing, "--help": complete.PredictNothing, + "--log": complete.PredictNothing, }, }, "doctor": complete.Command{ Sub: complete.Commands{}, Flags: complete.Flags{ - "--dryrun": complete.PredictNothing, - "--list": complete.PredictNothing, "--help": complete.PredictNothing, + "--list": complete.PredictNothing, + "--dryrun": complete.PredictNothing, }, }, }, Flags: complete.Flags{ - "--verbose": complete.PredictNothing, - "--debug": complete.PredictNothing, "--usage": complete.PredictNothing, - "--version": complete.PredictNothing, "--help": complete.PredictNothing, + "--debug": complete.PredictNothing, + "--verbose": complete.PredictNothing, + "--version": complete.PredictNothing, }, } complete.New("pyrevit", pyrevit).Run() diff --git a/dev/pyRevitLabs/pyRevitLabs.Common/CommonUtils.cs b/dev/pyRevitLabs/pyRevitLabs.Common/CommonUtils.cs index 7f6a55c21..8115ada2a 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Common/CommonUtils.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Common/CommonUtils.cs @@ -1,4 +1,4 @@ -using OpenMcdf; +using OpenMcdf; using System; using System.Text; using System.Diagnostics; @@ -211,19 +211,39 @@ public static bool CheckInternetConnection() { } public static byte[] GetStructuredStorageStream(string filePath, string streamName) { - logger.Debug(string.Format("Attempting to read \"{0}\" stream from structured storage file at \"{1}\"", - streamName, filePath)); + logger.Debug("Attempting to read \"{0}\" stream from structured storage file at \"{1}\"", + streamName, filePath); int res = StgIsStorageFile(filePath); if (res == 0) { - CompoundFile cf = new CompoundFile(filePath); - logger.Debug($"Found CF Root: {cf.RootStorage}"); - if (cf.RootStorage.TryGetStream(streamName, out var foundStream)) { - byte[] streamData = foundStream.GetData(); - cf.Close(); - return streamData; + using (var root = RootStorage.OpenRead(filePath)) { + logger.Debug("Opened structured storage root at \"{0}\"", filePath); + if (root.TryOpenStream(streamName, out CfbStream foundStream)) { + using (foundStream) { + long len = foundStream.Length; + if (len > int.MaxValue) + throw new NotSupportedException( + string.Format("Structured storage stream \"{0}\" in \"{1}\" exceeds maximum supported size.", + streamName, filePath)); + foundStream.Seek(0, SeekOrigin.Begin); + byte[] buffer = new byte[(int)len]; + int offset = 0; + int remaining = buffer.Length; + while (remaining > 0) { + int read = foundStream.Read(buffer, offset, remaining); + if (read == 0) + throw new InvalidDataException( + string.Format("Structured storage stream \"{0}\" in \"{1}\" ended before declared length.", + streamName, filePath)); + offset += read; + remaining -= read; + } + return buffer; + } + } + logger.Debug("Stream \"{0}\" not found in structured storage file \"{1}\"", streamName, filePath); + return null; } - return null; } else { throw new NotSupportedException("File is not a structured storage file"); diff --git a/dev/pyRevitLabs/pyRevitLabs.Common/pyRevitLabs.Common.csproj b/dev/pyRevitLabs/pyRevitLabs.Common/pyRevitLabs.Common.csproj index c9bf31cf1..3858b0bb3 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Common/pyRevitLabs.Common.csproj +++ b/dev/pyRevitLabs/pyRevitLabs.Common/pyRevitLabs.Common.csproj @@ -11,17 +11,17 @@ - - - - - + + + + + - + diff --git a/dev/pyRevitLabs/pyRevitLabs.PyRevit/pyRevitLabs.PyRevit.csproj b/dev/pyRevitLabs/pyRevitLabs.PyRevit/pyRevitLabs.PyRevit.csproj index 5370f8149..b8688e097 100644 --- a/dev/pyRevitLabs/pyRevitLabs.PyRevit/pyRevitLabs.PyRevit.csproj +++ b/dev/pyRevitLabs/pyRevitLabs.PyRevit/pyRevitLabs.PyRevit.csproj @@ -16,7 +16,7 @@ - + diff --git a/dev/pyRevitLabs/pyRevitLabs.UnitTests/pyRevitLabs.UnitTests.csproj b/dev/pyRevitLabs/pyRevitLabs.UnitTests/pyRevitLabs.UnitTests.csproj index b6275ef2c..a92d0499d 100644 --- a/dev/pyRevitLabs/pyRevitLabs.UnitTests/pyRevitLabs.UnitTests.csproj +++ b/dev/pyRevitLabs/pyRevitLabs.UnitTests/pyRevitLabs.UnitTests.csproj @@ -11,8 +11,8 @@ - - + + diff --git a/dev/pyRevitLoader/Directory.Build.targets b/dev/pyRevitLoader/Directory.Build.targets index b4b7451f9..8451412e1 100644 --- a/dev/pyRevitLoader/Directory.Build.targets +++ b/dev/pyRevitLoader/Directory.Build.targets @@ -72,7 +72,7 @@ - + diff --git a/dev/pyRevitLoader/Source/PyRevitRunnerCommand.cs b/dev/pyRevitLoader/Source/PyRevitRunnerCommand.cs index 7733023a5..7006925c9 100644 --- a/dev/pyRevitLoader/Source/PyRevitRunnerCommand.cs +++ b/dev/pyRevitLoader/Source/PyRevitRunnerCommand.cs @@ -231,7 +231,9 @@ private static void SeedEnvDictionary(UIApplication uiApp) { var ipyVersion = attachment?.Engine != null ? attachment.Engine.Version.Version.ToString() : "0"; var cpyVersion = PyRevitConfigs.GetCpythonEngineVersion().ToString(); - envData[EnvDictionaryKeys.SessionUUID] = Guid.NewGuid().ToString(); + if (!envData.Contains(EnvDictionaryKeys.SessionUUID) + || string.IsNullOrWhiteSpace(envData[EnvDictionaryKeys.SessionUUID] as string)) + envData[EnvDictionaryKeys.SessionUUID] = Guid.NewGuid().ToString(); envData[EnvDictionaryKeys.RevitVersion] = revitVersion; envData[EnvDictionaryKeys.Version] = pyRevitVersion; envData[EnvDictionaryKeys.Clone] = cloneName; diff --git a/dev/pyRevitLoader/pyRevitAssemblyBuilder/AssemblyMaker/AssemblyBuilderService.cs b/dev/pyRevitLoader/pyRevitAssemblyBuilder/AssemblyMaker/AssemblyBuilderService.cs index d06d1c0ec..ea29c390f 100644 --- a/dev/pyRevitLoader/pyRevitAssemblyBuilder/AssemblyMaker/AssemblyBuilderService.cs +++ b/dev/pyRevitLoader/pyRevitAssemblyBuilder/AssemblyMaker/AssemblyBuilderService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Collections.Generic; using System.Linq; @@ -52,10 +52,11 @@ public AssemblyBuilderService(string revitVersion, AssemblyBuildStrategy buildSt /// /// The parsed extension to build an assembly for. /// Optional collection of library extensions to include as references. + /// Whether rocket mode is enabled globally. /// Information about the built assembly. /// Thrown when extension is null. /// Thrown when assembly building fails. - public ExtensionAssemblyInfo BuildExtensionAssembly(ParsedExtension extension, IEnumerable libraryExtensions = null) + public ExtensionAssemblyInfo BuildExtensionAssembly(ParsedExtension extension, IEnumerable libraryExtensions = null, bool rocketMode = false) { if (extension == null) throw new ArgumentNullException(nameof(extension)); @@ -74,9 +75,13 @@ public ExtensionAssemblyInfo BuildExtensionAssembly(ParsedExtension extension, I // so they're available in the AppDomain for CLREngine to reference LoadExtensionModules(extension); - // Use build strategy as seed to differentiate DLLs built with different strategies - // This ensures DLLs are only regenerated when extension structure changes or build strategy changes - string strategySeed = _buildStrategy.ToString(); + // Include generation-time inputs that affect emitted command types, so cache invalidates + // when runtime behavior changes (e.g. rocket mode toggles or loader binary updates). + string strategySeed = string.Join("|", + _buildStrategy.ToString(), + $"rocket:{rocketMode}", + $"rocket_compat:{extension.RocketModeCompatible}", + $"builder:{GetAssemblyBuildFingerprint()}"); string hash = GetStableHash(extension.GetHash(strategySeed) + _revitVersion).Substring(0, 16); string fileName = $"pyRevit_{_revitVersion}_{hash}_{extension.Name}.dll"; @@ -117,7 +122,7 @@ public ExtensionAssemblyInfo BuildExtensionAssembly(ParsedExtension extension, I try { - BuildWithRoslyn(extension, outputPath, libraryExtensions); + BuildWithRoslyn(extension, outputPath, libraryExtensions, rocketMode); return new ExtensionAssemblyInfo(extension.Name, outputPath, isReloading); } @@ -260,11 +265,12 @@ private void UpdateReferencedAssemblies(HashSet newModulePaths) /// The parsed extension to build. /// The output path for the compiled assembly. /// Optional library extensions to reference. + /// Whether rocket mode is enabled globally. /// Thrown when Roslyn compilation fails. - private void BuildWithRoslyn(ParsedExtension extension, string outputPath, IEnumerable libraryExtensions) + private void BuildWithRoslyn(ParsedExtension extension, string outputPath, IEnumerable libraryExtensions, bool rocketMode) { var generator = new RoslynCommandTypeGenerator(); - string code = generator.GenerateExtensionCode(extension, _revitVersion, libraryExtensions); + string code = generator.GenerateExtensionCode(extension, _revitVersion, libraryExtensions, rocketMode); var csPath = Path.Combine(Path.GetDirectoryName(outputPath), $"{extension.Name}.cs"); File.WriteAllText(csPath, code); _logger.Debug($"Generated C# code file: {csPath}"); @@ -335,6 +341,23 @@ private static string GetStableHash(string input) return BitConverter.ToString(hash).Replace("-", string.Empty).ToLowerInvariant(); } + private static string GetAssemblyBuildFingerprint() + { + try + { + var asmPath = Assembly.GetExecutingAssembly().Location; + var writeTime = File.Exists(asmPath) + ? File.GetLastWriteTimeUtc(asmPath).Ticks.ToString() + : "0"; + var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "0"; + return string.Join("-", version, writeTime); + } + catch + { + return "0"; + } + } + /// /// Loads an assembly into the current AppDomain. /// diff --git a/dev/pyRevitLoader/pyRevitAssemblyBuilder/AssemblyMaker/CommandTypeGenerator.cs b/dev/pyRevitLoader/pyRevitAssemblyBuilder/AssemblyMaker/CommandTypeGenerator.cs index 85a8c4471..2887306ba 100644 --- a/dev/pyRevitLoader/pyRevitAssemblyBuilder/AssemblyMaker/CommandTypeGenerator.cs +++ b/dev/pyRevitLoader/pyRevitAssemblyBuilder/AssemblyMaker/CommandTypeGenerator.cs @@ -49,8 +49,8 @@ private static string GetPyRevitRoot() var dllDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); return Path.GetFullPath(Path.Combine(dllDir, "..", "..", "..", "..")); } - - public string GenerateExtensionCode(ParsedExtension extension, string revitVersion, IEnumerable libraryExtensions = null) + + public string GenerateExtensionCode(ParsedExtension extension, string revitVersion, IEnumerable libraryExtensions = null, bool rocketMode = false) { var sb = new StringBuilder(); sb.AppendLine("#nullable disable"); @@ -58,11 +58,32 @@ public string GenerateExtensionCode(ParsedExtension extension, string revitVersi sb.AppendLine("using PyRevitLabs.PyRevit.Runtime;"); sb.AppendLine(); + // Fix for #3140: Track emitted class names to prevent Roslyn CS0101 + // (duplicate type definition) which kills the entire extension assembly. + // The legacy loader isolates failures per-command via try/except in + // typemaker.make_bundle_types(); this HashSet provides equivalent safety. + var emittedClassNames = new HashSet(StringComparer.Ordinal); + foreach (var cmd in extension.CollectCommandComponents()) { string safeClassName = SanitizeClassName(cmd.UniqueId); + + if (!emittedClassNames.Add(safeClassName)) + { + // Duplicate — skip this command to avoid CS0101. + // Emit a comment in the generated .cs so the user can diagnose + // which script was dropped (the .cs file is saved alongside the DLL). + sb.AppendLine($"// WARNING [#3140]: Skipped duplicate class '{safeClassName}'"); + sb.AppendLine($"// Script: {cmd.ScriptPath ?? "(no script)"}"); + sb.AppendLine($"// UniqueId: {cmd.UniqueId}"); + sb.AppendLine($"// Two bundle directories produced the same UniqueId."); + sb.AppendLine($"// Rename one directory to fix this."); + sb.AppendLine(); + continue; + } + string scriptPath = cmd.ScriptPath; - + // Build search paths matching Python's behavior: // 1. Script's own directory // 2. Component hierarchy lib/ folders (button -> panel -> tab -> extension) @@ -112,7 +133,7 @@ public string GenerateExtensionCode(ParsedExtension extension, string revitVersi string ctrlId = cmd.ControlId ?? $"CustomCtrl_%CustomCtrl_%{extName}%{bundle}%{cmd.Name}"; // Build engine configs based on bundle configuration or script type - string engineCfgs = CommandGenerationUtilities.BuildEngineConfigs(cmd, scriptPath); + string engineCfgs = CommandGenerationUtilities.BuildEngineConfigs(cmd, scriptPath, extension, rocketMode); // Get context from component - only use if explicitly defined string context = cmd.Context ?? string.Empty; @@ -169,6 +190,14 @@ private static string SanitizeClassName(string name) var sb = new StringBuilder(); foreach (char c in name) sb.Append(char.IsLetterOrDigit(c) ? c : '_'); + + // Fix for #3107: C# identifiers cannot start with a digit. + // The legacy loader used Reflection.Emit (IL-level) where leading digits + // were valid. Roslyn compiles C# source, which requires a letter or '_' + // as the first character. Prepend '_' to make it a valid identifier. + if (sb.Length > 0 && char.IsDigit(sb[0])) + sb.Insert(0, '_'); + return sb.ToString(); } @@ -181,7 +210,7 @@ private static string Escape(string str) => .Replace("\"", "\\\""); } - internal static class CommandGenerationUtilities + public static class CommandGenerationUtilities { public static string BuildCommandArguments(ParsedExtension extension, ParsedComponent component, string revitVersion) { @@ -206,17 +235,25 @@ public static string BuildCommandArguments(ParsedExtension extension, ParsedComp /// /// Builds the engine configuration JSON string based on script type and bundle settings /// - public static string BuildEngineConfigs(ParsedComponent cmd, string scriptPath) + /// The command component to build configs for + /// Path to the script file + /// The parent extension (for rocket mode compatibility check) + /// Whether rocket mode is enabled globally + public static string BuildEngineConfigs(ParsedComponent cmd, string scriptPath, ParsedExtension extension = null, bool rocketMode = false) { var configs = new Dictionary(); // Check if this is a Dynamo script bool isDynamoScript = scriptPath != null && scriptPath.EndsWith(".dyn", StringComparison.OrdinalIgnoreCase); - - // Core engine settings (apply to all script types) - configs["clean"] = cmd.Engine?.Clean ?? false; - + + // Determine clean engine setting. + // Default is false (metadata-driven; matches legacy cached-engine behavior) + // In rocket mode with compatible extension, use cached engine (clean = false). + bool useCleanEngine = cmd.Engine?.Clean ?? false; + // No rocket-mode override needed — the logic is now purely metadata-driven + configs["clean"] = useCleanEngine; + // Add engine type only when explicitly specified in metadata. // Do not force the default IronPython value into configs, // otherwise runtime shebang detection (#! python3) is bypassed. @@ -228,9 +265,8 @@ public static string BuildEngineConfigs(ParsedComponent cmd, string scriptPath) if (isDynamoScript) { - // For Dynamo scripts, use appropriate settings - // Use automate or mainthread setting (automate is Dynamo-specific synonym) - bool requiresMainThread = (cmd.Engine?.MainThread ?? false) || (cmd.Engine?.Automate ?? true); + // Use EngineConfig.RequiresMainThread which already has the correct defaults. + bool requiresMainThread = cmd.Engine?.RequiresMainThread ?? false; configs["automate"] = requiresMainThread; // Add Dynamo-specific settings diff --git a/dev/pyRevitLoader/pyRevitAssemblyBuilder/AssemblyMaker/IAssemblyBuilderService.cs b/dev/pyRevitLoader/pyRevitAssemblyBuilder/AssemblyMaker/IAssemblyBuilderService.cs index 2b1be75c7..ec0cc2a5e 100644 --- a/dev/pyRevitLoader/pyRevitAssemblyBuilder/AssemblyMaker/IAssemblyBuilderService.cs +++ b/dev/pyRevitLoader/pyRevitAssemblyBuilder/AssemblyMaker/IAssemblyBuilderService.cs @@ -14,8 +14,9 @@ public interface IAssemblyBuilderService /// /// The parsed extension to build an assembly for. /// Optional collection of library extensions to include as references. + /// Whether rocket mode is enabled globally. /// Information about the built assembly, or null if building fails. - ExtensionAssemblyInfo? BuildExtensionAssembly(ParsedExtension extension, IEnumerable? libraryExtensions = null); + ExtensionAssemblyInfo? BuildExtensionAssembly(ParsedExtension extension, IEnumerable? libraryExtensions = null, bool rocketMode = false); /// /// Loads an assembly into the current AppDomain. diff --git a/dev/pyRevitLoader/pyRevitAssemblyBuilder/SessionManager/EnvDictionarySeeder.cs b/dev/pyRevitLoader/pyRevitAssemblyBuilder/SessionManager/EnvDictionarySeeder.cs new file mode 100644 index 000000000..9522513ff --- /dev/null +++ b/dev/pyRevitLoader/pyRevitAssemblyBuilder/SessionManager/EnvDictionarySeeder.cs @@ -0,0 +1,179 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using Autodesk.Revit.UI; +using pyRevitExtensionParser; + +namespace pyRevitAssemblyBuilder.SessionManager +{ + /// + /// Seeds the AppDomain environment dictionary consumed by the pyRevit Runtime. + /// + /// The Runtime's EnvDictionary class reads session state (UUID, versions, telemetry + /// settings, etc.) from an IronPython.Runtime.PythonDictionary stored in the AppDomain + /// under the key "PYREVITEnvVarsDict". Because this loader project has no compile-time + /// reference to IronPython (UseIronPython=false), we delegate the actual + /// PythonDictionary creation to the Runtime via EnvDictionary.Seed(), which is + /// invoked here through reflection — the same pattern already used for + /// ScriptExecutor.Initialize() and ScriptExecutor.ExecuteScript(). + /// + /// + internal static class EnvDictionarySeeder + { + // Env-dict key string values. These must match EnvDictionaryKeys in the Runtime + // (dev/pyRevitLabs.PyRevit.Runtime/EnvVariables.cs). The prefix is "PYREVIT" because + // PyRevitLabsConsts.ProductName = "PYREVIT". + private const string KeySessionUUID = "PYREVIT_UUID"; + private const string KeyRevitVersion = "PYREVIT_APPVERSION"; + private const string KeyVersion = "PYREVIT_VERSION"; + private const string KeyClone = "PYREVIT_CLONE"; + private const string KeyIPYVersion = "PYREVIT_IPYVERSION"; + private const string KeyCPYVersion = "PYREVIT_CPYVERSION"; + private const string KeyLoggingLevel = "PYREVIT_LOGGINGLEVEL"; + private const string KeyFileLogging = "PYREVIT_FILELOGGING"; + private const string KeyTelemetryState = "PYREVIT_TELEMETRYSTATE"; + private const string KeyTelemetryUTC = "PYREVIT_TELEMETRYUTCTIMESTAMPS"; + private const string KeyTelemetryFileDir = "PYREVIT_TELEMETRYDIR"; + private const string KeyTelemetryFile = "PYREVIT_TELEMETRYFILE"; + private const string KeyTelemetryServer = "PYREVIT_TELEMETRYSERVER"; + private const string KeyTelemetryHooks = "PYREVIT_TELEMETRYINCLUDEHOOKS"; + private const string KeyAppTelemetryState = "PYREVIT_APPTELEMETRYSTATE"; + private const string KeyAppTelemetryServer = "PYREVIT_APPTELEMETRYSERVER"; + private const string KeyAppTelemetryFlags = "PYREVIT_APPTELEMETRYEVENTFLAGS"; + private const string KeyAutoUpdating = "PYREVIT_AUTOUPDATE"; + private const string KeyOutputStyleSheet = "PYREVIT_STYLESHEET"; + + /// + /// Builds the session environment dictionary and stores it in the AppDomain via a reflection + /// call to EnvDictionary.Seed() in the Runtime assembly. + /// + /// The active Revit UIApplication (provides version number). + /// + /// The already-loaded pyRevitLabs.PyRevit.Runtime assembly. + /// + /// + /// Root directory of the pyRevit repository (used to read the version file and locate engine + /// binaries). May be empty — the seeder degrades gracefully to "Unknown" where needed. + /// + public static void Seed(UIApplication uiApp, Assembly runtimeAssembly, string pyRevitRoot) + { + var config = PyRevitConfig.Load(); + + var values = new Dictionary + { + [KeySessionUUID] = Guid.NewGuid().ToString(), + [KeyRevitVersion] = uiApp?.Application?.VersionNumber ?? string.Empty, + [KeyVersion] = ReadPyRevitVersion(pyRevitRoot), + [KeyClone] = "Unknown", + [KeyIPYVersion] = ReadIPYVersion(pyRevitRoot), + [KeyCPYVersion] = "3.12.3", // Known default for the bundled CPython engine + + // Fix for #3203: PyRevitConfig.LoggingLevel returns a pyRevit enum + // (0=Quiet, 1=Verbose, 2=Debug) but the Python logger reads this + // env var as a Python logging module level (10=DEBUG, 20=INFO, 30=WARNING). + // Translate to avoid corrupting the Python logging threshold. + [KeyLoggingLevel] = ToPythonLoggingLevel(config.LoggingLevel), + [KeyFileLogging] = config.FileLogging, + + [KeyTelemetryState] = config.TelemetryState, + [KeyTelemetryUTC] = config.TelemetryUTCTimeStamps, + [KeyTelemetryFileDir] = config.TelemetryFilePath, + [KeyTelemetryFile] = string.Empty, + [KeyTelemetryServer] = config.TelemetryServerUrl, + [KeyTelemetryHooks] = config.TelemetryIncludeHooks, + + [KeyAppTelemetryState] = config.AppTelemetryState, + [KeyAppTelemetryServer] = config.AppTelemetryServerUrl, + [KeyAppTelemetryFlags] = config.AppTelemetryEventFlags, + + [KeyAutoUpdating] = config.AutoUpdate, + [KeyOutputStyleSheet] = config.OutputStyleSheet, + }; + + // Delegate to EnvDictionary.Seed() in the Runtime, which owns PythonDictionary creation. + var envDictType = runtimeAssembly.GetType("PyRevitLabs.PyRevit.Runtime.EnvDictionary") + ?? throw new InvalidOperationException("Cannot find type PyRevitLabs.PyRevit.Runtime.EnvDictionary in runtime assembly."); + + var seedMethod = envDictType.GetMethod( + "Seed", + BindingFlags.Public | BindingFlags.Static, + null, + new[] { typeof(Dictionary) }, + null) + ?? throw new InvalidOperationException("Cannot find EnvDictionary.Seed(Dictionary) method."); + + seedMethod.Invoke(null, new object[] { values }); + } + + /// + /// Converts PyRevitConfig's logging level enum (0=Quiet, 1=Verbose, 2=Debug) + /// to Python's logging module level (30=WARNING, 20=INFO, 10=DEBUG). + /// + /// Python's logger (pyrevitlib/pyrevit/coreutils/logger.py) reads PYREVIT_LOGGINGLEVEL + /// and compares it directly: record.levelno >= _curlevel. The Python logging + /// constants are DEBUG=10, INFO=20, WARNING=30. If we store 0 (pyRevit Quiet) the + /// comparison 10 >= 0 is always true — every message passes, which forces the + /// console window open. + /// + /// + internal static int ToPythonLoggingLevel(int pyrevitLevel) + { + switch (pyrevitLevel) + { + case 2: return 10; // Debug → logging.DEBUG + case 1: return 20; // Verbose → logging.INFO + default: return 30; // Quiet → logging.WARNING (Python default) + } + } + + private static string ReadPyRevitVersion(string pyRevitRoot) + { + if (string.IsNullOrEmpty(pyRevitRoot)) + return "Unknown"; + + // pyRevit version is stored as a bare version string in pyrevitlib/pyrevit/version + var versionFile = Path.Combine(pyRevitRoot, "pyrevitlib", "pyrevit", "version"); + if (File.Exists(versionFile)) + { + try { return File.ReadAllText(versionFile).Trim(); } + catch { /* fall through to Unknown */ } + } + + return "Unknown"; + } + + private static string ReadIPYVersion(string pyRevitRoot) + { + // IronPython engines live under bin/ inside the repo root as well as beside this DLL. + // Check both the bin/ folder of the repo and the directory of the executing assembly. + var candidateDirs = new List(); + + if (!string.IsNullOrEmpty(pyRevitRoot)) + { + candidateDirs.Add(Path.Combine(pyRevitRoot, "bin", "IPY342")); + candidateDirs.Add(Path.Combine(pyRevitRoot, "bin", "IPY2712PR")); + candidateDirs.Add(Path.Combine(pyRevitRoot, "bin")); + } + + var selfDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + if (!string.IsNullOrEmpty(selfDir)) + candidateDirs.Add(selfDir); + + foreach (var dir in candidateDirs) + { + var dll = Path.Combine(dir, "IronPython.dll"); + if (!File.Exists(dll)) continue; + try + { + var ver = AssemblyName.GetAssemblyName(dll).Version; + if (ver != null) return ver.ToString(); + } + catch { /* try next candidate */ } + } + + return "Unknown"; + } + } +} diff --git a/dev/pyRevitLoader/pyRevitAssemblyBuilder/SessionManager/ExtensionManagerService.cs b/dev/pyRevitLoader/pyRevitAssemblyBuilder/SessionManager/ExtensionManagerService.cs index aa028fbec..194463689 100644 --- a/dev/pyRevitLoader/pyRevitAssemblyBuilder/SessionManager/ExtensionManagerService.cs +++ b/dev/pyRevitLoader/pyRevitAssemblyBuilder/SessionManager/ExtensionManagerService.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using pyRevitExtensionParser; -using static pyRevitExtensionParser.ExtensionParser; namespace pyRevitAssemblyBuilder.SessionManager { @@ -11,14 +10,20 @@ namespace pyRevitAssemblyBuilder.SessionManager /// public class ExtensionManagerService : IExtensionManagerService { + private readonly int _revitYear; private List? _cachedExtensions; + public ExtensionManagerService(int revitYear = 0) + { + _revitYear = revitYear; + } + /// /// Gets all parsed extensions (cached). /// private List GetAllExtensionsCached() { - return _cachedExtensions ??= ExtensionParser.ParseInstalledExtensions().ToList(); + return _cachedExtensions ??= ExtensionParser.ParseInstalledExtensions(_revitYear).ToList(); } /// diff --git a/dev/pyRevitLoader/pyRevitAssemblyBuilder/SessionManager/ServiceFactory.cs b/dev/pyRevitLoader/pyRevitAssemblyBuilder/SessionManager/ServiceFactory.cs index bea46bdd1..adb316d45 100644 --- a/dev/pyRevitLoader/pyRevitAssemblyBuilder/SessionManager/ServiceFactory.cs +++ b/dev/pyRevitLoader/pyRevitAssemblyBuilder/SessionManager/ServiceFactory.cs @@ -44,9 +44,9 @@ public static IAssemblyBuilderService CreateAssemblyBuilderService(string revitV /// Creates an ExtensionManagerService instance. /// /// A new IExtensionManagerService instance. - public static IExtensionManagerService CreateExtensionManagerService() + public static IExtensionManagerService CreateExtensionManagerService(int revitYear = 0) { - return new ExtensionManagerService(); + return new ExtensionManagerService(revitYear); } /// @@ -250,7 +250,8 @@ public static ISessionManagerService CreateSessionManagerService( // Create core services var assemblyBuilder = CreateAssemblyBuilderService(revitVersion, buildStrategy, logger); - var extensionManager = CreateExtensionManagerService(); + int.TryParse(revitVersion, out int revitYear); + var extensionManager = CreateExtensionManagerService(revitYear); var hookManager = CreateHookManager(logger); // Create icon and tooltip managers diff --git a/dev/pyRevitLoader/pyRevitAssemblyBuilder/UIManager/Builders/ITabBuilder.cs b/dev/pyRevitLoader/pyRevitAssemblyBuilder/UIManager/Builders/ITabBuilder.cs index bb58b3dd4..611824485 100644 --- a/dev/pyRevitLoader/pyRevitAssemblyBuilder/UIManager/Builders/ITabBuilder.cs +++ b/dev/pyRevitLoader/pyRevitAssemblyBuilder/UIManager/Builders/ITabBuilder.cs @@ -11,10 +11,16 @@ namespace pyRevitAssemblyBuilder.UIManager.Builders public interface ITabBuilder { /// - /// Creates a ribbon tab from the specified component. + /// Creates a ribbon tab from the specified component, or finds and re-enables + /// an existing one (including tabs whose Title was renamed by a script). /// /// The tab component to create. - void CreateTab(ParsedComponent component); + /// + /// The tab's current display Title if it differs from the requested name + /// (i.e. the tab was renamed by a script), or null if no rename was detected. + /// Callers can use this to dual-mark the scanner registry under both names. + /// + string? CreateTab(ParsedComponent component); /// /// Tags a ribbon tab with the pyRevit identifier for runtime icon toggling. diff --git a/dev/pyRevitLoader/pyRevitAssemblyBuilder/UIManager/Builders/StackBuilder.cs b/dev/pyRevitLoader/pyRevitAssemblyBuilder/UIManager/Builders/StackBuilder.cs index daafa79d0..c29b323e7 100644 --- a/dev/pyRevitLoader/pyRevitAssemblyBuilder/UIManager/Builders/StackBuilder.cs +++ b/dev/pyRevitLoader/pyRevitAssemblyBuilder/UIManager/Builders/StackBuilder.cs @@ -364,6 +364,11 @@ private static string SanitizeClassName(string name) var sb = new System.Text.StringBuilder(); foreach (char c in name) sb.Append(char.IsLetterOrDigit(c) ? c : '_'); + + // Fix for #3107: C# class names cannot start with a digit. + if (sb.Length > 0 && char.IsDigit(sb[0])) + sb.Insert(0, '_'); + return sb.ToString(); } diff --git a/dev/pyRevitLoader/pyRevitAssemblyBuilder/UIManager/Builders/TabBuilder.cs b/dev/pyRevitLoader/pyRevitAssemblyBuilder/UIManager/Builders/TabBuilder.cs index 5564633bf..a10232b3e 100644 --- a/dev/pyRevitLoader/pyRevitAssemblyBuilder/UIManager/Builders/TabBuilder.cs +++ b/dev/pyRevitLoader/pyRevitAssemblyBuilder/UIManager/Builders/TabBuilder.cs @@ -28,17 +28,17 @@ public TabBuilder(UIApplication uiApp, ILogger logger) } /// - public void CreateTab(ParsedComponent component) + public string? CreateTab(ParsedComponent component) { if (component == null) { _logger.Warning("Cannot create tab: component is null."); - return; + return null; } // Use localized title which handles fallback to DisplayName var tabText = ExtensionParser.GetComponentTitle(component); - + try { _uiApp.CreateRibbonTab(tabText); @@ -50,24 +50,56 @@ public void CreateTab(ParsedComponent component) _logger.Debug($"Failed to create ribbon tab '{tabText}'. Tab may already exist. Exception: {ex.Message}"); } - // Mark the tab as a pyRevit tab so toggle_icon can find it at runtime - TagTabAsPyRevit(tabText); - - // Ensure existing tab is visible/enabled on reload + // Find the tab, tag it as pyRevit, ensure it's visible, and detect renames. + // Done in a single ribbon scan to avoid the circular dependency where + // TagTabAsPyRevit would need the Tag to find renamed tabs but is itself + // the method that sets the Tag. + string? renamedTitle = null; try { var ribbon = ComponentManager.Ribbon; - var existingTab = ribbon?.Tabs?.FirstOrDefault(t => t.Title == tabText || t.Id == tabText); + if (ribbon?.Tabs == null) return null; + + // Primary search: exact Title or Id match + var existingTab = ribbon.Tabs.FirstOrDefault(t => + t.Title == tabText || t.Id == tabText); + + // Fallback: tab was renamed by a script (e.g. translation) so Title no + // longer matches. Search by pyRevit Tag (set during the previous session) + // combined with exact Id match. The Tag persists on the AdWindows object + // across reloads within the same Revit session. + if (existingTab == null) + { + existingTab = ribbon.Tabs.FirstOrDefault(t => + (t.Tag as string) == UIManagerConstants.PyRevitTabIdentifier + && string.Equals(t.Id, tabText, StringComparison.OrdinalIgnoreCase)); + } + if (existingTab != null) { + existingTab.Tag = UIManagerConstants.PyRevitTabIdentifier; existingTab.IsVisible = true; existingTab.IsEnabled = true; + + // Detect rename: if the current Title differs, return it so the + // caller can dual-mark the scanner registry under both names. + if (existingTab.Title != tabText) + { + renamedTitle = existingTab.Title; + _logger.Debug($"Tab '{tabText}' found with renamed Title '{renamedTitle}'."); + } + else + { + _logger.Debug($"Found and enabled tab '{tabText}'."); + } } } catch (Exception ex) { - _logger.Debug($"Failed to re-enable tab '{tabText}'. Exception: {ex.Message}"); + _logger.Debug($"Failed to find/enable tab '{tabText}'. Exception: {ex.Message}"); } + + return renamedTitle; } /// @@ -85,8 +117,18 @@ public void TagTabAsPyRevit(string tabName) if (ribbon?.Tabs == null) return; - // Find the tab by Title (display name) since that's how tabs are identified - var tab = ribbon.Tabs.FirstOrDefault(t => t.Title == tabName || t.Id == tabName); + // Primary: find by Title or Id + var tab = ribbon.Tabs.FirstOrDefault(t => + t.Title == tabName || t.Id == tabName); + + // Fallback: renamed tab — match by existing Tag + exact Id + if (tab == null) + { + tab = ribbon.Tabs.FirstOrDefault(t => + (t.Tag as string) == UIManagerConstants.PyRevitTabIdentifier + && string.Equals(t.Id, tabName, StringComparison.OrdinalIgnoreCase)); + } + if (tab != null) { tab.Tag = UIManagerConstants.PyRevitTabIdentifier; diff --git a/dev/pyRevitLoader/pyRevitAssemblyBuilder/UIManager/Buttons/ButtonBuilderBase.cs b/dev/pyRevitLoader/pyRevitAssemblyBuilder/UIManager/Buttons/ButtonBuilderBase.cs index c72d4615d..918d3810a 100644 --- a/dev/pyRevitLoader/pyRevitAssemblyBuilder/UIManager/Buttons/ButtonBuilderBase.cs +++ b/dev/pyRevitLoader/pyRevitAssemblyBuilder/UIManager/Buttons/ButtonBuilderBase.cs @@ -78,6 +78,11 @@ protected static string SanitizeClassName(string name) var sb = new System.Text.StringBuilder(); foreach (char c in name) sb.Append(char.IsLetterOrDigit(c) ? c : '_'); + + // Fix for #3107: C# class names cannot start with a digit. + if (sb.Length > 0 && char.IsDigit(sb[0])) + sb.Insert(0, '_'); + return sb.ToString(); } diff --git a/dev/pyRevitLoader/pyRevitAssemblyBuilder/UIManager/IUIManagerService.cs b/dev/pyRevitLoader/pyRevitAssemblyBuilder/UIManager/IUIManagerService.cs index d05030080..bbe95a229 100644 --- a/dev/pyRevitLoader/pyRevitAssemblyBuilder/UIManager/IUIManagerService.cs +++ b/dev/pyRevitLoader/pyRevitAssemblyBuilder/UIManager/IUIManagerService.cs @@ -15,6 +15,12 @@ public interface IUIManagerService /// UIApplication UIApplication { get; } + /// + /// Gets whether rocket mode is enabled. + /// When true, non-critical startup work is skipped and engine caching is used for compatible extensions. + /// + bool RocketMode { get; } + /// /// Builds the UI for the specified extension using the provided assembly information. /// diff --git a/dev/pyRevitLoader/pyRevitAssemblyBuilder/UIManager/IconsHandling/IconManager.cs b/dev/pyRevitLoader/pyRevitAssemblyBuilder/UIManager/IconsHandling/IconManager.cs index b851b42b2..a81ec7d2a 100644 --- a/dev/pyRevitLoader/pyRevitAssemblyBuilder/UIManager/IconsHandling/IconManager.cs +++ b/dev/pyRevitLoader/pyRevitAssemblyBuilder/UIManager/IconsHandling/IconManager.cs @@ -180,6 +180,14 @@ private void SetIconsOnItem(object item, BitmapSource largeImage, BitmapSource s /// public BitmapSource LoadBitmapSource(string imagePath, int targetSize = 0) { + if (string.IsNullOrEmpty(imagePath) || !File.Exists(imagePath)) + return null; + if (imagePath.EndsWith(".svg", StringComparison.OrdinalIgnoreCase)) + { + _logger.Debug($"Skipping SVG icon ..."); + return null; + } + if (string.IsNullOrEmpty(imagePath) || !File.Exists(imagePath)) return null; @@ -236,24 +244,29 @@ private ComponentIcon GetBestIconForSizeWithTheme(ParsedComponent component, int if (!component.HasValidIcons) return null; - // Return the appropriate icon based on theme preference + // Fix for #3173: SVG files are discovered as valid icons but cannot be rendered + // by WPF BitmapImage. Filter to renderable (raster) formats only. + bool IsRenderable(ComponentIcon icon) => + icon?.IsValid == true && !IsSvgIcon(icon); + if (isDarkTheme) { - // In dark theme, prefer dark icon, fall back to light var darkIcon = component.Icons.PrimaryDarkIcon; - if (darkIcon?.IsValid == true) + if (IsRenderable(darkIcon)) return darkIcon; } - // Use light icon (either because we're in light theme, or as fallback) var lightIcon = component.Icons.PrimaryIcon; - if (lightIcon?.IsValid == true) + if (IsRenderable(lightIcon)) return lightIcon; - // Final fallback - use any valid icon - return component.Icons.FirstOrDefault(i => i.IsValid); + // Final fallback - use any valid renderable icon + return component.Icons.FirstOrDefault(i => IsRenderable(i)); } + private static bool IsSvgIcon(ComponentIcon icon) => + icon != null && string.Equals(icon.Extension, ".svg", StringComparison.OrdinalIgnoreCase); + /// public void PreloadExtensionIcons(ParsedExtension extension) { diff --git a/dev/pyRevitLoader/pyRevitAssemblyBuilder/UIManager/SessionManagerService.cs b/dev/pyRevitLoader/pyRevitAssemblyBuilder/UIManager/SessionManagerService.cs index 55fadac26..329abbd0c 100644 --- a/dev/pyRevitLoader/pyRevitAssemblyBuilder/UIManager/SessionManagerService.cs +++ b/dev/pyRevitLoader/pyRevitAssemblyBuilder/UIManager/SessionManagerService.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System; using System.Collections.Generic; using System.Diagnostics; @@ -106,7 +106,14 @@ public void LoadSession() stepStopwatch.Restart(); InitializeScriptExecutor(); _logger.Debug($"[PERF] InitializeScriptExecutor: {stepStopwatch.ElapsedMilliseconds}ms"); - + + // Seed the AppDomain environment dictionary. Must run after InitializeScriptExecutor() + // (which loads _runtimeAssembly) and before any extension startup script (which may call + // pyrevit.sessioninfo, pyrevit.telemetry, etc.). + stepStopwatch.Restart(); + SeedEnvironmentDictionary(); + _logger.Debug($"[PERF] SeedEnvironmentDictionary: {stepStopwatch.ElapsedMilliseconds}ms"); + // Get all library extensions first - they need to be available to all UI extensions stepStopwatch.Restart(); var libraryExtensions = _extensionManager?.GetInstalledLibraryExtensions()?.ToList() ?? new List(); @@ -122,46 +129,67 @@ public void LoadSession() _logger.Warning("No UI extensions found or extension manager is null."); return; } - + + // ── PASS 1: Build and load ALL assemblies ────────────────────────── + // Fix for #3108: Legacy _new_session() uses separate loops to guarantee + // all assemblies exist in the AppDomain before any startup script runs. + // Cross-extension imports in startup scripts fail without this. + var assembledExtensions = new List<(ParsedExtension ext, ExtensionAssemblyInfo assmInfo)>(); + foreach (var ext in uiExtensions) { - if (ext == null) - { - _logger.Warning("Skipping null extension."); - continue; - } - - var extStopwatch = Stopwatch.StartNew(); - + if (ext == null) { _logger.Warning("Skipping null extension."); continue; } try { stepStopwatch.Restart(); - var assmInfo = _assemblyBuilder?.BuildExtensionAssembly(ext, libraryExtensions); + var rocketMode = _uiManager?.RocketMode ?? false; + var assmInfo = _assemblyBuilder?.BuildExtensionAssembly(ext, libraryExtensions, rocketMode); var buildTime = stepStopwatch.ElapsedMilliseconds; - + if (assmInfo == null) { _logger.Error($"Failed to build assembly for extension '{ext.Name}'."); continue; } - + _logger.Info($"Extension assembly created: {ext.Name}"); stepStopwatch.Restart(); _assemblyBuilder?.LoadAssembly(assmInfo); - var loadTime = stepStopwatch.ElapsedMilliseconds; - - _logger.Debug($"[PERF] {ext.Name} - Build: {buildTime}ms, Load: {loadTime}ms"); - - // Execute startup script after building assembly but before creating UI - // This matches the Python loader flow - if (!string.IsNullOrEmpty(ext.StartupScript)) + _logger.Debug($"[PERF] {ext.Name} - Build: {buildTime}ms, Load: {stepStopwatch.ElapsedMilliseconds}ms"); + + assembledExtensions.Add((ext, assmInfo)); + } + catch (Exception ex) + { + _logger.Error($"Error building/loading extension '{ext?.Name ?? "unknown"}': {ex}"); + } + } + + // ── PASS 2: Run ALL startup scripts ──────────────────────────────── + // All assemblies are now loaded, so cross-extension imports work. + foreach (var (ext, _) in assembledExtensions) + { + if (!string.IsNullOrEmpty(ext.StartupScript)) + { + try { _logger.Info($"Running startup tasks for {ext.Name}"); stepStopwatch.Restart(); ExecuteExtensionStartupScript(ext, libraryExtensions); _logger.Debug($"[PERF] {ext.Name} - StartupScript: {stepStopwatch.ElapsedMilliseconds}ms"); } + catch (Exception ex) + { + _logger.Error($"Startup script error for '{ext.Name}': {ex}"); + } + } + } + // ── PASS 3: Build ALL UI ─────────────────────────────────────────── + foreach (var (ext, assmInfo) in assembledExtensions) + { + try + { stepStopwatch.Restart(); _uiManager?.BuildUI(ext, assmInfo); _logger.Debug($"[PERF] {ext.Name} - BuildUI: {stepStopwatch.ElapsedMilliseconds}ms"); @@ -169,10 +197,10 @@ public void LoadSession() } catch (Exception ex) { - _logger.Error($"Error processing extension '{ext?.Name ?? "unknown"}': {ex.Message}"); + _logger.Error($"UI build error for '{ext?.Name ?? "unknown"}': {ex}"); } } - + // STEP 3: Apply external layout directives (panel reordering) // This applies directives that reference external targets (native Revit panels or panels from other extensions) // Must be called after ALL UI is built so all panels exist @@ -200,6 +228,26 @@ public void LoadSession() _logger.Info($"Session loaded in {totalStopwatch.ElapsedMilliseconds}ms"); } + private void SeedEnvironmentDictionary() + { + try + { + if (_runtimeAssembly == null) + { + _logger.Warning("Cannot seed environment dictionary: runtime assembly not loaded."); + return; + } + + EnvDictionarySeeder.Seed(_uiApp, _runtimeAssembly, _pyRevitRoot ?? string.Empty); + _logger.Debug("Session environment dictionary seeded successfully."); + } + catch (Exception ex) + { + _logger.Warning($"Failed to seed environment dictionary: {ex}"); + throw; + } + } + private void InitializeScriptExecutor() { // Cache runtime assembly lookup - it's used by every extension diff --git a/dev/pyRevitLoader/pyRevitAssemblyBuilder/UIManager/UIManagerService.cs b/dev/pyRevitLoader/pyRevitAssemblyBuilder/UIManager/UIManagerService.cs index ed95df0ba..93c8b18b2 100644 --- a/dev/pyRevitLoader/pyRevitAssemblyBuilder/UIManager/UIManagerService.cs +++ b/dev/pyRevitLoader/pyRevitAssemblyBuilder/UIManager/UIManagerService.cs @@ -27,13 +27,28 @@ public class UIManagerService : IUIManagerService private readonly IUIRibbonScanner? _ribbonScanner; private readonly UIApplication _uiApp; private ParsedExtension? _currentExtension; - private readonly bool _loadBeta; + /// + /// Cached Load Beta setting. Re-read at start of each BuildUI so reload picks up settings changes. + /// + private bool _loadBeta; + + /// + /// Cached Rocket Mode setting. Re-read at start of each BuildUI so reload picks up settings changes. + /// When true, non-critical startup work (e.g. icon pre-loading) is skipped to reduce load time. + /// + private bool _rocketMode; /// /// Gets the UIApplication instance used by this service. /// public UIApplication UIApplication => _uiApp; + /// + /// Gets whether rocket mode is enabled. + /// When true, non-critical startup work is skipped and engine caching is used for compatible extensions. + /// + public bool RocketMode => _rocketMode; + /// /// Initializes a new instance of the class. /// @@ -67,17 +82,20 @@ public UIManagerService( _comboBoxBuilder = comboBoxBuilder ?? throw new ArgumentNullException(nameof(comboBoxBuilder)); _ribbonScanner = ribbonScanner; - // Load beta settings from config + // Load beta and rocket mode settings from config try { var config = PyRevitConfig.Load(); _loadBeta = config.LoadBeta; + _rocketMode = config.RocketMode; _logger.Debug($"Beta tools loading: {_loadBeta}"); + _logger.Debug($"Rocket mode: {_rocketMode}"); } catch (Exception ex) { - _logger.Debug($"Failed to load beta config, defaulting to false: {ex.Message}"); + _logger.Debug($"Failed to load config, defaulting to false: {ex.Message}"); _loadBeta = false; + _rocketMode = false; } } @@ -94,6 +112,19 @@ public void BuildUI(ParsedExtension extension, ExtensionAssemblyInfo assemblyInf return; } + // Re-read Load Beta and Rocket Mode so toggling in settings is applied on next reload (#3109). + try + { + var config = PyRevitConfig.Load(); + _loadBeta = config.LoadBeta; + _rocketMode = config.RocketMode; + _logger.Debug($"Re-read config - Beta tools loading: {_loadBeta}, Rocket mode: {_rocketMode}"); + } + catch (Exception ex) + { + _logger.Debug($"Failed to re-read config: {ex.Message}"); + } + if (assemblyInfo == null) { _logger.Warning($"Cannot build UI for extension '{extension.Name}': assemblyInfo is null."); @@ -106,8 +137,11 @@ public void BuildUI(ParsedExtension extension, ExtensionAssemblyInfo assemblyInf return; } - // Pre-load icon files in parallel to warm OS file cache - _buttonPostProcessor.IconManager.PreloadExtensionIcons(extension); + // Pre-load icon files in parallel to warm OS file cache (skipped in Rocket Mode) + if (!_rocketMode) + _buttonPostProcessor.IconManager.PreloadExtensionIcons(extension); + else + _logger.Debug($"Rocket mode: skipping icon pre-load for extension '{extension.Name}'."); _currentExtension = extension; foreach (var component in extension.Children) @@ -212,7 +246,8 @@ private void RecursivelyBuildUI( ParsedComponent? parentComponent, RibbonPanel? parentPanel, string tabName, - ExtensionAssemblyInfo assemblyInfo) + ExtensionAssemblyInfo assemblyInfo, + string? renamedTabTitle = null) { if (component == null) { @@ -246,7 +281,7 @@ private void RecursivelyBuildUI( break; case CommandComponentType.Panel: - HandlePanel(component, tabName, assemblyInfo); + HandlePanel(component, tabName, assemblyInfo, renamedTabTitle); break; default: @@ -266,8 +301,10 @@ private void RecursivelyBuildUI( private void HandleTab(ParsedComponent component, ExtensionAssemblyInfo assemblyInfo) { - // Use TabBuilder to create the tab - _tabBuilder.CreateTab(component); + // CreateTab handles find → tag → re-enable in a single ribbon scan. + // Returns the tab's current Title if it was renamed (e.g. by a translation + // script), or null if no rename detected. + var renamedTabTitle = _tabBuilder.CreateTab(component); // Get tab name for children using localized title var tabText = ExtensionParser.GetComponentTitle(component); @@ -275,12 +312,21 @@ private void HandleTab(ParsedComponent component, ExtensionAssemblyInfo assembly // Mark tab as touched in the registry (matching Python's set_dirty_flag behavior) _ribbonScanner?.MarkElementTouched("tab", tabText); - // Recursively build children + // If CreateTab detected a rename, also mark the current (renamed) Title + // so CleanupOrphanedElements() doesn't deactivate the tab (#3167). + if (!string.IsNullOrEmpty(renamedTabTitle)) + { + _ribbonScanner?.MarkElementTouched("tab", renamedTabTitle); + _logger.Debug($"Tab '{tabText}' has current Title '{renamedTabTitle}' — marked both as touched."); + } + + // Recursively build children, passing the renamed title so panels can dual-mark too foreach (var child in component.Children ?? Enumerable.Empty()) - RecursivelyBuildUI(child, component, null, tabText, assemblyInfo); + RecursivelyBuildUI(child, component, null, tabText, assemblyInfo, renamedTabTitle); } - private void HandlePanel(ParsedComponent component, string tabName, ExtensionAssemblyInfo assemblyInfo) + private void HandlePanel(ParsedComponent component, string tabName, + ExtensionAssemblyInfo assemblyInfo, string? renamedTabTitle = null) { // Use PanelBuilder to create the panel var panel = _panelBuilder.CreatePanel(component, tabName); @@ -291,12 +337,21 @@ private void HandlePanel(ParsedComponent component, string tabName, ExtensionAss // Mark panel as touched in the registry (matching Python's set_dirty_flag behavior) _ribbonScanner?.MarkElementTouched("panel", panelText, tabName); + // If the parent tab was renamed (e.g. by a translation script), the scanner + // registered this panel under "panel:{renamedTab}:{panelText}". Mark that + // key as touched too so cleanup doesn't hide the panel. + if (!string.IsNullOrEmpty(renamedTabTitle)) + { + _ribbonScanner?.MarkElementTouched("panel", panelText, renamedTabTitle); + } + // Apply background colors if specified _panelBuilder.ApplyPanelBackgroundColors(panel, component, tabName); - // Recursively build children + // Recursively build children — propagate renamedTabTitle so any nested + // components that depend on the tab name for registry keys stay consistent. foreach (var child in component.Children ?? Enumerable.Empty()) - RecursivelyBuildUI(child, component, panel, tabName, assemblyInfo); + RecursivelyBuildUI(child, component, panel, tabName, assemblyInfo, renamedTabTitle); } private void EnsureSlideOutApplied(ParsedComponent? parentComponent, RibbonPanel? parentPanel) diff --git a/dev/pyRevitLoader/pyRevitAssemblyBuilder/pyRevitAssemblyBuilder.csproj b/dev/pyRevitLoader/pyRevitAssemblyBuilder/pyRevitAssemblyBuilder.csproj index d329e7421..abb4dc525 100644 --- a/dev/pyRevitLoader/pyRevitAssemblyBuilder/pyRevitAssemblyBuilder.csproj +++ b/dev/pyRevitLoader/pyRevitAssemblyBuilder/pyRevitAssemblyBuilder.csproj @@ -17,6 +17,10 @@ true + + + + diff --git a/dev/pyRevitLoader/pyRevitExtensionParser/ExtensionParser.cs b/dev/pyRevitLoader/pyRevitExtensionParser/ExtensionParser.cs index 087e39d0c..b1a361eec 100644 --- a/dev/pyRevitLoader/pyRevitExtensionParser/ExtensionParser.cs +++ b/dev/pyRevitLoader/pyRevitExtensionParser/ExtensionParser.cs @@ -79,6 +79,59 @@ private static void LogParseException(string parsedFile, Exception ex) LogError(msg); } + /// + /// Returns true if the given revitYear falls within the declared min/max version range. + /// A null or empty constraint is treated as no restriction (open-ended). + /// A non-empty value that cannot be parsed to an integer is treated as a hard fail. + /// + /// The minimum Revit version. + /// The maximum Revit version. + /// The Revit year of the running Revit instance. + /// The component or extension name, used in log messages. + private static bool IsRevitVersionCompatible(string minRevitVersion, string maxRevitVersion, int revitYear, string name) + { + // Early exit when Revit version is unknown — skip version filtering entirely + if (revitYear <= 0) + { + LogWarning("Skipping min / max version test, since Revit version is unknown"); + return true; + } + + bool compatible = true; + + // Parse and validate min_revit_version + if (!string.IsNullOrEmpty(minRevitVersion)) + { + if (!int.TryParse(minRevitVersion, out var min)) + { + LogWarning($"'{name}': min_revit_version value '{minRevitVersion}' is not a valid integer - skipping."); + compatible = false; + } + else if (revitYear < min) + { + LogInfo($"'{name}': skipped - requires Revit {min} or later (running {revitYear})."); + compatible = false; + } + } + + // Parse and validate max_revit_version + if (!string.IsNullOrEmpty(maxRevitVersion)) + { + if (!int.TryParse(maxRevitVersion, out var max)) + { + LogWarning($"'{name}': max_revit_version value '{maxRevitVersion}' is not a valid integer - skipping."); + compatible = false; + } + else if (revitYear > max) + { + LogInfo($"'{name}': skipped - requires Revit {max} or earlier (running {revitYear})."); + compatible = false; + } + } + + return compatible; + } + private static int GetExceptionInt(Exception ex, params string[] keys) { if (ex?.Data == null) @@ -106,21 +159,21 @@ private static string GetExceptionString(Exception ex, params string[] keys) return string.Empty; } - + // Cache file existence checks to avoid repeated file system calls private static Dictionary _fileExistsCache = new Dictionary(); - + // Cache directory file listings to avoid repeated Directory.GetFiles calls private static Dictionary _directoryFilesCache = new Dictionary(); - + // Cache icon parsing results per component directory private static Dictionary _iconCache = new Dictionary(); - + private static bool FileExists(string path) { if (string.IsNullOrEmpty(path)) return false; - + if (!_fileExistsCache.TryGetValue(path, out bool exists)) { exists = File.Exists(path); @@ -128,7 +181,7 @@ private static bool FileExists(string path) } return exists; } - + private static string[] GetFilesInDirectory(string directory, string searchPattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly) { if (string.IsNullOrEmpty(directory) || !Directory.Exists(directory)) @@ -153,17 +206,17 @@ private static string[] GetFilesInDirectory(string directory, string searchPatte // Cache extension roots to avoid repeated directory traversal and config reading private static List _cachedExtensionRoots; - + /// /// Flag to track if locale has been initialized from config /// private static bool _localeInitialized = false; - + /// /// Cached locale value for cache invalidation when locale changes /// private static string _cachedLocale = null; - + /// /// Clears all static caches to force re-parsing of extensions. /// This should be called before reloading pyRevit to ensure newly installed @@ -178,11 +231,11 @@ public static void ClearAllCaches() _cachedConfig = null; _pythonScriptCache.Clear(); _localeInitialized = false; - + // Also clear the BundleParser cache BundleParser.BundleYamlParser.ClearCache(); } - + /// /// Initializes the DefaultLocale from user configuration if not already set. /// Should be called before parsing extensions to ensure locale-aware localization. @@ -192,7 +245,7 @@ private static void InitializeLocaleFromConfig() { var config = GetConfig(); var userLocale = config.UserLocale; - + // Check if locale has changed since last initialization // If locale changed, we need to invalidate all caches to force re-parsing if (_localeInitialized && userLocale != _cachedLocale) @@ -200,7 +253,7 @@ private static void InitializeLocaleFromConfig() logger.Debug("Locale changed from '{0}' to '{1}'. Clearing caches...", _cachedLocale, userLocale); ClearAllCaches(); } - + if (!string.IsNullOrEmpty(userLocale)) { DefaultLocale = userLocale; @@ -208,7 +261,7 @@ private static void InitializeLocaleFromConfig() _cachedLocale = userLocale; _localeInitialized = true; } - + private static List GetCachedExtensionRoots() { if (_cachedExtensionRoots == null) @@ -222,7 +275,7 @@ private static List GetCachedExtensionRoots() return _cachedExtensionRoots; } - public static IEnumerable ParseInstalledExtensions() + public static IEnumerable ParseInstalledExtensions(int revitYear = 0) { var extensionRoots = GetCachedExtensionRoots(); @@ -253,7 +306,9 @@ public static IEnumerable ParseInstalledExtensions() var fullPath = Path.GetFullPath(extDir); if (discoveredExtensions.Add(fullPath)) { - yield return ParseExtension(extDir); + var parsed = ParseExtension(extDir, revitYear); + if (parsed != null) + yield return parsed; } } @@ -273,7 +328,9 @@ public static IEnumerable ParseInstalledExtensions() var fullPath = Path.GetFullPath(libDir); if (discoveredExtensions.Add(fullPath)) { - yield return ParseExtension(libDir); + var parsed = ParseExtension(libDir, revitYear); + if (parsed != null) + yield return parsed; } } } @@ -283,8 +340,9 @@ public static IEnumerable ParseInstalledExtensions() /// Parses a specific extension from the given extension path /// /// The full path to the .extension or .lib directory + /// The running Revit version year (e.g. 2024). Pass 0 to skip version filtering. /// A single ParsedExtension if the path is valid and contains an extension, otherwise empty - public static IEnumerable ParseInstalledExtensions(string extensionPath) + public static IEnumerable ParseInstalledExtensions(string extensionPath, int revitYear = 0) { if (string.IsNullOrWhiteSpace(extensionPath) || !Directory.Exists(extensionPath)) yield break; @@ -294,15 +352,18 @@ public static IEnumerable ParseInstalledExtensions(string exten !extensionPath.EndsWith(".lib", StringComparison.OrdinalIgnoreCase)) yield break; - yield return ParseExtension(extensionPath); + var parsed = ParseExtension(extensionPath, revitYear); + if (parsed != null) + yield return parsed; } /// /// Parses specific extensions from the given extension paths /// /// The full paths to the .extension or .lib directories + /// The running Revit version year (e.g. 2024). Pass 0 to skip version filtering. /// ParsedExtensions for valid paths that contain extensions - public static IEnumerable ParseInstalledExtensions(IEnumerable extensionPaths) + public static IEnumerable ParseInstalledExtensions(IEnumerable extensionPaths, int revitYear = 0) { if (extensionPaths == null) yield break; @@ -317,7 +378,9 @@ public static IEnumerable ParseInstalledExtensions(IEnumerable< !extensionPath.EndsWith(".lib", StringComparison.OrdinalIgnoreCase)) continue; - yield return ParseExtension(extensionPath); + var parsed = ParseExtension(extensionPath, revitYear); + if (parsed != null) + yield return parsed; } } @@ -334,11 +397,12 @@ private static PyRevitConfig GetConfig() /// Parses a single extension from the given extension directory path /// /// The path to the .extension directory - /// A ParsedExtension object - private static ParsedExtension ParseExtension(string extDir) + /// The running Revit version year (e.g. 2024). Pass 0 to skip version filtering. + /// A ParsedExtension object, or null if the extension is incompatible with the given Revit version + private static ParsedExtension ParseExtension(string extDir, int revitYear = 0) { var extName = Path.GetFileNameWithoutExtension(extDir); - + var bundlePath = Path.Combine(extDir, "bundle.yaml"); ParsedBundle parsedBundle = null; if (FileExists(bundlePath)) @@ -353,19 +417,24 @@ private static ParsedExtension ParseExtension(string extDir) } } + // Extension-level version gate: skip the entire extension (and its directory tree) + // if it declares a version range that doesn't include the running Revit year. + if (!IsRevitVersionCompatible(parsedBundle?.MinRevitVersion, parsedBundle?.MaxRevitVersion, revitYear, extName)) + return null; + // Pass extension-level templates to child components // Include author as a template if it exists - var extensionTemplates = parsedBundle?.Templates != null + var extensionTemplates = parsedBundle?.Templates != null ? new Dictionary(parsedBundle.Templates) : new Dictionary(); - + // If extension has an author, add it as a template for children to inherit if (!string.IsNullOrEmpty(parsedBundle?.Author)) { extensionTemplates["author"] = parsedBundle.Author; } - - // Read extension.json for additional templates + // Read extension.json for additional templates and rocket_mode_compatible + bool rocketModeCompatible = false; var extensionJsonPath = Path.Combine(extDir, "extension.json"); if (FileExists(extensionJsonPath)) { @@ -394,6 +463,13 @@ private static ParsedExtension ParseExtension(string extDir) extensionTemplates["author"] = author; } } + + // Read rocket_mode_compatible setting + var rocketModeValue = json["rocket_mode_compatible"]?.ToString(); + if (!string.IsNullOrEmpty(rocketModeValue)) + { + rocketModeCompatible = rocketModeValue.Equals("true", StringComparison.OrdinalIgnoreCase); + } } catch (Exception ex) { @@ -401,7 +477,16 @@ private static ParsedExtension ParseExtension(string extDir) } } - var children = ParseComponents(extDir, extName, null, extensionTemplates.Count > 0 ? extensionTemplates : null); + // pyRevitCore is always rocket mode compatible (hardcoded, matches Python behavior) + if (string.Equals(extName, "pyRevitCore", StringComparison.OrdinalIgnoreCase)) + { + rocketModeCompatible = true; + } + + // FIXED — pass revitYear through: + var children = ParseComponents(extDir, extName, null, + extensionTemplates.Count > 0 ? extensionTemplates : null, + revitYear); // Read extension config from pyRevit config file (cached). // Config is keyed by folder name (e.g. [extension_test.extension]) so it matches install and Python. @@ -422,7 +507,8 @@ private static ParsedExtension ParseExtension(string extDir) MaxRevitVersion = parsedBundle?.MaxRevitVersion, Context = parsedBundle?.GetFormattedContext(), Engine = parsedBundle?.Engine, - Config = extConfig + Config = extConfig, + RocketModeCompatible = rocketModeCompatible }; ReorderByLayout(parsedExtension, parsedExtension, null); @@ -534,7 +620,7 @@ private static void ReorderByLayout(ParsedComponent component, ParsedExtension e } } } - + /// /// Applies layout directives (before, after, beforeall, afterall) to reorder components. /// Directives that reference external components (not found in children) are stored @@ -674,7 +760,7 @@ private static List GetExtensionRoots() Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "pyRevit", "Extensions"); - + if (Directory.Exists(thirdPartyExtensionsPath)) { roots.Add(thirdPartyExtensionsPath); @@ -694,7 +780,8 @@ private static List GetExtensionRoots() try { - var normalizedPath = Path.GetFullPath(extPath); + var expandedPath = Environment.ExpandEnvironmentVariables(extPath); + var normalizedPath = Path.GetFullPath(expandedPath); if (Directory.Exists(normalizedPath)) { roots.Add(normalizedPath); @@ -758,7 +845,7 @@ private static string SubstituteTemplates(string input, Dictionary private static Dictionary SubstituteTemplatesInDict( - Dictionary localizedValues, + Dictionary localizedValues, Dictionary templates) { if (localizedValues == null || templates == null || templates.Count == 0) @@ -776,7 +863,8 @@ private static List ParseComponents( string baseDir, string extensionName, string parentPath = null, - Dictionary inheritedTemplates = null) + Dictionary inheritedTemplates = null, + int revitYear = 0) { var components = new List(); @@ -817,19 +905,23 @@ private static List ParseComponents( { // Look for script files in order of preference: .py, .cs, .vb, .rb, .dyn, .gh, .ghx, .rfa // Use cached file listing instead of EnumerateFiles - var dirFiles = GetFilesInDirectory(dir, "script.*", SearchOption.TopDirectoryOnly); + var dirFiles = GetFilesInDirectory(dir, "*script.*", SearchOption.TopDirectoryOnly); + var validEndings = new[] { "script", "_script", "-script", ".script" }; + dirFiles = dirFiles.Where(f => + validEndings.Any(end => Path.GetFileNameWithoutExtension(f).EndsWith(end, StringComparison.OrdinalIgnoreCase)) + ).ToArray(); // Check for scripts in priority order var scriptExtensions = new[] { ".py", ".cs", ".vb", ".rb", ".dyn", ".gh", ".ghx", ".rfa" }; foreach (var scriptExt in scriptExtensions) { var scriptFile = $"script{scriptExt}"; - scriptPath = dirFiles.FirstOrDefault(f => + scriptPath = dirFiles.FirstOrDefault(f => f.EndsWith(scriptFile, StringComparison.OrdinalIgnoreCase)); if (scriptPath != null) break; } - + // If no script.* file found, look for any file with the target extensions // This handles cases like BIM1_ArrowHeadSwitcher_script.dyn if (scriptPath == null) @@ -838,9 +930,9 @@ private static List ParseComponents( foreach (var scriptExt in scriptExtensions) { // Look for any file ending with _script{ext} or just {ext} - scriptPath = allFiles.FirstOrDefault(f => + scriptPath = allFiles.FirstOrDefault(f => (f.EndsWith($"_script{scriptExt}", StringComparison.OrdinalIgnoreCase) || - (f.EndsWith(scriptExt, StringComparison.OrdinalIgnoreCase) && + (f.EndsWith(scriptExt, StringComparison.OrdinalIgnoreCase) && !f.EndsWith($"_config{scriptExt}", StringComparison.OrdinalIgnoreCase)))); if (scriptPath != null) break; @@ -908,13 +1000,13 @@ private static List ParseComponents( LogParseException(bundleYaml, ex); } } - + // Try to get content from bundle.yaml metadata first if (tempBundle != null && !string.IsNullOrEmpty(tempBundle.Content)) { scriptPath = ResolveContentPath(dir, tempBundle.Content); } - + // If no content in metadata, use naming convention if (scriptPath == null) { @@ -945,7 +1037,7 @@ private static List ParseComponents( } } } - + // Handle alternative content (CTRL+Click) if (tempBundle != null && !string.IsNullOrEmpty(tempBundle.ContentAlt)) { @@ -976,10 +1068,10 @@ private static List ParseComponents( } } } - + // Look for on/off icons for smartbuttons and toggle buttons string onIconPath = null, onIconDarkPath = null, offIconPath = null, offIconDarkPath = null; - if (componentType == CommandComponentType.SmartButton || + if (componentType == CommandComponentType.SmartButton || componentType == CommandComponentType.PushButton) { // Parse on/off icons with theme support @@ -990,7 +1082,7 @@ private static List ParseComponents( var mediaFile = FindMediaFile(dir); var bundleFile = Path.Combine(dir, "bundle.yaml"); - + // Then parse bundle and override with bundle values if they exist ParsedBundle bundleInComponent = null; if (FileExists(bundleFile)) @@ -1033,7 +1125,7 @@ private static List ParseComponents( } // Pass merged templates to child components - var children = ParseComponents(dir, extensionName, fullPath, mergedTemplates); + var children = ParseComponents(dir, extensionName, fullPath, mergedTemplates, revitYear); // First, get values from Python script string title = null, author = null, doc = null; @@ -1043,7 +1135,7 @@ private static List ParseComponents( Dictionary scriptLocalizedTitles = null; Dictionary scriptLocalizedTooltips = null; Dictionary scriptLocalizedHelpUrls = null; - + if (scriptPath != null && scriptPath.EndsWith(".py", StringComparison.OrdinalIgnoreCase)) { var scriptConstants = ReadPythonScriptConstants(scriptPath); @@ -1083,13 +1175,13 @@ private static List ParseComponents( { bundleTooltip = bundleTooltipEnUs; } - + if (!string.IsNullOrEmpty(bundleTitle)) title = bundleTitle; - + if (!string.IsNullOrEmpty(bundleTooltip)) doc = bundleTooltip; - + if (!string.IsNullOrEmpty(bundleInComponent.Author)) author = bundleInComponent.Author; } @@ -1098,7 +1190,7 @@ private static List ParseComponents( var finalLocalizedTitles = scriptLocalizedTitles ?? new Dictionary(); var finalLocalizedTooltips = scriptLocalizedTooltips ?? new Dictionary(); var finalLocalizedHelpUrls = scriptLocalizedHelpUrls ?? new Dictionary(); - + // If bundle has localized values, they override script values if (bundleInComponent?.Titles != null) { @@ -1107,7 +1199,7 @@ private static List ParseComponents( finalLocalizedTitles[kvp.Key] = kvp.Value; } } - + if (bundleInComponent?.Tooltips != null) { foreach (var kvp in bundleInComponent.Tooltips) @@ -1115,7 +1207,7 @@ private static List ParseComponents( finalLocalizedTooltips[kvp.Key] = kvp.Value; } } - + if (bundleInComponent?.HelpUrls != null) { foreach (var kvp in bundleInComponent.HelpUrls) @@ -1130,7 +1222,7 @@ private static List ParseComponents( author = SubstituteTemplates(author, mergedTemplates); var hyperlink = SubstituteTemplates(bundleInComponent?.Hyperlink, mergedTemplates); scriptHelpUrl = SubstituteTemplates(scriptHelpUrl, mergedTemplates); - + // Apply template substitution to localized values finalLocalizedTitles = SubstituteTemplatesInDict(finalLocalizedTitles, mergedTemplates); finalLocalizedTooltips = SubstituteTemplatesInDict(finalLocalizedTooltips, mergedTemplates); @@ -1141,8 +1233,8 @@ private static List ParseComponents( // so we need to check if there's actually a context defined in the bundle string finalContext; var bundleContext = bundleInComponent?.GetFormattedContext(); - if (bundleInComponent != null && - (bundleInComponent.ContextItems?.Count > 0 || + if (bundleInComponent != null && + (bundleInComponent.ContextItems?.Count > 0 || bundleInComponent.ContextRules?.Count > 0 || !string.IsNullOrEmpty(bundleInComponent.Context))) { @@ -1161,8 +1253,8 @@ private static List ParseComponents( } // Determine final highlight: bundle takes precedence over script - string finalHighlight = !string.IsNullOrEmpty(bundleInComponent?.Highlight) - ? bundleInComponent.Highlight + string finalHighlight = !string.IsNullOrEmpty(bundleInComponent?.Highlight) + ? bundleInComponent.Highlight : scriptHighlight; // Determine final help URL: bundle helpurl takes precedence over script helpurl @@ -1184,8 +1276,8 @@ private static List ParseComponents( : scriptMaxRevitVersion; // Determine final beta status: bundle takes precedence over script - bool finalIsBeta = bundleInComponent != null && bundleInComponent.IsBeta - ? bundleInComponent.IsBeta + bool finalIsBeta = bundleInComponent != null && bundleInComponent.IsBeta + ? bundleInComponent.IsBeta : scriptIsBeta; // Determine final engine config: bundle takes precedence, but script can add flags @@ -1194,6 +1286,11 @@ private static List ParseComponents( if (scriptFullFrameEngine) finalEngine.FullFrame = true; if (scriptPersistentEngine) finalEngine.Persistent = true; + // Component-level version gate: skip this component (and its children) if it declares + // a version range that doesn't include the running Revit year. + if (!IsRevitVersionCompatible(finalMinRevitVersion, finalMaxRevitVersion, revitYear, displayName)) + continue; + components.Add(new ParsedComponent { Name = namePart, @@ -1264,7 +1361,7 @@ public static string GetComponentTitle(ParsedComponent component) { if (component == null) return string.Empty; - + // First try localized titles if (component.LocalizedTitles != null && component.LocalizedTitles.Count > 0) { @@ -1272,7 +1369,7 @@ public static string GetComponentTitle(ParsedComponent component) if (!string.IsNullOrEmpty(localizedTitle)) return localizedTitle; } - + // Fall back to pre-resolved Title or DisplayName return !string.IsNullOrEmpty(component.Title) ? component.Title : component.DisplayName; } @@ -1286,7 +1383,7 @@ public static string GetComponentTooltip(ParsedComponent component) { if (component == null) return string.Empty; - + // First try localized tooltips if (component.LocalizedTooltips != null && component.LocalizedTooltips.Count > 0) { @@ -1294,7 +1391,7 @@ public static string GetComponentTooltip(ParsedComponent component) if (!string.IsNullOrEmpty(localizedTooltip)) return localizedTooltip; } - + // Fall back to pre-resolved Tooltip return component.Tooltip ?? string.Empty; } @@ -1335,7 +1432,7 @@ private static string ResolveContentPath(string bundleDir, string contentPath) // Check if it's an absolute path if (Path.IsPathRooted(contentPath)) { - if (FileExists(contentPath) && + if (FileExists(contentPath) && contentPath.EndsWith(".rfa", StringComparison.OrdinalIgnoreCase)) { return contentPath; @@ -1346,7 +1443,7 @@ private static string ResolveContentPath(string bundleDir, string contentPath) // Treat as relative to bundle directory // Normalize the path to handle .. and . properly var resolvedPath = Path.GetFullPath(Path.Combine(bundleDir, contentPath)); - if (FileExists(resolvedPath) && + if (FileExists(resolvedPath) && resolvedPath.EndsWith(".rfa", StringComparison.OrdinalIgnoreCase)) { return resolvedPath; @@ -1355,11 +1452,62 @@ private static string ResolveContentPath(string bundleDir, string contentPath) return null; } + /// + /// Sanitizes a string for use as a unique command identifier / C# class name. + /// Replicates the legacy Python coreutils.cleanup_string() behavior with + /// the SPECIAL_CHARS replacement table and skip=['_'] (the separator). + /// See: pyrevitlib/pyrevit/coreutils/__init__.py lines 295-344 + /// Fix for #3164: Unique ID generation must match legacy Python loader. + /// private static string SanitizeClassName(string name) { - var sb = new StringBuilder(); - foreach (char c in name) - sb.Append(char.IsLetterOrDigit(c) ? c : '_'); + var result = name + .Replace(" ", "") + .Replace("~", "") + .Replace("!", "EXCLAM") + .Replace("@", "AT") + .Replace("#", "SHARP") + .Replace("$", "DOLLAR") + .Replace("%", "PERCENT") + .Replace("^", "") + .Replace("&", "AND") + .Replace("*", "STAR") + .Replace("+", "PLUS") + .Replace(";", "") + .Replace(":", "") + .Replace(",", "") + .Replace("\"", "") + .Replace("{", "") + .Replace("}", "") + .Replace("[", "") + .Replace("]", "") + .Replace("\\(", "") + .Replace("\\)", "") + .Replace("(", "") + .Replace(")", "") + .Replace("-", "MINUS") + .Replace("=", "EQUALS") + .Replace("<", "") + .Replace(">", "") + .Replace("?", "QMARK") + .Replace(".", "DOT") + // '_' is intentionally NOT replaced — it is the separator (skip=['_']) + .Replace("|", "VERT") + .Replace("\\/", "") + .Replace("\\", ""); + + // Final safety pass: strip any character not valid in a C# identifier + var sb = new StringBuilder(result.Length); + foreach (char c in result) + if (char.IsLetterOrDigit(c) || c == '_') + sb.Append(c); + + // Fix for #3107: Ensure UniqueId is a valid C# identifier. + // Leading digits are invalid in C# class names generated by Roslyn. + // The legacy loader used Reflection.Emit which accepted leading digits. + if (sb.Length > 0 && char.IsDigit(sb[0])) + sb.Insert(0, '_'); + return sb.ToString(); } @@ -1387,7 +1535,7 @@ private struct PythonScriptConstants } // Cache Python script constant parsing to avoid re-reading files - private static Dictionary _pythonScriptCache = + private static Dictionary _pythonScriptCache = new Dictionary(); private static PythonScriptConstants ReadPythonScriptConstants(string scriptPath) @@ -1395,7 +1543,7 @@ private static PythonScriptConstants ReadPythonScriptConstants(string scriptPath // Check cache first if (_pythonScriptCache.TryGetValue(scriptPath, out var cached)) return cached; - + var result = new PythonScriptConstants(); try @@ -1403,11 +1551,11 @@ private static PythonScriptConstants ReadPythonScriptConstants(string scriptPath // Read all lines to handle multiline strings properly var allLines = File.ReadAllLines(scriptPath); var lineIndex = 0; - + foreach (var line in allLines) { var trimmedLine = line.TrimStart(); - + if (trimmedLine.StartsWith("__title__")) { // Check if it's a dictionary @@ -1531,7 +1679,7 @@ private static PythonScriptConstants ReadPythonScriptConstants(string scriptPath { result.PersistentEngine = ExtractPythonBoolValue(trimmedLine); } - + lineIndex++; } } @@ -1559,16 +1707,16 @@ private static List ExtractPythonList(string line) var items = new List(); // Remove outer brackets value = value.Substring(1, value.Length - 2); - + // Split by comma, handling quoted strings var currentItem = ""; var inQuote = false; var quoteChar = '\0'; - + for (int i = 0; i < value.Length; i++) { var ch = value[i]; - + if (!inQuote && (ch == '"' || ch == '\'')) { inQuote = true; @@ -1591,12 +1739,12 @@ private static List ExtractPythonList(string line) currentItem += ch; } } - + // Add last item var lastTrimmed = currentItem.Trim().Trim('\'', '"'); if (!string.IsNullOrWhiteSpace(lastTrimmed)) items.Add(lastTrimmed); - + return items.Count > 0 ? items : null; } } @@ -1626,10 +1774,10 @@ private static string ExtractPythonMultilineString(string firstLine, IEnumerable int firstQuotePos = firstLineTrimmed.IndexOf("\"\"\""); if (firstQuotePos == -1) return null; - + int contentStart = firstQuotePos + 3; string partialContent = firstLineTrimmed.Substring(contentStart); - + // Check if the closing quote is on the same line int closingQuotePos = partialContent.IndexOf("\"\"\""); if (closingQuotePos != -1) @@ -1637,17 +1785,17 @@ private static string ExtractPythonMultilineString(string firstLine, IEnumerable // Single-line multiline string return partialContent.Substring(0, closingQuotePos); } - + // Need to read more lines to find the closing triple quote var content = new StringBuilder(); content.Append(partialContent); content.Append("\n"); - + foreach (var line in remainingLines) { content.Append(line); content.Append("\n"); - + // Check if this line contains the closing triple quote if (line.Contains("\"\"\"")) { @@ -1664,7 +1812,7 @@ private static string ExtractPythonMultilineString(string firstLine, IEnumerable break; } } - + // Process escape sequences in the collected content return ProcessPythonEscapeSequences(content.ToString()); } @@ -1691,18 +1839,18 @@ private static string ExtractPythonValue(string line) if (parts.Length == 2) { var value = parts[1].Trim(); - + // Try to extract quoted string first var quotedValue = ExtractPythonStringContent(value); if (quotedValue != null) return ProcessPythonEscapeSequences(quotedValue); - + // If no quotes, return the value as-is (for unquoted numbers, etc.) // Remove any trailing comments var commentIndex = value.IndexOf('#'); if (commentIndex >= 0) value = value.Substring(0, commentIndex).Trim(); - + return string.IsNullOrEmpty(value) ? null : value; } return null; @@ -1735,11 +1883,11 @@ private static string ExtractPythonStringContent(string value) return null; var trimmedValue = value.TrimStart(); - + // Find the first quote (either single or double) int startIndex = -1; char quoteChar = '\0'; - + for (int i = 0; i < trimmedValue.Length; i++) { if (trimmedValue[i] == '"' || trimmedValue[i] == '\'') @@ -1763,13 +1911,13 @@ private static string ExtractPythonStringContent(string value) endIndex += 2; continue; } - + if (trimmedValue[endIndex] == quoteChar) { // Found the closing quote return trimmedValue.Substring(startIndex + 1, endIndex - startIndex - 1); } - + endIndex++; } @@ -1841,17 +1989,17 @@ private static Dictionary ExtractPythonDictionary(string line) var dict = new Dictionary(); // Remove outer braces value = value.Substring(1, value.Length - 2); - + // Split by comma, but handle commas within quoted strings var items = new List(); var currentItem = ""; var inQuote = false; var quoteChar = '\0'; - + for (int i = 0; i < value.Length; i++) { var ch = value[i]; - + if (!inQuote && (ch == '"' || ch == '\'')) { inQuote = true; @@ -1875,10 +2023,10 @@ private static Dictionary ExtractPythonDictionary(string line) currentItem += ch; } } - + if (!string.IsNullOrWhiteSpace(currentItem)) items.Add(currentItem.Trim()); - + // Parse each key-value pair foreach (var item in items) { @@ -1891,7 +2039,7 @@ private static Dictionary ExtractPythonDictionary(string line) dict[key] = val; } } - + return dict.Count > 0 ? dict : null; } } @@ -1908,7 +2056,7 @@ private static ComponentIconCollection ParseIconsForComponent(string componentDi // Check cache first if (_iconCache.TryGetValue(componentDirectory, out var cached)) return cached; - + var icons = new ComponentIconCollection(); if (!Directory.Exists(componentDirectory)) @@ -1949,7 +2097,7 @@ private static ComponentIconCollection ParseIconsForComponent(string componentDi // Cache the result _iconCache[componentDirectory] = icons; - + return icons; } @@ -2051,11 +2199,11 @@ private static (string onIconPath, string onIconDarkPath, string offIconPath, st try { var files = GetFilesInDirectory(componentDirectory, "*", SearchOption.TopDirectoryOnly); - + foreach (var file in files) { var fileName = Path.GetFileName(file).ToLowerInvariant(); - + // Check for on icons if (fileName == "on.png" || fileName == "on.ico") onIconPath = file; @@ -2090,12 +2238,12 @@ private static string FindMediaFile(string componentDirectory) try { var files = GetFilesInDirectory(componentDirectory, "*", SearchOption.TopDirectoryOnly); - + foreach (var file in files) { var fileName = Path.GetFileName(file).ToLowerInvariant(); var fileNameWithoutExt = Path.GetFileNameWithoutExtension(fileName); - + // Match by name 'tooltip' (like Python's finder='name' mode) // Supports: tooltip.mp4, tooltip.swf, tooltip.png if (fileNameWithoutExt == "tooltip") diff --git a/dev/pyRevitLoader/pyRevitExtensionParser/ParsedExtension.cs b/dev/pyRevitLoader/pyRevitExtensionParser/ParsedExtension.cs index 193d78d27..1f5fa195e 100644 --- a/dev/pyRevitLoader/pyRevitExtensionParser/ParsedExtension.cs +++ b/dev/pyRevitLoader/pyRevitExtensionParser/ParsedExtension.cs @@ -17,6 +17,13 @@ public class ParsedExtension : ParsedComponent public new string MaxRevitVersion { get; set; } public ExtensionConfig Config { get; set; } + /// + /// Gets or sets whether this extension is compatible with rocket mode. + /// When rocket mode is enabled and the extension is compatible, the engine + /// will be reused between command executions for better performance. + /// + public bool RocketModeCompatible { get; set; } = false; + /// /// Layout directives that reference external components (from other extensions or native Revit). /// These must be applied after the full UI is built using the Revit ribbon API. diff --git a/dev/pyRevitLoader/pyRevitExtensionParser/PyRevitConfig.cs b/dev/pyRevitLoader/pyRevitExtensionParser/PyRevitConfig.cs index f85cb6b24..8096bc886 100644 --- a/dev/pyRevitLoader/pyRevitExtensionParser/PyRevitConfig.cs +++ b/dev/pyRevitLoader/pyRevitExtensionParser/PyRevitConfig.cs @@ -94,6 +94,26 @@ public bool NewLoader } } + /// + /// Gets or sets whether Rocket Mode is enabled. + /// + /// + /// When true, pyRevit skips non-critical startup work (e.g. icon pre-loading) + /// to reduce session load time. Defaults to false. + /// + public bool RocketMode + { + get + { + var value = _ini.IniReadValue("core", "rocketmode"); + return bool.TryParse(value, out var result) && result; + } + set + { + _ini.IniWriteValue("core", "rocketmode", value ? TrueString : FalseString); + } + } + /// /// Gets or sets whether to load beta/experimental commands. /// @@ -115,6 +135,211 @@ public bool LoadBeta } } + /// + /// Gets or sets the logging verbosity level. + /// + /// + /// 0 = Quiet (default), 1 = Verbose, 2 = Debug. + /// Derived from the [core] verbose and debug keys, matching PyRevitLogLevels in the CLI library. + /// Read-only; change level by setting the [core] verbose or debug INI keys. + /// + public int LoggingLevel + { + get + { + var verbose = _ini.IniReadValue("core", "verbose"); + var debug = _ini.IniReadValue("core", "debug"); + bool isDebug = bool.TryParse(debug, out var d) && d; + bool isVerbose = bool.TryParse(verbose, out var v) && v; + if (isDebug) return 2; + if (isVerbose) return 1; + return 0; + } + } + + /// + /// Gets or sets whether to write log output to a file. + /// + public bool FileLogging + { + get + { + var value = _ini.IniReadValue("core", "filelogging"); + return bool.TryParse(value, out var result) && result; + } + set + { + _ini.IniWriteValue("core", "filelogging", value ? TrueString : FalseString); + } + } + + /// + /// Gets or sets whether pyRevit should auto-update on startup. + /// + public bool AutoUpdate + { + get + { + var value = _ini.IniReadValue("core", "autoupdate"); + return bool.TryParse(value, out var result) && result; + } + set + { + _ini.IniWriteValue("core", "autoupdate", value ? TrueString : FalseString); + } + } + + /// + /// Gets or sets the path to a custom CSS stylesheet for pyRevit output windows. + /// + public string OutputStyleSheet + { + get + { + var value = _ini.IniReadValue("core", "outputstylesheet"); + return string.IsNullOrEmpty(value) ? string.Empty : value.Trim(); + } + set + { + _ini.IniWriteValue("core", "outputstylesheet", value ?? string.Empty); + } + } + + // ── Telemetry ──────────────────────────────────────────────────────────── + + /// + /// Gets or sets whether script-execution telemetry is enabled. + /// + public bool TelemetryState + { + get + { + var value = _ini.IniReadValue("telemetry", "active"); + return bool.TryParse(value, out var result) && result; + } + set + { + _ini.IniWriteValue("telemetry", "active", value ? TrueString : FalseString); + } + } + + /// + /// Gets or sets whether telemetry timestamps are recorded in UTC. + /// + public bool TelemetryUTCTimeStamps + { + get + { + var value = _ini.IniReadValue("telemetry", "utc_timestamps"); + if (bool.TryParse(value, out var result)) + return result; + + // Default to true when the value is missing or unparseable so that + // loader behavior matches CLI/user_config defaults. + return true; + } + set + { + _ini.IniWriteValue("telemetry", "utc_timestamps", value ? TrueString : FalseString); + } + } + + /// + /// Gets or sets the directory path for telemetry log files. + /// + public string TelemetryFilePath + { + get + { + var value = _ini.IniReadValue("telemetry", "telemetry_file_dir"); + return string.IsNullOrEmpty(value) ? string.Empty : value.Trim(); + } + set + { + _ini.IniWriteValue("telemetry", "telemetry_file_dir", value ?? string.Empty); + } + } + + /// + /// Gets or sets the URL of the telemetry server. + /// + public string TelemetryServerUrl + { + get + { + var value = _ini.IniReadValue("telemetry", "telemetry_server_url"); + return string.IsNullOrEmpty(value) ? string.Empty : value.Trim(); + } + set + { + _ini.IniWriteValue("telemetry", "telemetry_server_url", value ?? string.Empty); + } + } + + /// + /// Gets or sets whether hook script executions are included in telemetry. + /// + public bool TelemetryIncludeHooks + { + get + { + var value = _ini.IniReadValue("telemetry", "include_hooks"); + return bool.TryParse(value, out var result) && result; + } + set + { + _ini.IniWriteValue("telemetry", "include_hooks", value ? TrueString : FalseString); + } + } + + /// + /// Gets or sets whether application-event telemetry is enabled. + /// + public bool AppTelemetryState + { + get + { + var value = _ini.IniReadValue("telemetry", "active_app"); + return bool.TryParse(value, out var result) && result; + } + set + { + _ini.IniWriteValue("telemetry", "active_app", value ? TrueString : FalseString); + } + } + + /// + /// Gets or sets the URL of the application-event telemetry server. + /// + public string AppTelemetryServerUrl + { + get + { + var value = _ini.IniReadValue("telemetry", "apptelemetry_server_url"); + return string.IsNullOrEmpty(value) ? string.Empty : value.Trim(); + } + set + { + _ini.IniWriteValue("telemetry", "apptelemetry_server_url", value ?? string.Empty); + } + } + + /// + /// Gets or sets the event-flags bitmask for application telemetry. + /// + public string AppTelemetryEventFlags + { + get + { + var value = _ini.IniReadValue("telemetry", "apptelemetry_event_flags"); + return string.IsNullOrEmpty(value) ? string.Empty : value.Trim(); + } + set + { + _ini.IniWriteValue("telemetry", "apptelemetry_event_flags", value ?? string.Empty); + } + } + /// /// Gets or sets the timeout (in seconds) for displaying startup log messages. /// diff --git a/dev/pyRevitLoader/pyRevitExtensionParser/pyRevitExtensionParser.csproj b/dev/pyRevitLoader/pyRevitExtensionParser/pyRevitExtensionParser.csproj index e583b1576..a7869a7b5 100644 --- a/dev/pyRevitLoader/pyRevitExtensionParser/pyRevitExtensionParser.csproj +++ b/dev/pyRevitLoader/pyRevitExtensionParser/pyRevitExtensionParser.csproj @@ -17,7 +17,7 @@ - + diff --git a/dev/pyRevitLoader/pyRevitExtensionParserTester/ComponentValidationTests.cs b/dev/pyRevitLoader/pyRevitExtensionParserTester/ComponentValidationTests.cs index e1379315e..382972321 100644 --- a/dev/pyRevitLoader/pyRevitExtensionParserTester/ComponentValidationTests.cs +++ b/dev/pyRevitLoader/pyRevitExtensionParserTester/ComponentValidationTests.cs @@ -445,7 +445,12 @@ private bool IsValidUniqueId(string uniqueId) { if (string.IsNullOrEmpty(uniqueId)) return false; - + + // Fix for #3107: UniqueId must also be a valid C# identifier, + // which means it cannot start with a digit. + if (char.IsDigit(uniqueId[0])) + return false; + return uniqueId.All(c => char.IsLetterOrDigit(c) || c == '_'); } } diff --git a/dev/pyRevitLoader/pyRevitExtensionParserTester/DynamoScriptTests.cs b/dev/pyRevitLoader/pyRevitExtensionParserTester/DynamoScriptTests.cs index 897f9aa7d..c04e4d988 100644 --- a/dev/pyRevitLoader/pyRevitExtensionParserTester/DynamoScriptTests.cs +++ b/dev/pyRevitLoader/pyRevitExtensionParserTester/DynamoScriptTests.cs @@ -67,21 +67,15 @@ public void Parser_Should_Find_TestDynamoBIMGUI_With_Custom_Named_Script() var extension = parsedExtensions.First(); - // Look for Test DynamoBIM GUI button (has BIM1_ArrowHeadSwitcher_script.dyn) + // Look for Test DynamoBIM GUI button (has custom-named folie_architecturale_script.dyn) var dynamoGuiButton = FindComponentRecursively(extension, "TestDynamoBIMGUI"); - + Assert.That(dynamoGuiButton, Is.Not.Null, "Should find TestDynamoBIMGUI button"); - Assert.That(dynamoGuiButton.ScriptPath, Does.EndWith("BIM1_ArrowHeadSwitcher_script.dyn"), - "TestDynamoBIMGUI should have BIM1_ArrowHeadSwitcher_script.dyn"); - Assert.That(File.Exists(dynamoGuiButton.ScriptPath), Is.True, - "BIM1_ArrowHeadSwitcher_script.dyn file should exist"); - - // Also verify the config file exists - var configPath = Path.Combine(Path.GetDirectoryName(dynamoGuiButton.ScriptPath)!, - "BIM1_DeleteUnusedViewTemplates_config.dyn"); - Assert.That(File.Exists(configPath), Is.True, - "BIM1_DeleteUnusedViewTemplates_config.dyn should also exist"); - + Assert.That(dynamoGuiButton.ScriptPath, Does.EndWith("folie_architecturale_script.dyn"), + "TestDynamoBIMGUI should have custom-named folie_architecturale_script.dyn"); + Assert.That(File.Exists(dynamoGuiButton.ScriptPath), Is.True, + "folie_architecturale_script.dyn file should exist"); + TestContext.WriteLine($"Found TestDynamoBIMGUI script at: {dynamoGuiButton.ScriptPath}"); } diff --git a/dev/pyRevitLoader/pyRevitExtensionParserTester/EnvDictionarySeederTests.cs b/dev/pyRevitLoader/pyRevitExtensionParserTester/EnvDictionarySeederTests.cs new file mode 100644 index 000000000..4224cf0bf --- /dev/null +++ b/dev/pyRevitLoader/pyRevitExtensionParserTester/EnvDictionarySeederTests.cs @@ -0,0 +1,49 @@ +using NUnit.Framework; +using pyRevitAssemblyBuilder.SessionManager; + +namespace pyRevitExtensionParserTester +{ + /// + /// Tests for EnvDictionarySeeder logging level translation. + /// Verifies that pyRevit's logging level enum (0/1/2) is correctly + /// mapped to Python's logging module scale (30/20/10). + /// See: issue #3203 + /// + [TestFixture] + public class EnvDictionarySeederTests + { + [Test] + public void ToPythonLoggingLevel_Quiet_ReturnsWarning30() + { + // pyRevit Quiet (0) → Python logging.WARNING (30) + Assert.AreEqual(30, EnvDictionarySeeder.ToPythonLoggingLevel(0), + "Quiet (0) must map to logging.WARNING (30) to suppress DEBUG/INFO output"); + } + + [Test] + public void ToPythonLoggingLevel_Verbose_ReturnsInfo20() + { + // pyRevit Verbose (1) → Python logging.INFO (20) + Assert.AreEqual(20, EnvDictionarySeeder.ToPythonLoggingLevel(1), + "Verbose (1) must map to logging.INFO (20)"); + } + + [Test] + public void ToPythonLoggingLevel_Debug_ReturnsDebug10() + { + // pyRevit Debug (2) → Python logging.DEBUG (10) + Assert.AreEqual(10, EnvDictionarySeeder.ToPythonLoggingLevel(2), + "Debug (2) must map to logging.DEBUG (10)"); + } + + [Test] + public void ToPythonLoggingLevel_UnexpectedValue_DefaultsToWarning30() + { + // Any unrecognized value should fall through to WARNING (safe default) + Assert.AreEqual(30, EnvDictionarySeeder.ToPythonLoggingLevel(-1), + "Negative values should default to WARNING (30)"); + Assert.AreEqual(30, EnvDictionarySeeder.ToPythonLoggingLevel(99), + "Out-of-range values should default to WARNING (30)"); + } + } +} diff --git a/dev/pyRevitLoader/pyRevitExtensionParserTester/ParsedBundleTests.cs b/dev/pyRevitLoader/pyRevitExtensionParserTester/ParsedBundleTests.cs index 890c63bb9..f857663be 100644 --- a/dev/pyRevitLoader/pyRevitExtensionParserTester/ParsedBundleTests.cs +++ b/dev/pyRevitLoader/pyRevitExtensionParserTester/ParsedBundleTests.cs @@ -526,6 +526,11 @@ private static string SanitizeClassName(string name) var sb = new System.Text.StringBuilder(); foreach (char c in name) sb.Append(char.IsLetterOrDigit(c) ? c : '_'); + + // Fix for #3107: Match production code — C# class names cannot start with a digit. + if (sb.Length > 0 && char.IsDigit(sb[0])) + sb.Insert(0, '_'); + return sb.ToString(); } diff --git a/dev/pyRevitLoader/pyRevitExtensionParserTester/ScriptMetadataParsingTest.cs b/dev/pyRevitLoader/pyRevitExtensionParserTester/ScriptMetadataParsingTest.cs index 9759fbeba..a6a7c79fc 100644 --- a/dev/pyRevitLoader/pyRevitExtensionParserTester/ScriptMetadataParsingTest.cs +++ b/dev/pyRevitLoader/pyRevitExtensionParserTester/ScriptMetadataParsingTest.cs @@ -1,6 +1,7 @@ using pyRevitExtensionParser; using pyRevitExtensionParserTest; using pyRevitExtensionParserTest.TestHelpers; +using pyRevitAssemblyBuilder.AssemblyMaker; using System.IO; using System.Text; using NUnit.Framework; @@ -211,6 +212,111 @@ public void TestBundleOverridesScriptMetadata() Assert.Pass("Bundle override of script metadata validated successfully."); } + [Test] + public void TestLoggingLevelConfigFromIni() + { + var configPath = Path.Combine(TestTempDir, "pyRevit_config_logging.ini"); + + // Default: Quiet (0) when nothing is set + File.WriteAllText(configPath, ""); + Assert.AreEqual(0, PyRevitConfig.Load(configPath).LoggingLevel, "Default should be 0 (Quiet)"); + + // Verbose only → 1 + File.WriteAllText(configPath, "[core]\nverbose = true\ndebug = false"); + Assert.AreEqual(1, PyRevitConfig.Load(configPath).LoggingLevel, "verbose=true should give 1"); + + // Debug → 2 (takes priority) + File.WriteAllText(configPath, "[core]\nverbose = true\ndebug = true"); + Assert.AreEqual(2, PyRevitConfig.Load(configPath).LoggingLevel, "debug=true should give 2"); + + // Explicit Quiet + File.WriteAllText(configPath, "[core]\nverbose = false\ndebug = false"); + Assert.AreEqual(0, PyRevitConfig.Load(configPath).LoggingLevel, "Both false should give 0"); + + Assert.Pass("LoggingLevel config parsing validated successfully."); + } + + [Test] + public void TestTelemetryConfigFromIni() + { + var configPath = Path.Combine(TestTempDir, "pyRevit_config_telem.ini"); + + // Defaults: all false / empty when section is absent + File.WriteAllText(configPath, ""); + var cfg0 = PyRevitConfig.Load(configPath); + Assert.IsFalse(cfg0.TelemetryState, "Default TelemetryState should be false"); + Assert.IsTrue(cfg0.TelemetryUTCTimeStamps, "Default TelemetryUTCTimeStamps should be true"); + Assert.AreEqual(string.Empty, cfg0.TelemetryFilePath, "Default TelemetryFilePath should be empty"); + Assert.AreEqual(string.Empty, cfg0.TelemetryServerUrl, "Default TelemetryServerUrl should be empty"); + Assert.IsFalse(cfg0.TelemetryIncludeHooks, "Default TelemetryIncludeHooks should be false"); + Assert.IsFalse(cfg0.AppTelemetryState, "Default AppTelemetryState should be false"); + Assert.AreEqual(string.Empty, cfg0.AppTelemetryServerUrl, "Default AppTelemetryServerUrl should be empty"); + Assert.AreEqual(string.Empty, cfg0.AppTelemetryEventFlags, "Default AppTelemetryEventFlags should be empty"); + + // Set values and verify read-back + var iniContent = string.Join("\n", new[] { + "[telemetry]", + "active = true", + "utc_timestamps = true", + "telemetry_file_dir = C:\\logs", + "telemetry_server_url = https://telem.example.com", + "include_hooks = true", + "active_app = true", + "apptelemetry_server_url = https://apptelm.example.com", + "apptelemetry_event_flags = 255", + }); + File.WriteAllText(configPath, iniContent); + var cfg1 = PyRevitConfig.Load(configPath); + Assert.IsTrue(cfg1.TelemetryState); + Assert.IsTrue(cfg1.TelemetryUTCTimeStamps); + Assert.AreEqual("C:\\logs", cfg1.TelemetryFilePath); + Assert.AreEqual("https://telem.example.com", cfg1.TelemetryServerUrl); + Assert.IsTrue(cfg1.TelemetryIncludeHooks); + Assert.IsTrue(cfg1.AppTelemetryState); + Assert.AreEqual("https://apptelm.example.com", cfg1.AppTelemetryServerUrl); + Assert.AreEqual("255", cfg1.AppTelemetryEventFlags); + + // Write-then-read round-trip + var configPath2 = Path.Combine(TestTempDir, "pyRevit_config_telem_rw.ini"); + File.WriteAllText(configPath2, ""); + var cfgRw = PyRevitConfig.Load(configPath2); + cfgRw.TelemetryState = true; + cfgRw.TelemetryServerUrl = "https://rw.example.com"; + Assert.IsTrue(PyRevitConfig.Load(configPath2).TelemetryState); + Assert.AreEqual("https://rw.example.com", PyRevitConfig.Load(configPath2).TelemetryServerUrl); + + Assert.Pass("Telemetry config parsing validated successfully."); + } + + [Test] + public void TestFileLoggingAndAutoUpdateConfigFromIni() + { + var configPath = Path.Combine(TestTempDir, "pyRevit_config_misc.ini"); + + // Defaults + File.WriteAllText(configPath, ""); + var cfg0 = PyRevitConfig.Load(configPath); + Assert.IsFalse(cfg0.FileLogging, "Default FileLogging should be false"); + Assert.IsFalse(cfg0.AutoUpdate, "Default AutoUpdate should be false"); + Assert.AreEqual(string.Empty, cfg0.OutputStyleSheet, "Default OutputStyleSheet should be empty"); + + // Set values + File.WriteAllText(configPath, "[core]\nfilelogging = true\nautoupdate = true\noutputstylesheet = C:\\style.css"); + var cfg1 = PyRevitConfig.Load(configPath); + Assert.IsTrue(cfg1.FileLogging); + Assert.IsTrue(cfg1.AutoUpdate); + Assert.AreEqual("C:\\style.css", cfg1.OutputStyleSheet); + + // Write-then-read round-trip for OutputStyleSheet + var configPath2 = Path.Combine(TestTempDir, "pyRevit_config_misc_rw.ini"); + File.WriteAllText(configPath2, ""); + var cfgRw = PyRevitConfig.Load(configPath2); + cfgRw.OutputStyleSheet = "C:\\custom.css"; + Assert.AreEqual("C:\\custom.css", PyRevitConfig.Load(configPath2).OutputStyleSheet); + + Assert.Pass("FileLogging / AutoUpdate / OutputStyleSheet config parsing validated successfully."); + } + [Test] public void TestLoadBetaConfigFromIni() { @@ -240,6 +346,101 @@ public void TestLoadBetaConfigFromIni() Assert.Pass("LoadBeta config parsing validated successfully."); } + [Test] + public void TestRocketModeEngineConfigs() + { + var testCases = new[] + { + new { Name = "RocketMode_Off_CompatibleExt", RocketMode = false, Compatible = true, ExplicitClean = false, ExpectedClean = true }, + new { Name = "RocketMode_On_CompatibleExt", RocketMode = true, Compatible = true, ExplicitClean = false, ExpectedClean = false }, + new { Name = "RocketMode_On_IncompatibleExt", RocketMode = true, Compatible = false, ExplicitClean = false, ExpectedClean = true }, + new { Name = "RocketMode_On_CompatibleExt_ExplicitClean", RocketMode = true, Compatible = true, ExplicitClean = true, ExpectedClean = true }, + new { Name = "RocketMode_Off_IncompatibleExt", RocketMode = false, Compatible = false, ExplicitClean = false, ExpectedClean = true }, + }; + + foreach (var tc in testCases) + { + var extensionDir = Path.Combine(TestTempDir, $"{tc.Name}.extension"); + var bundleDir = Path.Combine(extensionDir, "TestPanel.panel", "TestButton.pushbutton"); + Directory.CreateDirectory(bundleDir); + + var scriptContent = new StringBuilder(); + scriptContent.AppendLine("__title__ = 'Test Button'"); + if (tc.ExplicitClean) + { + scriptContent.AppendLine("__cleanengine__ = True"); + } + File.WriteAllText(Path.Combine(bundleDir, "script.py"), scriptContent.ToString()); + + if (tc.Compatible) + { + var extensionJson = "{ \"rocket_mode_compatible\": \"True\" }"; + File.WriteAllText(Path.Combine(extensionDir, "extension.json"), extensionJson); + } + + var extensions = ParseInstalledExtensions(extensionDir).ToList(); + Assert.AreEqual(1, extensions.Count, $"{tc.Name}: Expected 1 extension"); + var extension = extensions.First(); + + Assert.AreEqual(tc.Compatible, extension.RocketModeCompatible, + $"{tc.Name}: RocketModeCompatible should be {tc.Compatible}"); + + var button = FindComponentRecursively(extension, "TestButton"); + Assert.IsNotNull(button, $"{tc.Name}: TestButton not found"); + + var engineCfgs = pyRevitAssemblyBuilder.AssemblyMaker.CommandGenerationUtilities.BuildEngineConfigs( + button, button.ScriptPath, extension, tc.RocketMode); + + TestContext.Out.WriteLine($"{tc.Name}: RocketMode={tc.RocketMode}, Compatible={tc.Compatible}, ExplicitClean={tc.ExplicitClean}"); + TestContext.Out.WriteLine($" Engine Configs: {engineCfgs}"); + + Assert.IsTrue(engineCfgs.Contains($"\"clean\":{tc.ExpectedClean.ToString().ToLower()}"), + $"{tc.Name}: Expected clean={tc.ExpectedClean}, got: {engineCfgs}"); + + Directory.Delete(extensionDir, true); + } + + Assert.Pass("Rocket mode engine configs validated successfully."); + } + + [Test] + public void TestRocketModeCompatibilityFromExtensionJson() + { + var extensionDir = Path.Combine(TestTempDir, "RocketModeCompatibilityTest.extension"); + var bundleDir = Path.Combine(extensionDir, "TestPanel.panel", "TestButton.pushbutton"); + Directory.CreateDirectory(bundleDir); + + File.WriteAllText(Path.Combine(bundleDir, "script.py"), "__title__ = 'Test'"); + File.WriteAllText(Path.Combine(extensionDir, "extension.json"), "{ \"rocket_mode_compatible\": \"True\" }"); + + var extensions = ParseInstalledExtensions(extensionDir).ToList(); + Assert.AreEqual(1, extensions.Count); + var extension = extensions.First(); + + Assert.IsTrue(extension.RocketModeCompatible, "Extension should be rocket mode compatible"); + + Directory.Delete(extensionDir, true); + } + + [Test] + public void TestPyRevitCoreAlwaysRocketModeCompatible() + { + var extensionDir = Path.Combine(TestTempDir, "pyRevitCore.extension"); + var bundleDir = Path.Combine(extensionDir, "TestPanel.panel", "TestButton.pushbutton"); + Directory.CreateDirectory(bundleDir); + + File.WriteAllText(Path.Combine(bundleDir, "script.py"), "__title__ = 'Test'"); + // No extension.json - pyRevitCore should still be compatible + + var extensions = ParseInstalledExtensions(extensionDir).ToList(); + Assert.AreEqual(1, extensions.Count); + var extension = extensions.First(); + + Assert.IsTrue(extension.RocketModeCompatible, "pyRevitCore should always be rocket mode compatible"); + + Directory.Delete(extensionDir, true); + } + private ParsedComponent? FindComponentRecursively(ParsedComponent? component, string targetName) { if (component == null) diff --git a/dev/pyRevitLoader/pyRevitExtensionParserTester/VersionFilteringTests.cs b/dev/pyRevitLoader/pyRevitExtensionParserTester/VersionFilteringTests.cs new file mode 100644 index 000000000..0d9f0c8c5 --- /dev/null +++ b/dev/pyRevitLoader/pyRevitExtensionParserTester/VersionFilteringTests.cs @@ -0,0 +1,156 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NUnit.Framework; +using pyRevitExtensionParser; +using pyRevitExtensionParserTest.TestHelpers; +using static pyRevitExtensionParser.ExtensionParser; + +namespace pyRevitExtensionParserTest +{ + /// + /// Tests for min_revit_version / max_revit_version filtering in ExtensionParser. + /// All tests use temporary on-disk extension structures and call ParseInstalledExtensions + /// directly so filtering behaviour is verified at the parser level, independently of + /// ExtensionManagerService or any real installed extensions. + /// + [TestFixture] + public class VersionFilteringTests : TempFileTestBase + { + // ------------------------------------------------------------------------- + // Test 1 — Extension excluded by min_revit_version + // ------------------------------------------------------------------------- + + [Test] + public void Extension_WithMinVersion_IsExcluded_WhenRevitYearIsTooLow() + { + // Arrange — extension requires Revit 2025, running 2024 + var builder = new TestExtensionBuilder(TestTempDir, "MinVersionExtension"); + builder.Create(); + TestExtensionBuilder.WriteBundleYaml(builder.ExtensionPath, "min_revit_version: \"2025\""); + builder.AddTab("Tab").AddPanel("Panel").AddPushButton("Button", "pass"); + + // Act + var results = ParseInstalledExtensions(new[] { builder.ExtensionPath }, revitYear: 2024).ToList(); + + // Assert + Assert.IsEmpty(results, "Extension should be excluded when running Revit version is below min_revit_version"); + } + + // ------------------------------------------------------------------------- + // Test 2 — Extension excluded by max_revit_version + // ------------------------------------------------------------------------- + + [Test] + public void Extension_WithMaxVersion_IsExcluded_WhenRevitYearIsTooHigh() + { + // Arrange — extension supports up to Revit 2022, running 2024 + var builder = new TestExtensionBuilder(TestTempDir, "MaxVersionExtension"); + builder.Create(); + TestExtensionBuilder.WriteBundleYaml(builder.ExtensionPath, "max_revit_version: \"2022\""); + builder.AddTab("Tab").AddPanel("Panel").AddPushButton("Button", "pass"); + + // Act + var results = ParseInstalledExtensions(new[] { builder.ExtensionPath }, revitYear: 2024).ToList(); + + // Assert + Assert.IsEmpty(results, "Extension should be excluded when running Revit version is above max_revit_version"); + } + + // ------------------------------------------------------------------------- + // Test 3 — Incompatible button excluded while compatible sibling survives + // ------------------------------------------------------------------------- + + [Test] + public void Button_WithMinVersion_IsExcluded_WhileCompatibleSiblingSurvives() + { + // Arrange — two buttons in the same panel; one requires Revit 2025, one has no constraint + var builder = new TestExtensionBuilder(TestTempDir, "MixedButtonExtension"); + builder.Create(); + var panel = builder.AddTab("Tab").AddPanel("Panel"); + + // Button that should be filtered out + panel.AddPushButton("FutureButton", "pass", "min_revit_version: \"2025\""); + + // Button that should survive + panel.AddPushButton("CompatibleButton", "pass"); + + // Act + var extension = ParseInstalledExtensions(new[] { builder.ExtensionPath }, revitYear: 2024) + .Single(); + var allButtons = GetAllButtons(extension); + + // Assert + Assert.IsFalse(allButtons.Any(b => b.Name == "FutureButton"), + "Button with min_revit_version: 2025 should be excluded when running Revit 2024"); + Assert.IsTrue(allButtons.Any(b => b.Name == "CompatibleButton"), + "Button with no version constraint should survive"); + } + + // ------------------------------------------------------------------------- + // Test 4 — Invalid version string is handled gracefully (no exception) + // ------------------------------------------------------------------------- + + [Test] + public void Extension_WithInvalidVersionString_IsExcluded_WithoutThrowing() + { + // Arrange — min_revit_version contains a non-integer value + var builder = new TestExtensionBuilder(TestTempDir, "InvalidVersionExtension"); + builder.Create(); + TestExtensionBuilder.WriteBundleYaml(builder.ExtensionPath, "min_revit_version: \"not_a_year\""); + builder.AddTab("Tab").AddPanel("Panel").AddPushButton("Button", "pass"); + + // Act & Assert — should not throw, and the extension should be excluded + List results = null; + Assert.DoesNotThrow(() => + { + results = ParseInstalledExtensions(new[] { builder.ExtensionPath }, revitYear: 2024).ToList(); + }, "Parsing an invalid version string should not throw an exception"); + + Assert.IsEmpty(results, "Extension with an unparseable version string should be excluded"); + } + + // ------------------------------------------------------------------------- + // Test 5 — revitYear = 0 bypasses all filtering + // ------------------------------------------------------------------------- + + [Test] + public void Extension_WithAnyVersionConstraint_IsIncluded_WhenRevitYearIsZero() + { + // Arrange — extension nominally requires a future Revit version + var builder = new TestExtensionBuilder(TestTempDir, "FarFutureExtension"); + builder.Create(); + TestExtensionBuilder.WriteBundleYaml(builder.ExtensionPath, "min_revit_version: \"2099\""); + builder.AddTab("Tab").AddPanel("Panel").AddPushButton("Button", "pass"); + + // Act — revitYear: 0 means Revit version is unknown; filtering should be skipped + var results = ParseInstalledExtensions(new[] { builder.ExtensionPath }, revitYear: 0).ToList(); + + // Assert + Assert.IsNotEmpty(results, "Extension should be included when revitYear is 0 (version unknown, filtering disabled)"); + } + + // ------------------------------------------------------------------------- + // Helper + // ------------------------------------------------------------------------- + + /// + /// Recursively collects all pushbutton-level components from an extension. + /// + private static List GetAllButtons(ParsedComponent root) + { + var result = new List(); + if (root.Children == null) return result; + + foreach (var child in root.Children) + { + if (child.Type == CommandComponentType.PushButton) + result.Add(child); + else + result.AddRange(GetAllButtons(child)); + } + + return result; + } + } +} diff --git a/dev/pyRevitLoader/pyRevitExtensionParserTester/pyRevitExtensionParserTest.csproj b/dev/pyRevitLoader/pyRevitExtensionParserTester/pyRevitExtensionParserTest.csproj index 49b3e1576..23a1e68f8 100644 --- a/dev/pyRevitLoader/pyRevitExtensionParserTester/pyRevitExtensionParserTest.csproj +++ b/dev/pyRevitLoader/pyRevitExtensionParserTester/pyRevitExtensionParserTest.csproj @@ -60,15 +60,17 @@ + + - - - - - - + + + + + + @@ -94,6 +96,7 @@ + diff --git a/dev/pyRevitTelemetryServer/go.mod b/dev/pyRevitTelemetryServer/go.mod index e0471950f..24cbcc506 100644 --- a/dev/pyRevitTelemetryServer/go.mod +++ b/dev/pyRevitTelemetryServer/go.mod @@ -3,36 +3,28 @@ module pyrevittelemetryserver go 1.24.4 require ( - github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d - github.com/denisenkom/go-mssqldb v0.11.0 + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 + github.com/denisenkom/go-mssqldb v0.12.3 github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 - github.com/go-sql-driver/mysql v1.6.0 - github.com/gofrs/uuid v4.3.1+incompatible - github.com/gorilla/mux v1.8.0 - github.com/lib/pq v1.10.3 - github.com/mattn/go-sqlite3 v1.14.8 + github.com/go-sql-driver/mysql v1.9.3 + github.com/gofrs/uuid v4.4.0+incompatible + github.com/gorilla/mux v1.8.1 + github.com/lib/pq v1.12.1 + github.com/mattn/go-sqlite3 v1.14.38 github.com/pkg/errors v0.9.1 github.com/satori/go.uuid v1.2.0 - go.mongodb.org/mongo-driver/v2 v2.5.0 pkg.re/essentialkaos/ek.v10 v12.32.0+incompatible ) require ( - github.com/montanaflynn/stats v0.7.1 // indirect - github.com/xdg-go/pbkdf2 v1.0.0 // indirect - github.com/xdg-go/scram v1.2.0 // indirect - github.com/xdg-go/stringprep v1.0.4 // indirect - github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + filippo.io/edwards25519 v1.1.1 // indirect + github.com/golang-sql/sqlexp v0.1.0 // indirect ) require ( // Indirect dependencies github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect - github.com/golang/snappy v0.0.4 // indirect - github.com/klauspost/compress v1.17.6 // indirect golang.org/x/crypto v0.45.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/text v0.31.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect pkg.re/essentialkaos/check.v1 v1.0.0 // indirect ) diff --git a/dev/pyRevitTelemetryServer/go.sum b/dev/pyRevitTelemetryServer/go.sum index 8adbdf436..8594e8437 100644 --- a/dev/pyRevitTelemetryServer/go.sum +++ b/dev/pyRevitTelemetryServer/go.sum @@ -1,36 +1,74 @@ -github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ= -github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/denisenkom/go-mssqldb v0.11.0 h1:9rHa233rhdOyrz2GcP9NM+gi2psgJZ4GWDpL/7ND8HI= -github.com/denisenkom/go-mssqldb v0.11.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw= +filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0= +github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denisenkom/go-mssqldb v0.12.3 h1:pBSGx9Tq67pBOTLmxNuirNTeB8Vjmf886Kx+8Y+8shw= +github.com/denisenkom/go-mssqldb v0.12.3/go.mod h1:k0mtMFOnU+AihqFxPMiF05rtiDrorD1Vrm1KEz5hxDo= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= -github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/gofrs/uuid v4.3.1+incompatible h1:0/KbAdpx3UXAx1kEOWHJeOkpbgRFGHVgv+CFIY7dBJI= -github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/lib/pq v1.10.3 h1:v9QZf2Sn6AmjXtQeFpdoq/eaNtYP6IN+7lcrygsIAtg= -github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.12.1 h1:x1nbl/338GLqeDJ/FAiILallhAsqubLzEZu/pXtHUow= +github.com/lib/pq v1.12.1/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/mattn/go-sqlite3 v1.14.8 h1:gDp86IdQsN/xWjIEmr9MF6o9mpksUgh0fu+9ByFxzIU= github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.38 h1:tDUzL85kMvOrvpCt8P64SbGgVFtJB11GPi2AdmITgb4= +github.com/mattn/go-sqlite3 v1.14.38/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= +github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= pkg.re/essentialkaos/check.v1 v1.0.0 h1:2V++mhtm9yHqvW7gtXqcU1D+98vTICGnXmaZloLsZVY= pkg.re/essentialkaos/check.v1 v1.0.0/go.mod h1:B7CoMnGFRnruw7X2Z45kWNvoCW+5OhUsLUm1EBM1aJs= pkg.re/essentialkaos/ek.v10 v12.32.0+incompatible h1:MSnAZgf9WxV/kBpmPpD7md3ajOSXrugvbGIqRd9AWTI= diff --git a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Extensions.smartbutton/script.py b/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Extensions.smartbutton/script.py index 6777d6de0..e0bcdb92c 100644 --- a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Extensions.smartbutton/script.py +++ b/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Extensions.smartbutton/script.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """Add or remove pyRevit extensions.""" # pylint: disable=E0401,W0703,W0613,C0103,C0111 @@ -18,6 +19,65 @@ logger = script.get_logger() +def _ensure_path_registered(dest_path): + """Add dest_path to the Custom Extension Directories list if not already there. + + The old 5.x Install dropdown only offered pre-registered directories, so this + was never needed. The new 'Pick' button allows arbitrary folders, so we must + register them or pyRevit won't discover the installed extension after reload. + Fix for #3193. + + Avoid explicitly registering the implicit default third-party extensions + directory in the Custom Extension Directories list. + """ + # Normalize destination for reliable comparison across platforms + norm_dest = os.path.normcase(os.path.normpath(dest_path)) + + # Skip registering the default third-party extensions directory, which is + # meant to be implicit and not stored as a custom extension root. + try: + from pyrevit import THIRDPARTY_EXTENSIONS_DEFAULT_DIR + + default_norm = os.path.normcase( + os.path.normpath(THIRDPARTY_EXTENSIONS_DEFAULT_DIR) + ) + if norm_dest == default_norm: + return + except Exception: + # If the default directory cannot be resolved for any reason, + # fall back to normal registration logic. + pass + + + Note: + This function must *not* use get_thirdparty_ext_root_dirs() as the source + of truth, since that helper may filter out non-existent paths (e.g. network + locations that are temporarily offline). We therefore read the raw configured + list from user_config when available, and only fall back to the helper for + backward compatibility. + """ + norm_dest = os.path.normpath(dest_path) + + # Prefer the raw configured list to avoid silently dropping offline paths. + try: + raw_dirs = list(user_config.thirdparty_ext_root_dirs or []) + except AttributeError: + # Fallback for older configs: use existing helper (may filter non-existent). + raw_dirs = user_config.get_thirdparty_ext_root_dirs(include_default=False) + + normalized_existing = [os.path.normpath(d) for d in raw_dirs] + if norm_dest not in normalized_existing: + raw_dirs.append(norm_dest) + user_config.set_thirdparty_ext_root_dirs(raw_dirs) + user_config.save_changes() + +def _get_default_ext_dir(): + """Return the best default extension installation directory.""" + dirs = user_config.get_thirdparty_ext_root_dirs(include_default=True) + if dirs: + return dirs[0] + from pyrevit import THIRDPARTY_EXTENSIONS_DEFAULT_DIR + return THIRDPARTY_EXTENSIONS_DEFAULT_DIR def _repo_name_from_git_url(git_url): """Derive repo/folder name from a Git URL (e.g. .../owner/repo.git -> repo).""" @@ -340,22 +400,25 @@ def update_ext_info(self, sender, args): self._update_add_custom_section_for_new() def _update_add_custom_section_for_selection(self, ext_pkg_item): - """Populate Add Custom section from selected extension; disable Pick, show Install only if not installed.""" + """Populate Add Custom section from selected extension.""" self.custom_git_url_tb.Text = ext_pkg_item.GitURL or "" if getattr(self, "custom_ext_name_tb", None): self.custom_ext_name_tb.Text = ext_pkg_item.Name or "" + # Git URL and name come from the catalog — make read-only self.custom_git_url_tb.IsReadOnly = True if getattr(self, "custom_ext_name_tb", None): self.custom_ext_name_tb.IsReadOnly = True - self.path_custom_ext_b.IsEnabled = False if ext_pkg_item.ext_pkg.is_installed: + # Already installed — show where it lives, disable path change self.custom_ext_install_path_tb.Text = ext_pkg_item.ext_pkg.is_installed - else: - default_path = user_config.get_thirdparty_ext_root_dirs(include_default=True)[0] - self.custom_ext_install_path_tb.Text = default_path - if ext_pkg_item.ext_pkg.is_installed: + self.path_custom_ext_b.IsEnabled = False self.hide_element(self.install_custom_ext_b) else: + # Not yet installed — let user pick where to install + # Fix for #3193: Keep "Pick installation path" enabled so user can choose + self.path_custom_ext_b.IsEnabled = True + default_path = _get_default_ext_dir() + self.custom_ext_install_path_tb.Text = default_path self.show_element(self.install_custom_ext_b) self.install_custom_ext_b.Content = self.get_locale_string("Buttons.InstallExtension") @@ -436,6 +499,7 @@ def install_custom_extension(self, sender, args): self.selected_pkg.ext_pkg.config.private_repo = True self.selected_pkg.ext_pkg.config.token = token extpkgs.install(self.selected_pkg.ext_pkg, dest_path) + _ensure_path_registered(dest_path) self._refresh_extension_list() self.Close() call_reload() @@ -512,6 +576,7 @@ def install_custom_extension(self, sender, args): user_config.save_changes() # i don't like it - drop this later extpkgs.install(temp_pkg, dest_path) + _ensure_path_registered(dest_path) self._refresh_extension_list() forms.alert( diff --git a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Settings.smartbutton/script.py b/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Settings.smartbutton/script.py index bc4429ca5..8946e4379 100644 --- a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Settings.smartbutton/script.py +++ b/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Settings.smartbutton/script.py @@ -1028,7 +1028,7 @@ def __selfinit__(script_cmp, ui_button_cmp, __rvt__): # windows explorer # otherwise, will show the Settings user interface if __name__ == "__main__": - if __shiftclick__: # pylint: disable=E0602 + if EXEC_PARAMS.config_mode: script.show_file_in_explorer(user_config.config_file) elif user_config.is_readonly: forms.alert("pyRevit settings are set by your admin.", exitscript=True) diff --git a/extensions/pyRevitDevTools.extension/SamplePanel.ResourceDictionary.en_us.xaml b/extensions/pyRevitDevTools.extension/SamplePanel.ResourceDictionary.en_us.xaml new file mode 100644 index 000000000..cda59cf25 --- /dev/null +++ b/extensions/pyRevitDevTools.extension/SamplePanel.ResourceDictionary.en_us.xaml @@ -0,0 +1,13 @@ + + + + Hello from the English ResourceDictionary! + + \ No newline at end of file diff --git a/extensions/pyRevitDevTools.extension/SamplePanel.xaml b/extensions/pyRevitDevTools.extension/SamplePanel.xaml new file mode 100644 index 000000000..80e386b5e --- /dev/null +++ b/extensions/pyRevitDevTools.extension/SamplePanel.xaml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/extensions/pyRevitTools.extension/lib/match/filter_utils.py b/extensions/pyRevitTools.extension/lib/match/filter_utils.py new file mode 100644 index 000000000..cd8dd1992 --- /dev/null +++ b/extensions/pyRevitTools.extension/lib/match/filter_utils.py @@ -0,0 +1,171 @@ +# -*- coding: utf-8 -*- +from pyrevit.revit import query +from pyrevit import DB +from pyrevit.compat import get_elementid_value_func + +get_elementid_value = get_elementid_value_func() + + +def dissect_parameter_filter(doc, filter_element): + """ + Extract information from a simple equals ParameterFilterElement. + + Returns a dict, or None if the filter is not a single equals rule. + + Keys: + parameter_id – DB.ElementId of the parameter + parameter_name – human-readable name string + categories – list of category name strings + storage_type – DB.StorageType enum value + value – raw value (int / float / str / DB.ElementId) + display_value – formatted string for display + rule – the raw Revit filter rule object + """ + + result = { + "parameter_id": None, + "parameter_name": None, + "categories": [], + "storage_type": None, + "value": None, + "display_value": None, + "rule": None, + } + + # ── categories ──────────────────────────────────────────────────── + try: + for cid in filter_element.GetCategories(): + bic = DB.BuiltInCategory(get_elementid_value(cid)) + cat = doc.Settings.Categories.get_Item(bic) + if cat: + result["categories"].append(cat.Name) + except Exception: + pass + + # ── unwrap to ElementParameterFilter ───────────────────────────── + element_filter = filter_element.GetElementFilter() + + if isinstance(element_filter, (DB.LogicalAndFilter, DB.LogicalOrFilter)): + sub_filters = element_filter.GetFilters() + if len(sub_filters) != 1: + return None + element_filter = sub_filters[0] + + if not isinstance(element_filter, DB.ElementParameterFilter): + return None + + rules = element_filter.GetRules() + if len(rules) != 1: + return None + + rule = rules[0] + result["rule"] = rule + + # ── parameter id / name ─────────────────────────────────────────── + param_id = rule.GetRuleParameter() + result["parameter_id"] = param_id + + param_elem = doc.GetElement(param_id) + if param_elem: + result["parameter_name"] = param_elem.Name + else: + try: + bip = DB.BuiltInParameter(get_elementid_value(param_id)) + result["parameter_name"] = DB.LabelUtils.GetLabelFor(bip) + except Exception: + result["parameter_name"] = str(get_elementid_value(param_id)) + + # ── evaluator must be equals ────────────────────────────────────── + evaluator = rule.GetEvaluator() + if not isinstance(evaluator, (DB.FilterNumericEquals, DB.FilterStringEquals)): + return None + + # ── value extraction (sets storage_type as DB.StorageType enum) ─── + if isinstance(rule, DB.FilterStringRule): + val = rule.RuleString + result["storage_type"] = DB.StorageType.String + result["value"] = val + result["display_value"] = val + + elif isinstance(rule, DB.FilterDoubleRule): + val = rule.RuleValue + result["storage_type"] = DB.StorageType.Double + result["value"] = val + try: + spec = None + if param_elem: + spec = param_elem.GetDataType() + else: + try: + bip = DB.BuiltInParameter(get_elementid_value(param_id)) + bics = [query.get_builtincategory(bic_name) for bic_name in result["categories"]] + collector = query.get_elements_by_categories(bics) + elem = next(iter(collector), None) + param = elem.get_Parameter(bip) if elem else None + if param: + spec = param.Definition.GetDataType() + except Exception: + pass + display = DB.UnitFormatUtils.Format( + doc.GetUnits(), spec, val, False + ) + except Exception: + display = str(val) + result["display_value"] = display + + elif isinstance(rule, DB.FilterIntegerRule): + val = rule.RuleValue + result["storage_type"] = DB.StorageType.Integer + result["value"] = val + # special case: workset parameter + if get_elementid_value(param_id) == int( + DB.BuiltInParameter.ELEM_PARTITION_PARAM + ): + try: + ws = doc.GetWorksetTable().GetWorkset(DB.WorksetId(val)) + result["display_value"] = ws.Name if ws else str(val) + except Exception: + result["display_value"] = str(val) + else: + result["display_value"] = str(val) + + elif isinstance(rule, DB.FilterElementIdRule): + val = rule.RuleValue + result["storage_type"] = DB.StorageType.ElementId + result["value"] = val + elem = doc.GetElement(val) + if elem: + try: + result["display_value"] = elem.Name + except Exception: + result["display_value"] = str(get_elementid_value(val)) + else: + result["display_value"] = str(get_elementid_value(val)) + + else: + return None + + return result + + +def get_most_common_filter_parameter(doc, view): + """ + Return the ElementId of the parameter used most often in simple + equals filters on the given view. Returns None if none found. + """ + param_count = {} + + for fid in view.GetFilters(): + filter_elem = doc.GetElement(fid) + if not isinstance(filter_elem, DB.ParameterFilterElement): + continue + info = dissect_parameter_filter(doc, filter_elem) + if not info: + continue + pid = info["parameter_id"] + param_count[pid] = param_count.get(pid, 0) + 1 + + if not param_count: + return None + + return max(param_count, key=param_count.get) diff --git a/extensions/pyRevitTools.extension/lib/match/match_utils.py b/extensions/pyRevitTools.extension/lib/match/match_utils.py new file mode 100644 index 000000000..8e97d21bf --- /dev/null +++ b/extensions/pyRevitTools.extension/lib/match/match_utils.py @@ -0,0 +1,224 @@ +# -*- coding: utf-8 -*- +from pyrevit import script, forms, revit +from pyrevit import DB, UI +from pyrevit.compat import get_elementid_value_func + +get_elementid_value = get_elementid_value_func() + +logger = script.get_logger() + + +def safe_get_parameter(elem, param_id): + if not param_id: + return None + + try: + pid_val = get_elementid_value(param_id) + + # BuiltInParameter (negative ids) + if pid_val < 0: + bip = DB.BuiltInParameter(pid_val) + return elem.get_Parameter(bip) + + # Shared / Project Parameter + doc = elem.Document + param_el = doc.GetElement(param_id) + + if not param_el: + return None + + # Prefer GUID + if hasattr(param_el, "GuidValue"): + guid = param_el.GuidValue + if guid: + return elem.get_Parameter(guid) + + # Fallback - this should not be entered, as this would mean a non-filterable parameter + definition = ( + param_el.GetDefinition() if hasattr(param_el, "GetDefinition") else None + ) + if definition: + return param_el.get_Parameter(definition) + + except Exception: + return None + + +class PickByCategorySelectionFilter(UI.Selection.ISelectionFilter): + def __init__(self, category_ids): + self.category_ids = category_ids + + # standard API override function + def AllowElement(self, element): + if element.Category and element.Category.Id in self.category_ids: + return True + else: + return False + + # standard API override function + def AllowReference(self, refer, point): # pylint: disable=W0613 + return False + + +class PropKeyValue(object): + """Storage class for matched property info and value.""" + + def __init__( + self, name, datatype, value, istype, display_value=None, categories=None + ): + self.name = name + self.datatype = datatype + self.value = value + self.istype = istype + self.display_value = display_value or name + self.categories = categories if categories is not None else [] + + def __repr__(self): + return str(self.__dict__) + + +def match_prop(dest_inst, dest_type, src_props): + """Match given properties on target instance or type""" + for pkv in src_props: + logger.debug("Applying %s", pkv.name) + + # determine target + target = dest_type if pkv.istype else dest_inst + # ensure target is valid if it is type + if pkv.istype and not target: + logger.warning("Element type is not accessible.") + continue + logger.debug("Target is %s", target) + + # find parameter + dparam = target.LookupParameter(pkv.name) + if dparam and pkv.datatype == dparam.StorageType: + try: + if dparam.StorageType == DB.StorageType.Integer: + dparam.Set(pkv.value or 0) + elif dparam.StorageType == DB.StorageType.Double: + dparam.Set(pkv.value or 0.0) + elif dparam.StorageType == DB.StorageType.ElementId: + if not isinstance(pkv.value, DB.ElementId): + dparam.Set(DB.ElementId(pkv.value)) + else: + dparam.Set(pkv.value) + else: + dparam.Set(pkv.value or "") + except Exception as setex: + logger.warning("Error applying value to: %s | %s", pkv.name, setex) + continue + else: + logger.debug('Parameter "%s"not found on target.', pkv.name) + + +def get_source_properties(src_element, simple=False): + """Return info on selected properties.""" + props = [] + + src_type = revit.query.get_type(src_element) + + selected_params = ( + forms.select_parameters( + src_element, + title="Select Parameters", + multiple=True, + include_instance=True, + include_type=True, + ) + or [] + ) + + logger.debug("Selected parameters: %s", [x.name for x in selected_params]) + + for sparam in selected_params: + logger.debug("Reading %s", sparam.name) + target = src_type if sparam.istype else src_element + tparam = target.LookupParameter(sparam.name) + if tparam: + if tparam.StorageType == DB.StorageType.Integer: + value = tparam.AsInteger() + elif tparam.StorageType == DB.StorageType.Double: + value = tparam.AsDouble() + elif tparam.StorageType == DB.StorageType.ElementId: + value = get_elementid_value(tparam.AsElementId()) + else: + value = tparam.AsString() + + props.append( + PropKeyValue( + name=sparam.name, + datatype=tparam.StorageType, + value=value, + istype=sparam.istype, + display_value=tparam.AsValueString() if not simple else None, + categories=[src_element.Category] if not simple else [], + ) + ) + + return props + + +def paste_props(source_props, paste_mode, category_filter=False): + """ + Core paste routine — runs inside an ExternalEvent (Revit API context). + paste_mode: "single" | "rectangle" | "selection" + """ + # Build category filter if the checkbox is ticked and categories are known + pick_filter = None + if category_filter: + cat_ids = set() + for p in source_props: + for c in p.categories or []: + if hasattr(c, "Id"): + cat_ids.add(c.Id) + if cat_ids: + pick_filter = PickByCategorySelectionFilter(list(cat_ids)) + + # Status-bar message shown to the user while picking + if len(source_props) == 1: + title = "Match: {} = {}".format( + source_props[0].name, + source_props[0].display_value or str(source_props[0].value), + ) + else: + title = "Pick elements to match {} parameter(s):".format(len(source_props)) + + with forms.WarningBar(title=title): + while True: + dest_elements = [] + + if paste_mode == "single": + elem = revit.pick_element(pick_filter=pick_filter) + if elem: + dest_elements = [elem] + + elif paste_mode == "rectangle": + try: + dest_elements = revit.pick_rectangle(pick_filter=pick_filter) + except Exception: + # To handle esc by user, this would throw a OperationCanceledException + pass + + elif paste_mode == "selection": + dest_elements = list(revit.get_selection()) + if category_filter: + dest_elements = [ + el for el in dest_elements + if el.Category and el.Category.Id in cat_ids + ] + + if not dest_elements: + break # user cancelled / nothing selected + + for dest in dest_elements: + dest_type = revit.query.get_type(dest) + with revit.Transaction("Match Properties"): + # type parameters first so instance params can reference them + match_prop(dest, dest_type, [p for p in source_props if p.istype]) + match_prop( + dest, dest_type, [p for p in source_props if not p.istype] + ) + + if paste_mode == "selection": + break # selection is one-shot, not a pick loop diff --git a/extensions/pyRevitTools.extension/lib/match/panel.py b/extensions/pyRevitTools.extension/lib/match/panel.py new file mode 100644 index 000000000..b0246c585 --- /dev/null +++ b/extensions/pyRevitTools.extension/lib/match/panel.py @@ -0,0 +1,342 @@ +# -*- coding: utf-8 -*- +import re + +from pyrevit import script, forms, revit, op +from pyrevit import UI +from pyrevit.revit.events import _GenericExternalEventHandler +from pyrevit.framework import ComponentModel + +from match_utils import ( + PropKeyValue, + get_source_properties, + safe_get_parameter, + paste_props +) +from filter_utils import get_most_common_filter_parameter, dissect_parameter_filter + +logger = script.get_logger() + +MAX_HISTORY_ITEMS = 50 + + +# ───────────────────────────────────────────────────────────────────────────── +# INotifyPropertyChanged base — required for two-way checkbox binding in WPF +# ───────────────────────────────────────────────────────────────────────────── + + +class _INotifyBase(ComponentModel.INotifyPropertyChanged): + def __init__(self): + self._handlers = [] + + def add_PropertyChanged(self, handler): + self._handlers.append(handler) + + def remove_PropertyChanged(self, handler): + if handler in self._handlers: + self._handlers.remove(handler) + + def _notify(self, prop_name): + ev_args = ComponentModel.PropertyChangedEventArgs(prop_name) + for h in self._handlers: + h(self, ev_args) + + +# ───────────────────────────────────────────────────────────────────────────── +# WPF-bindable list-view row backed by a PropKeyValue +# ───────────────────────────────────────────────────────────────────────────── + + +class ParameterItem(_INotifyBase): + """One row in the history list view.""" + + def __init__(self, pkv): + _INotifyBase.__init__(self) + self._pkv = pkv + self._selected = False + + # -- display properties (read by WPF bindings) -- + + @property + def Name(self): + return self._pkv.name or "" + + @property + def DisplayValue(self): + dv = self._pkv.display_value + if dv is None: + return str(self._pkv.value) if self._pkv.value is not None else "" + return dv + + @property + def Category(self): + """Single category name, 'multiple', or em-dash when unknown.""" + cats = self._pkv.categories or [] + if not cats: + return "unknown" + if len(cats) == 1: + c = cats[0] + return c.Name if hasattr(c, "Name") else str(c) + return "multiple" + + # -- checkable state with WPF change notification -- + + @property + def IsSelected(self): + return self._selected + + @IsSelected.setter + def IsSelected(self, value): + if self._selected != value: + self._selected = value + self._notify("IsSelected") + + # -- access to the underlying PropKeyValue for paste operations -- + + @property + def source_prop(self): + return self._pkv + + +# ───────────────────────────────────────────────────────────────────────────── +# Dockable pane +# ───────────────────────────────────────────────────────────────────────────── + + +class MatchHistoryClipboard(forms.WPFPanel): + panel_title = "pyRevit MatchHistory Clipboard" + panel_id = "0f3a0866-0123-4178-9f2c-121961bd292c" + panel_source = op.join(op.dirname(__file__), "clipboard_pane_ui.xaml") + + def __init__(self): + forms.WPFPanel.__init__(self) + self._handler = _GenericExternalEventHandler() + self._ext_event = UI.ExternalEvent.Create(self._handler) + self._items = [] # ordered list of ParameterItem (full history) + + # ── external-event plumbing ────────────────────────────────────────────── + + def _run_in_revit(self, func, *args, **kwargs): + """Schedule func(*args, **kwargs) to run in the next Revit event loop.""" + self._handler.func = func + self._handler.args = args + self._handler.kwargs = kwargs + self._ext_event.Raise() + + # ── history management ─────────────────────────────────────────────────── + + def _add_to_history(self, props): + """ + Prepend props to history, uncheck everything, enforce MAX_HISTORY_ITEMS. + Called after any of the three load-source actions. + """ + if not props: + return + new_items = [ParameterItem(p) for p in props] + self._items = (new_items + self._items)[:MAX_HISTORY_ITEMS] + for item in self._items: + item.IsSelected = False + self._refresh_list() + self._update_ui_state() + + def _selected_props(self): + """Return PropKeyValue objects for every checked history row.""" + return [item.source_prop for item in self._items if item.IsSelected] + + # ── list display / search filtering ───────────────────────────────────── + + def _refresh_list(self, search_text=None): + """ + Rebuild ListView.ItemsSource. + With no search_text the full history is shown. + With search_text either regex or substring match is applied to + both the parameter name and display value. + """ + if not search_text: + self.paramListView.ItemsSource = list(self._items) + return + + use_regex = bool(self.regexToggle_b.IsChecked) + if use_regex: + try: + pat = re.compile(search_text, re.IGNORECASE) + items = [ + i + for i in self._items + if pat.search(i.Name) or pat.search(i.DisplayValue) + ] + except re.error: + items = list(self._items) # invalid pattern → show all + else: + low = search_text.lower() + items = [ + i + for i in self._items + if low in i.Name.lower() or low in i.DisplayValue.lower() + ] + + self.paramListView.ItemsSource = items + + def _set_check_states(self, state=None, flip=False): + """ + Apply a uniform check state to all visible rows. + Deduplication: only the first occurrence of each parameter name + is checked; later duplicates are forced unchecked. + """ + seen = set() + source = self.paramListView.ItemsSource or [] + for item in source: + if item.Name not in seen: + seen.add(item.Name) + item.IsSelected = (not item.IsSelected) if flip else state + else: + item.IsSelected = False + self._update_ui_state() + + def _update_ui_state(self): + """Enable paste buttons only when at least one row is checked.""" + has_checked = any(i.IsSelected for i in self._items) + self.pasteSingleBtn.IsEnabled = has_checked + self.pasteRectBtn.IsEnabled = has_checked + self.pasteSelBtn.IsEnabled = has_checked + + # ── load-source handlers (Button Click events from XAML) ───────────────── + # NOTE: pick_element / get_source_properties are called directly here + # (not via _run_in_revit) because pyrevit's WPFPanel allows Revit picks + # from WPF event handlers. Only write-operations (Transactions) require + # the ExternalEvent mechanism. + + def load_from_element(self, sender, args): + """Pick an element, choose parameters interactively, add to history.""" + sel = revit.get_selection() + elem = sel[0] if len(sel) == 1 else revit.pick_element() + if not elem: + return + props = get_source_properties(elem) # opens pyrevit parameter-picker dialog + count = len(props) + self._add_to_history(props) + for i in range(min(count, len(self._items))): + self._items[i].IsSelected = True + self._update_ui_state() + + def load_from_view_filters(self, sender, args): + """Read all equals-filter parameter values from the active view.""" + view_filters = revit.query.get_view_filters(revit.active_view) + props = [] + for f in view_filters: + info = dissect_parameter_filter(revit.doc, f) + if not info: + continue + props.append( + PropKeyValue( + name=info["parameter_name"], + datatype=info["storage_type"], + value=info["value"], + istype=False, + display_value=info["display_value"], + categories=info["categories"], + ) + ) + self._add_to_history(props) + + def load_from_filter_and_element(self, sender, args): + """ + Read the value of the most-common filter parameter from a picked element. + Useful for quickly setting up a match from a 'key' parameter. + """ + param_id = get_most_common_filter_parameter(revit.doc, revit.active_view) + if not param_id: + logger.warning("No simple equals filter found on active view.") + return + sel = revit.get_selection() + elem = sel[0] if len(sel) == 1 else revit.pick_element() + if not elem: + return + try: + tparam = safe_get_parameter(elem, param_id) + if not tparam: + return + value = revit.query.get_param_value(tparam) + props = [ + PropKeyValue( + name=tparam.Definition.Name, + datatype=tparam.StorageType, + value=value, + istype=False, + display_value=tparam.AsValueString() or str(value), + categories=[elem.Category] + ) + ] + self._add_to_history(props) + self._items[0].IsSelected = True + self._update_ui_state() + except Exception as ex: + logger.warning("load_from_filter_and_element: %s", ex) + + # ── paste handlers ─────────────────────────────────────────────────────── + + def paste_single(self, sender, args): + """Paste checked parameters by picking elements one at a time (loops).""" + props = self._selected_props() + if props: + self._run_in_revit(paste_props, props, "single", bool(self.categoryFilterCheck.IsChecked)) + + def paste_rectangle(self, sender, args): + """Paste checked parameters to elements inside a drawn rectangle (loops).""" + props = self._selected_props() + if props: + self._run_in_revit(paste_props, props, "rectangle", bool(self.categoryFilterCheck.IsChecked)) + + def paste_selection(self, sender, args): + """Paste checked parameters to the current Revit selection (one-shot).""" + props = self._selected_props() + if props: + self._run_in_revit(paste_props, props, "selection", bool(self.categoryFilterCheck.IsChecked)) + + # ── check / search UI handlers ─────────────────────────────────────────── + + def check_all(self, sender, args): + self._set_check_states(state=True) + + def uncheck_all(self, sender, args): + self._set_check_states(state=False) + + def toggle_all(self, sender, args): + self._set_check_states(flip=True) + + def toggle_regex(self, sender, args): + """Switch between substring and regex search; swap the button icon.""" + if bool(self.regexToggle_b.IsChecked): + self.regexToggle_b.Content = self.Resources["regexIcon"] + else: + self.regexToggle_b.Content = self.Resources["filterIcon"] + text = self.search_tb.Text.strip() + self._refresh_list(search_text=text if text else None) + self.search_tb.Focus() + + def clear_search(self, sender, args): + self.search_tb.Text = "" + self.search_tb.Focus() + + def search_changed(self, sender, args): + """TextChanged handler — show/hide the clear button, refresh list.""" + text = self.search_tb.Text + if text: + self.show_element(self.clrsearch_b) + else: + self.hide_element(self.clrsearch_b) + stripped = text.strip() + self._refresh_list(search_text=stripped if stripped else None) + + def checkbox_click(self, sender, args): + """ + When a row is checked, uncheck all other rows that share the same + parameter name — prevents duplicate parameters being applied twice. + """ + clicked = sender.DataContext + if not clicked: + return + if clicked.IsSelected: + for item in self._items: + if item is not clicked and item.Name == clicked.Name: + item.IsSelected = False + self._update_ui_state() diff --git a/extensions/pyRevitTools.extension/pyRevit.tab/Analysis.panel/Tools.stack/Inspect.pulldown/Compare Detail Views.deprecate/script.py b/extensions/pyRevitTools.extension/pyRevit.tab/Analysis.panel/Tools.stack/Inspect.pulldown/Compare Detail Views.deprecate/script.py index 51ef99e46..34f3122b3 100644 --- a/extensions/pyRevitTools.extension/pyRevit.tab/Analysis.panel/Tools.stack/Inspect.pulldown/Compare Detail Views.deprecate/script.py +++ b/extensions/pyRevitTools.extension/pyRevit.tab/Analysis.panel/Tools.stack/Inspect.pulldown/Compare Detail Views.deprecate/script.py @@ -1,4 +1,4 @@ -from pyrevit import revit, DB, UI +from pyrevit import revit, DB, EXEC_PARAMS from pyrevit import forms import diffutils @@ -16,7 +16,7 @@ comp = diffutils.compare_views(revit.doc, view_list[0], view_list[1], - compare_types=__shiftclick__, + compare_types=EXEC_PARAMS.config_mode, diff_results=res) forms.alert('Views are smiliar (not identical).' diff --git a/extensions/pyRevitTools.extension/pyRevit.tab/Analysis.panel/Tools.stack/Inspect.pulldown/LinesPerViewCounter.pushbutton/LinesPerViewCounter_script.py b/extensions/pyRevitTools.extension/pyRevit.tab/Analysis.panel/Tools.stack/Inspect.pulldown/LinesPerViewCounter.pushbutton/LinesPerViewCounter_script.py index d1822019a..9a4fa8a65 100644 --- a/extensions/pyRevitTools.extension/pyRevit.tab/Analysis.panel/Tools.stack/Inspect.pulldown/LinesPerViewCounter.pushbutton/LinesPerViewCounter_script.py +++ b/extensions/pyRevitTools.extension/pyRevit.tab/Analysis.panel/Tools.stack/Inspect.pulldown/LinesPerViewCounter.pushbutton/LinesPerViewCounter_script.py @@ -2,7 +2,7 @@ from collections import defaultdict from pyrevit import script -from pyrevit import revit, DB +from pyrevit import revit, DB, EXEC_PARAMS from pyrevit.compat import get_elementid_value_func @@ -62,7 +62,7 @@ def line_count(document=doc): if __name__ == '__main__': output.print_md("\n\n# LINES PER VIEW IN CURRENT DOCUMENT\n___\n\n") line_count() - if __shiftclick__: + if EXEC_PARAMS.config_mode: output.print_md("\n\n# LINES PER VIEW IN LINKS\n___\n\n") revit_links = DB.FilteredElementCollector(doc).OfClass(DB.RevitLinkInstance).ToElements() for link in revit_links: diff --git a/extensions/pyRevitTools.extension/pyRevit.tab/Drawing Set.panel/Keynotes.pushbutton/EditRecord.xaml b/extensions/pyRevitTools.extension/pyRevit.tab/Drawing Set.panel/Keynotes.pushbutton/EditRecord.xaml index 44d954204..383621d40 100644 --- a/extensions/pyRevitTools.extension/pyRevit.tab/Drawing Set.panel/Keynotes.pushbutton/EditRecord.xaml +++ b/extensions/pyRevitTools.extension/pyRevit.tab/Drawing Set.panel/Keynotes.pushbutton/EditRecord.xaml @@ -1,85 +1,85 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +