From d101186e39da3b4ec62fa44eec7735b08ddeb780 Mon Sep 17 00:00:00 2001 From: Igor Malyushkin Date: Thu, 19 Oct 2023 09:40:51 +0700 Subject: [PATCH] ver 0.1.5-alpha --- README.md | 4 - poetry.lock | 227 ++++++------ pyproject.toml | 5 +- requirements.txt | Bin 1028 -> 1066 bytes thymus/__init__.py | 12 +- thymus/app_settings.py | 91 +++-- thymus/clier.py | 48 ++- thymus/contexts/context.py | 183 ++++++---- thymus/contexts/ios.py | 176 +++++----- thymus/contexts/junos.py | 160 +++++---- thymus/lexers/__init__.py | 9 +- thymus/lexers/common/__init__.py | 16 + thymus/lexers/common/common.py | 24 +- thymus/lexers/ios/__init__.py | 5 + thymus/lexers/ios/ios.py | 4 +- thymus/lexers/junos/__init__.py | 5 + thymus/lexers/junos/junos.py | 4 +- thymus/parsers/ios/__init__.py | 21 -- thymus/parsers/ios/ios.py | 365 -------------------- thymus/parsers/junos/__init__.py | 39 --- thymus/parsers/junos/junos.py | 337 ------------------ thymus/responses/responses.py | 22 +- thymus/styles/main.css | 190 ++++++++++ thymus/tui/__init__.py | 7 + thymus/tui/modals/__init__.py | 21 ++ thymus/tui/modals/contexts_modal.py | 6 +- thymus/tui/modals/error_modal.py | 4 +- thymus/tui/modals/logs_modal.py | 4 +- thymus/tui/modals/quit_modal.py | 6 +- thymus/tui/net_loader.py | 2 +- thymus/tui/open_dialog.py | 13 +- thymus/tui/working_screen/__init__.py | 7 + thymus/tui/working_screen/extended_input.py | 11 +- thymus/tui/working_screen/working_screen.py | 52 ++- thymus/tuier.py | 21 +- 35 files changed, 866 insertions(+), 1235 deletions(-) create mode 100644 thymus/lexers/common/__init__.py create mode 100644 thymus/lexers/ios/__init__.py create mode 100644 thymus/lexers/junos/__init__.py delete mode 100644 thymus/parsers/ios/__init__.py delete mode 100644 thymus/parsers/ios/ios.py delete mode 100644 thymus/parsers/junos/__init__.py delete mode 100644 thymus/parsers/junos/junos.py create mode 100644 thymus/styles/main.css create mode 100644 thymus/tui/__init__.py create mode 100644 thymus/tui/modals/__init__.py create mode 100644 thymus/tui/working_screen/__init__.py diff --git a/README.md b/README.md index 48d8d1c..fbe5a83 100644 --- a/README.md +++ b/README.md @@ -33,10 +33,6 @@ Thymus supports: -## Requirements - -Python **3.8.1**. Please, see also the `requirements.txt`. - ## Modes Thymus operates in two modes: diff --git a/poetry.lock b/poetry.lock index 9d14bf8..fb6e4ca 100644 --- a/poetry.lock +++ b/poetry.lock @@ -37,76 +37,64 @@ typecheck = ["mypy"] [[package]] name = "cffi" -version = "1.15.1" +version = "1.16.0" description = "Foreign Function Interface for Python calling C code." category = "main" optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, - {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, - {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, - {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, - {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, - {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, - {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, - {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, - {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, - {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, - {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, - {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, - {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, - {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, - {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, - {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, - {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, - {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, - {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, + {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, + {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, + {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, + {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, + {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, + {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, + {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, + {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, + {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, + {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, + {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, + {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, + {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, ] [package.dependencies] @@ -114,35 +102,35 @@ pycparser = "*" [[package]] name = "cryptography" -version = "41.0.3" +version = "41.0.4" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-41.0.3-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:652627a055cb52a84f8c448185922241dd5217443ca194d5739b44612c5e6507"}, - {file = "cryptography-41.0.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:8f09daa483aedea50d249ef98ed500569841d6498aa9c9f4b0531b9964658922"}, - {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fd871184321100fb400d759ad0cddddf284c4b696568204d281c902fc7b0d81"}, - {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84537453d57f55a50a5b6835622ee405816999a7113267739a1b4581f83535bd"}, - {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3fb248989b6363906827284cd20cca63bb1a757e0a2864d4c1682a985e3dca47"}, - {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:42cb413e01a5d36da9929baa9d70ca90d90b969269e5a12d39c1e0d475010116"}, - {file = "cryptography-41.0.3-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:aeb57c421b34af8f9fe830e1955bf493a86a7996cc1338fe41b30047d16e962c"}, - {file = "cryptography-41.0.3-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6af1c6387c531cd364b72c28daa29232162010d952ceb7e5ca8e2827526aceae"}, - {file = "cryptography-41.0.3-cp37-abi3-win32.whl", hash = "sha256:0d09fb5356f975974dbcb595ad2d178305e5050656affb7890a1583f5e02a306"}, - {file = "cryptography-41.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:a983e441a00a9d57a4d7c91b3116a37ae602907a7618b882c8013b5762e80574"}, - {file = "cryptography-41.0.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5259cb659aa43005eb55a0e4ff2c825ca111a0da1814202c64d28a985d33b087"}, - {file = "cryptography-41.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:67e120e9a577c64fe1f611e53b30b3e69744e5910ff3b6e97e935aeb96005858"}, - {file = "cryptography-41.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7efe8041897fe7a50863e51b77789b657a133c75c3b094e51b5e4b5cec7bf906"}, - {file = "cryptography-41.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce785cf81a7bdade534297ef9e490ddff800d956625020ab2ec2780a556c313e"}, - {file = "cryptography-41.0.3-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:57a51b89f954f216a81c9d057bf1a24e2f36e764a1ca9a501a6964eb4a6800dd"}, - {file = "cryptography-41.0.3-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c2f0d35703d61002a2bbdcf15548ebb701cfdd83cdc12471d2bae80878a4207"}, - {file = "cryptography-41.0.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:23c2d778cf829f7d0ae180600b17e9fceea3c2ef8b31a99e3c694cbbf3a24b84"}, - {file = "cryptography-41.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:95dd7f261bb76948b52a5330ba5202b91a26fbac13ad0e9fc8a3ac04752058c7"}, - {file = "cryptography-41.0.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:41d7aa7cdfded09b3d73a47f429c298e80796c8e825ddfadc84c8a7f12df212d"}, - {file = "cryptography-41.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d0d651aa754ef58d75cec6edfbd21259d93810b73f6ec246436a21b7841908de"}, - {file = "cryptography-41.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ab8de0d091acbf778f74286f4989cf3d1528336af1b59f3e5d2ebca8b5fe49e1"}, - {file = "cryptography-41.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a74fbcdb2a0d46fe00504f571a2a540532f4c188e6ccf26f1f178480117b33c4"}, - {file = "cryptography-41.0.3.tar.gz", hash = "sha256:6d192741113ef5e30d89dcb5b956ef4e1578f304708701b8b73d38e3e1461f34"}, + {file = "cryptography-41.0.4-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:80907d3faa55dc5434a16579952ac6da800935cd98d14dbd62f6f042c7f5e839"}, + {file = "cryptography-41.0.4-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:35c00f637cd0b9d5b6c6bd11b6c3359194a8eba9c46d4e875a3660e3b400005f"}, + {file = "cryptography-41.0.4-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cecfefa17042941f94ab54f769c8ce0fe14beff2694e9ac684176a2535bf9714"}, + {file = "cryptography-41.0.4-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e40211b4923ba5a6dc9769eab704bdb3fbb58d56c5b336d30996c24fcf12aadb"}, + {file = "cryptography-41.0.4-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:23a25c09dfd0d9f28da2352503b23e086f8e78096b9fd585d1d14eca01613e13"}, + {file = "cryptography-41.0.4-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2ed09183922d66c4ec5fdaa59b4d14e105c084dd0febd27452de8f6f74704143"}, + {file = "cryptography-41.0.4-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:5a0f09cefded00e648a127048119f77bc2b2ec61e736660b5789e638f43cc397"}, + {file = "cryptography-41.0.4-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:9eeb77214afae972a00dee47382d2591abe77bdae166bda672fb1e24702a3860"}, + {file = "cryptography-41.0.4-cp37-abi3-win32.whl", hash = "sha256:3b224890962a2d7b57cf5eeb16ccaafba6083f7b811829f00476309bce2fe0fd"}, + {file = "cryptography-41.0.4-cp37-abi3-win_amd64.whl", hash = "sha256:c880eba5175f4307129784eca96f4e70b88e57aa3f680aeba3bab0e980b0f37d"}, + {file = "cryptography-41.0.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:004b6ccc95943f6a9ad3142cfabcc769d7ee38a3f60fb0dddbfb431f818c3a67"}, + {file = "cryptography-41.0.4-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:86defa8d248c3fa029da68ce61fe735432b047e32179883bdb1e79ed9bb8195e"}, + {file = "cryptography-41.0.4-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:37480760ae08065437e6573d14be973112c9e6dcaf5f11d00147ee74f37a3829"}, + {file = "cryptography-41.0.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b5f4dfe950ff0479f1f00eda09c18798d4f49b98f4e2006d644b3301682ebdca"}, + {file = "cryptography-41.0.4-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7e53db173370dea832190870e975a1e09c86a879b613948f09eb49324218c14d"}, + {file = "cryptography-41.0.4-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5b72205a360f3b6176485a333256b9bcd48700fc755fef51c8e7e67c4b63e3ac"}, + {file = "cryptography-41.0.4-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:93530900d14c37a46ce3d6c9e6fd35dbe5f5601bf6b3a5c325c7bffc030344d9"}, + {file = "cryptography-41.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:efc8ad4e6fc4f1752ebfb58aefece8b4e3c4cae940b0994d43649bdfce8d0d4f"}, + {file = "cryptography-41.0.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c3391bd8e6de35f6f1140e50aaeb3e2b3d6a9012536ca23ab0d9c35ec18c8a91"}, + {file = "cryptography-41.0.4-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0d9409894f495d465fe6fda92cb70e8323e9648af912d5b9141d616df40a87b8"}, + {file = "cryptography-41.0.4-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8ac4f9ead4bbd0bc8ab2d318f97d85147167a488be0e08814a37eb2f439d5cf6"}, + {file = "cryptography-41.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:047c4603aeb4bbd8db2756e38f5b8bd7e94318c047cfe4efeb5d715e08b49311"}, + {file = "cryptography-41.0.4.tar.gz", hash = "sha256:7febc3094125fc126a7f6fb1f420d0da639f3f32cb15c8ff0dc3997c4549f51a"}, ] [package.dependencies] @@ -320,14 +308,14 @@ textfsm = ">=1.1.3" [[package]] name = "ntc-templates" -version = "3.5.0" +version = "4.0.1" description = "TextFSM Templates for Network Devices, and Python wrapper for TextFSM's CliTable." category = "main" optional = false python-versions = ">=3.7,<4.0" files = [ - {file = "ntc_templates-3.5.0-py3-none-any.whl", hash = "sha256:86d75c077eb1ceb97f4f8c69c9e3c7a32b08210ceb8228e5fa4e87e080746fd4"}, - {file = "ntc_templates-3.5.0.tar.gz", hash = "sha256:ee0dab4440dab1b3286549f8c08695b30037c1f36f55763c5a39005525f722c7"}, + {file = "ntc_templates-4.0.1-py3-none-any.whl", hash = "sha256:4d20943fdffc70595fb2b983c6fcab926635c3e4621aaec13a9063a9a61241dd"}, + {file = "ntc_templates-4.0.1.tar.gz", hash = "sha256:5bd158592ac99e769a0b7e82e53fd714a410f912fc9e438e95cc0130cf7290a8"}, ] [package.dependencies] @@ -357,14 +345,14 @@ invoke = ["invoke (>=2.0)"] [[package]] name = "pycodestyle" -version = "2.11.0" +version = "2.11.1" description = "Python style guide checker" category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "pycodestyle-2.11.0-py2.py3-none-any.whl", hash = "sha256:5d1013ba8dc7895b548be5afb05740ca82454fd899971563d2ef625d090326f8"}, - {file = "pycodestyle-2.11.0.tar.gz", hash = "sha256:259bcc17857d8a8b3b4a2327324b79e5f020a13c16074670f9c8c8f872ea76d0"}, + {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, + {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, ] [[package]] @@ -500,14 +488,14 @@ files = [ [[package]] name = "rich" -version = "13.5.2" +version = "13.6.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" category = "main" optional = false python-versions = ">=3.7.0" files = [ - {file = "rich-13.5.2-py3-none-any.whl", hash = "sha256:146a90b3b6b47cac4a73c12866a499e9817426423f57c5a66949c086191a8808"}, - {file = "rich-13.5.2.tar.gz", hash = "sha256:fb9d6c0a0f643c99eed3875b5377a184132ba9be4d61516a55273d3554d75a39"}, + {file = "rich-13.6.0-py3-none-any.whl", hash = "sha256:2b38e2fe9ca72c9a00170a1a2d20c63c790d0e10ef1fe35eba76e1e7b1d7d245"}, + {file = "rich-13.6.0.tar.gz", hash = "sha256:5c14d22737e6d5084ef4771b62d5d4363165b403455a30a1c8ca39dc7b644bef"}, ] [package.dependencies] @@ -563,14 +551,14 @@ six = "*" [[package]] name = "textual" -version = "0.36.0" +version = "0.40.0" description = "Modern Text User Interface framework" category = "main" optional = false python-versions = ">=3.7,<4.0" files = [ - {file = "textual-0.36.0-py3-none-any.whl", hash = "sha256:7d04880bee0274f8cdf05cbe22d9effad3efa458676af2c431997a6d4576005c"}, - {file = "textual-0.36.0.tar.gz", hash = "sha256:fbfc799a55938cfade6cfbf7c5ae3c3e5fc87ff9deaaed788a6dcefe72245451"}, + {file = "textual-0.40.0-py3-none-any.whl", hash = "sha256:3e98f0c9c9a9361d3077c00e3fc5a708f927dd1ce45a1149eb1ba6945ce9d71c"}, + {file = "textual-0.40.0.tar.gz", hash = "sha256:0fd014f9fab7f6d88167c82f90e115b118b3016b8597281d14c9257967f7812e"}, ] [package.dependencies] @@ -579,16 +567,31 @@ markdown-it-py = {version = ">=2.1.0", extras = ["linkify", "plugins"]} rich = ">=13.3.3" typing-extensions = ">=4.4.0,<5.0.0" +[package.extras] +syntax = ["tree-sitter (>=0.20.1,<0.21.0)", "tree_sitter_languages (>=1.7.0)"] + +[[package]] +name = "thymus-ast" +version = "0.1.2" +description = "A set of tools to build ASTs for different NOS" +category = "main" +optional = false +python-versions = ">=3.8.1,<4.0.0" +files = [ + {file = "thymus_ast-0.1.2-py3-none-any.whl", hash = "sha256:82d7ab525c37c4a115b4d4adb9ebe8ed2bf17d5e903fc6dfd0b62536f702d262"}, + {file = "thymus_ast-0.1.2.tar.gz", hash = "sha256:c9438bd001ac91130ef9b0b41439a336f66630f2222529df8573e7872b6bcca8"}, +] + [[package]] name = "typing-extensions" -version = "4.7.1" -description = "Backported and Experimental Type Hints for Python 3.7+" +version = "4.8.0" +description = "Backported and Experimental Type Hints for Python 3.8+" category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, - {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, + {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, + {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, ] [[package]] @@ -608,21 +611,21 @@ test = ["coverage", "pytest", "pytest-cov"] [[package]] name = "zipp" -version = "3.16.2" +version = "3.17.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "zipp-3.16.2-py3-none-any.whl", hash = "sha256:679e51dd4403591b2d6838a48de3d283f3d188412a9782faadf845f298736ba0"}, - {file = "zipp-3.16.2.tar.gz", hash = "sha256:ebc15946aa78bd63458992fc81ec3b6f7b1e92d51c35e6de1c3804e73b799147"}, + {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, + {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] [metadata] lock-version = "2.0" python-versions = "^3.8.1" -content-hash = "41925189ce311c5528bee424634c5cac61d58083d9fb4c9e53342b6ad479d160" +content-hash = "5b87d44ac4a85031603cf46a66d3e3bdb52461964bd6462e30e170bc6e40602d" diff --git a/pyproject.toml b/pyproject.toml index 9d30fd5..1bd5d85 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,14 +1,15 @@ [tool.poetry] name = "thymus" -version = "0.1.3" +version = "0.1.5" description = "A browser for network configuration files" authors = ["Igor Malyushkin "] readme = "README.md" [tool.poetry.dependencies] python = "^3.8.1" -textual = "0.36.0" +textual = "0.40.0" netmiko = "^4.2.0" +thymus-ast = "^0.1.2" [tool.poetry.group.dev.dependencies] diff --git a/requirements.txt b/requirements.txt index 04f35eeec784b8d949314f18fc3744c871d7255f..87bb196e4cbdfbdc58043d32bc8ea899cca06b75 100644 GIT binary patch delta 146 zcmZqSSj92Hh}DcikHKJ~xi+K8#^hv1#dwAihE#@Jh608hhD0#Cn86kZO+dfaMGrcp12WdNLR)fhLqP6f@`o%_s(nfJ^}E2AW_rxt&>` S)dFPR= 9: + from collections.abc import Callable +else: + from typing import Callable + +from pygments.styles import get_all_styles # type: ignore from . import __version__ as app_ver from . import ( @@ -37,57 +47,45 @@ SAVES_DIR, SCREENS_SAVES_DIR, ) -from .responses import SettingsResponse - -import os -import sys -import json -import logging - - -if TYPE_CHECKING: - from typing import Any - if sys.version_info.major == 3 and sys.version_info.minor >= 9: - from collections.abc import Callable - else: - from typing import Callable +from .responses import Response, SettingsResponse -DEFAULT_GLOBALS = { +DEFAULT_GLOBALS: dict[str, str | int] = { 'theme': 'monokai', 'night_mode': 'off', 'filename_len': 256, 'sidebar_limit': 64, 'sidebar_strict_on_tab': 'on', + 'open_dialog_platform': 'junos', } -DEFAULT_JUNOS = { +DEFAULT_JUNOS: dict[str, str | int] = { 'spaces': 2, } -DEFAULT_IOS = { +DEFAULT_IOS: dict[str, str | int] = { 'spaces': 1, 'heuristics': 'off', 'base_heuristics': 'on', 'crop': 'off', 'promisc': 'off', } -DEFAULT_EOS = { +DEFAULT_EOS: dict[str, str | int] = { 'spaces': 2, 'heuristics': 'off', 'base_heuristics': 'on', 'crop': 'off', 'promisc': 'off', } -DEFAULT_NXOS = { +DEFAULT_NXOS: dict[str, str | int] = { 'spaces': 2, 'heuristics': 'off', 'base_heuristics': 'on', 'crop': 'off', } -PLATFORMS = { +PLATFORMS: dict[str, dict[str, str | int]] = { 'junos': DEFAULT_JUNOS, 'ios': DEFAULT_IOS, - 'eos': DEFAULT_EOS, 'nxos': DEFAULT_NXOS, + 'eos': DEFAULT_EOS, } @@ -128,8 +126,11 @@ def styles(self) -> list[str]: def logger(self) -> logging.Logger: return self.__logger + @property + def platforms(self) -> list[str]: + return list(self.__platforms.keys()) + def __init__(self) -> None: - self.__logger: logging.Logger = None self.__globals: dict[str, str | int] = {} self.__platforms: dict[str, dict[str, str | int]] = {} for platform in PLATFORMS: @@ -256,7 +257,9 @@ def __save_config(self) -> None: if not self.__is_dir: return self.__logger.debug(f'Saving a configuration into the file: {CONFIG_PATH}{CONFIG_NAME}.') - data = self.globals + data: dict[str, str | int | dict[str, str | int]] = {} + for k, v in self.globals.items(): + data[k] = v for platform, platform_data in PLATFORMS.items(): data.update( { @@ -301,7 +304,6 @@ def validate_keys( errors.append(err_msg) except Exception: # makes it default - err_msg: str = '' if platform: err_msg = f'Incorrect value for the global attribute "{key}": {value}.' self.__platforms[platform][key] = store[key] @@ -325,6 +327,9 @@ def __validate_globals(self, key: str, value: str | int) -> str | int: value = int(value) if value <= 0 or value > N_VALUE_LIMIT: raise Exception + elif key == 'open_dialog_platform': + if value not in PLATFORMS: + raise Exception elif key in ('sidebar_strict_on_tab', 'night_mode'): if value not in ('0', '1', 'on', 'off', 0, 1): raise Exception @@ -389,7 +394,7 @@ def is_bool_set(self, key: str, *, attr_name: str = 'globals') -> bool: return False return attr[key] in (1, '1', 'on') - def process_command(self, command: str) -> SettingsResponse: + def process_command(self, command: str) -> Response: if not command.startswith('global '): return SettingsResponse.error('Unknown global command.') parts = command.split() @@ -403,21 +408,26 @@ def process_command(self, command: str) -> SettingsResponse: if arg == 'themes': if len(parts) == 4: return SettingsResponse.error(f'Too many arguments for "global show {arg}" command.') - result: list[str] = [] - result.append('* -- current theme') + themes_result = [] + themes_result.append('* -- current theme') for theme in self.styles: - result.append(f'{theme}*' if theme == self.globals['theme'] else theme) - return SettingsResponse.success(result) + themes_result.append(f'{theme}*' if theme == self.globals['theme'] else theme) + return SettingsResponse.success(themes_result) + elif arg == 'open_dialog_platform': + if len(parts) == 4: + return SettingsResponse.error(f'Too many arguments for "global show {arg}" command.') + platf_result = self.globals[arg] + return SettingsResponse.success(str(platf_result)) elif arg in ('filename_len', 'sidebar_limit'): if len(parts) == 4: return SettingsResponse.error(f'Too many arguments for "global show {arg}" command.') - result: int = self.globals[arg] - return SettingsResponse.success(str(result)) + num_result = self.globals[arg] + return SettingsResponse.success(str(num_result)) elif arg in ('sidebar_strict_on_tab', 'night_mode'): if len(parts) == 4: return SettingsResponse.error(f'Too many arguments for "global show {arg}" command.') - result: bool = self.is_bool_set(arg) - return SettingsResponse.success(str(result)) + bool_result = self.is_bool_set(arg) + return SettingsResponse.success(str(bool_result)) elif arg in PLATFORMS: if len(parts) == 4: subarg = parts[3] @@ -450,6 +460,15 @@ def process_command(self, command: str) -> SettingsResponse: self.__globals[arg] = value self.__save_config() return SettingsResponse.success(f'The "{arg}" was changed to: {value}.') + elif arg == 'open_dialog_platform': + if len(parts) > 4: + return SettingsResponse.error('Too many arguments for "global set" command.') + value = parts[3] + if value not in PLATFORMS: + return SettingsResponse.error(f'Unsupported platform: {value}.') + self.__globals[arg] = value + self.__save_config() + return SettingsResponse.success(f'The "{arg}" was changed to: {value}.') elif arg in ('filename_len', 'sidebar_limit'): if len(parts) > 4: return SettingsResponse.error(f'Too many arguments for "global set {arg}" command.') @@ -464,7 +483,7 @@ def process_command(self, command: str) -> SettingsResponse: return SettingsResponse.error(f'Too many arguments for "global set {arg}" command.') value = parts[3] if value not in ('0', '1', 'on', 'off', 0, 1): - raise SettingsResponse.error('Value must be in (0, 1, on, off).') + return SettingsResponse.error('Value must be in (0, 1, on, off).') self.__globals[arg] = value self.__save_config() return SettingsResponse.success(f'The "{arg}" was changed to: {value}.') diff --git a/thymus/clier.py b/thymus/clier.py index 88e779a..959a841 100644 --- a/thymus/clier.py +++ b/thymus/clier.py @@ -1,21 +1,20 @@ from __future__ import annotations -from typing import TYPE_CHECKING +import sys +import time + +from typing import Optional +from logging import Logger, getLogger from .contexts import ( + Context, JunOSContext, IOSContext, EOSContext ) -import sys -import time - - -if TYPE_CHECKING: - from .contexts import Context - -NOS_LIST = { +ENCODING = 'utf-8' +NOS_LIST: dict[str, type[Context]] = { 'junos': JunOSContext, 'ios': IOSContext, 'eos': EOSContext, @@ -34,18 +33,20 @@ class SystemWrapper: '__contexts', '__current', '__number', + '__logger', ) def __init__(self) -> None: self.__base_prompt: str = 'thymus> ' self.__contexts: dict[str, Context] = {} - self.__current: Context = {} + self.__current: Optional[Context] = None self.__number: int = 0 + self.__logger: Logger = getLogger('clier_logger') def __open_config(self, args: list[str]) -> None: if len(args) != 2: err_print('Incorrect arguments for "open". Usage: "open nos_type file".') - return + return None nos = args[0] config_path = args[1] nos = nos.lower() @@ -53,29 +54,37 @@ def __open_config(self, args: list[str]) -> None: err_print(f'Unknown network OS: {nos}. Use:') for key in NOS_LIST: err_print(f'\t{key}') - return + return None config: list[str] = [] try: with open(config_path, encoding='utf-8-sig', errors='replace') as f: config = f.readlines() except FileNotFoundError: err_print(f'Cannot open the file: {config_path}.') - return + return None context_name = f'vty{self.__number}' - self.__contexts[context_name] = NOS_LIST[nos](context_name, config) + self.__contexts[context_name] = NOS_LIST[nos]( + context_name, + config, + encoding=ENCODING, + settings={}, + logger=self.__logger + ) self.__current = self.__contexts[context_name] self.__number += 1 + print(f'[{nos}] "{config_path}" successfully opened!') def __switch_context(self, args: list[str]) -> None: if len(args) != 1: err_print('Incorrect arguments for "switch".') - return + return None context_name = args[0] if context_name not in self.__contexts or not self.__contexts[context_name]: err_print(f'No such a context: {context_name}.') - return + return None self.__current = self.__contexts[context_name] print(f'Context is switched to: {context_name}.') + return None def __new_prompt(self) -> str: if not self.__current: @@ -104,10 +113,12 @@ def process_command(self, value: str) -> str: err_print('Unknown command or no valid context.') return self.__base_prompt result = self.__current.on_enter(value) - out = print if result.is_ok else err_print for line in result.value: if line: - out(line) + if result.is_ok: + print(line) + else: + err_print(line) return self.__new_prompt() return self.__base_prompt @@ -129,3 +140,4 @@ def main() -> None: t = time.time() prompt = cli.process_command(user_input) print(f'\nCommand execution time is {time.time() - t} secs.') + return None diff --git a/thymus/contexts/context.py b/thymus/contexts/context.py index 6024b67..a8d3aa3 100644 --- a/thymus/contexts/context.py +++ b/thymus/contexts/context.py @@ -1,40 +1,41 @@ from __future__ import annotations +import shlex +import re +import sys +import os + from functools import reduce from collections import deque -from typing import TYPE_CHECKING +from typing import Any from logging import Logger, getLogger +from abc import ABC, abstractmethod from .. import SAVES_DIR -from ..responses import AlertResponse +from ..responses import Response, AlertResponse from ..lexers import CommonLexer -import shlex -import re -import sys -import os +if sys.version_info.major == 3 and sys.version_info.minor >= 9: + from collections.abc import Generator, Iterator +else: + from typing import Generator, Iterator -if TYPE_CHECKING: - if sys.version_info.major == 3 and sys.version_info.minor >= 9: - from collections.abc import Generator, Iterable - else: - from typing import Generator, Iterable - - from ..responses import Response - UP_LIMIT = 8 +CMD_LOG_LIMIT = 30 -class Context: +class Context(ABC): __slots__: tuple[str, ...] = ( '__name', '__content', '__encoding', '__spaces', '__logger', + '__commands_log', + '__commands_index', ) - __names_cache: list[tuple[Context, str]] = [] + __names_cache: list[tuple[type[Context], str]] = [] delimiter: str = '^' keywords: dict[str, list[str]] = { 'go': ['go'], @@ -54,20 +55,41 @@ class Context: 'help': ['help'], 'global': ['global'], } - lexer: CommonLexer = CommonLexer + lexer: type[CommonLexer] = CommonLexer @property + @abstractmethod def prompt(self) -> str: - return '' + raise NotImplementedError @property def name(self) -> str: return self.__name + @name.setter + def name(self, value: str) -> None: + if type(value) is not str: + raise TypeError('Context name type must str.') + if not re.match(r'^[0-9a-z]{4,16}$', value, re.I): + raise ValueError('Incorrect format of the name: use only these 0-9 or a-z.\nFrom 4 to 16 symbols.') + if (type(self), value) not in self.__names_cache: + self.__names_cache.append((type(self), value)) + else: + raise ValueError(f'The name "{value}" is already set.') + self.__name = value + @property def encoding(self) -> str: return self.__encoding + @encoding.setter + def encoding(self, value: str) -> None: + try: + 'schlop'.encode(value) + self.__encoding = value + except LookupError: + raise ValueError(f'"{value}" is not a correct encoding.') + @property def content(self) -> list[str]: return self.__content @@ -76,39 +98,27 @@ def content(self) -> list[str]: def spaces(self) -> int: return self.__spaces - @property - def nos_type(self) -> str: - return '' - - @property - def logger(self) -> Logger: - return self.__logger - @spaces.setter def spaces(self, value: int | str) -> None: + tval = 0 if type(value) is str and value.isdigit(): - value = int(value) - if value not in (1, 2, 4): + tval = int(value) + elif type(value) is int: + tval = value + else: + raise TypeError('Spaces type must be int or str (digital).') + if tval not in (1, 2, 4): raise ValueError('Spaces number can be 1, 2, 4.') - self.__spaces = value + self.__spaces = tval - @name.setter - def name(self, value: str) -> None: - if type(value) is not str or not re.match(r'^[0-9a-z]{4,16}$', value, re.I): - raise ValueError('Incorrect format of the name: use only these 0-9 or a-z.\nFrom 4 to 16 symbols.') - if (type(self), value) not in self.__names_cache: - self.__names_cache.append((type(self), value)) - else: - raise ValueError(f'The name "{value}" is already set.') - self.__name = value + @property + @abstractmethod + def nos_type(self) -> str: + raise NotImplementedError - @encoding.setter - def encoding(self, value: str) -> None: - try: - 'schlop'.encode(value) - self.__encoding = value - except LookupError: - raise ValueError(f'"{value}" is not a correct encoding.') + @property + def logger(self) -> Logger: + return self.__logger @logger.setter def logger(self, value: Logger) -> None: @@ -116,6 +126,11 @@ def logger(self, value: Logger) -> None: raise ValueError('Incorrect type of a logger.') self.__logger = value + @property + @abstractmethod + def tree(self) -> Any: + raise NotImplementedError + def __init__( self, name: str, @@ -131,6 +146,8 @@ def __init__( self.__logger = logger if logger else getLogger() self.__spaces = 2 self.apply_settings(settings) + self.__commands_log: deque[str] = deque() + self.__commands_index = -1 def free(self) -> None: if (type(self), self.__name) in self.__names_cache: @@ -145,16 +162,48 @@ def apply_settings(self, settings: dict[str, str | int]) -> None: except Exception as err: self.__logger.error(f'{err}') - def command_show(self, args: deque[str] = [], mods: list[list[str]] = []) -> Response: + def add_input_to_log(self, input: str) -> None: + if not input: + return + if input in self.__commands_log: + return + if len(self.__commands_log) == CMD_LOG_LIMIT: + self.__commands_log.popleft() + self.__commands_log.append(input) + self.__commands_index = len(self.__commands_log) - 1 + + def get_input_from_log(self, *, forward: bool = True) -> str: + result = '' + if not self.__commands_log: + return '' + if forward: + result = self.__commands_log[self.__commands_index] + if not self.__commands_index: + self.__commands_index = len(self.__commands_log) - 1 + else: + self.__commands_index -= 1 + else: + if self.__commands_index >= len(self.__commands_log) - 1: + self.__commands_index = 0 + else: + self.__commands_index += 1 + result = self.__commands_log[self.__commands_index] + return result + + @abstractmethod + def command_show(self, args: deque[str], mods: list[list[str]]) -> Response: raise NotImplementedError + @abstractmethod def command_go(self, args: deque[str]) -> Response: raise NotImplementedError + @abstractmethod def command_top(self, args: deque[str], mods: list[list[str]]) -> Response: raise NotImplementedError - def command_up(self, args: deque[str]) -> Response: + @abstractmethod + def command_up(self, args: deque[str], mods: list[list[str]]) -> Response: raise NotImplementedError def command_set(self, args: deque[str]) -> Response: @@ -174,7 +223,11 @@ def command_set(self, args: deque[str]) -> Response: return AlertResponse.error(f'Unknown argument for "set": {command}.') return AlertResponse.success(f'The "set {command}" was successfully modified.') - def mod_filter(self, data: Iterable[str], args: list[str]) -> Generator[str | FabricException, None, None]: + def mod_filter( + self, + data: Iterator[str] | Generator[str | Exception, None, None], + args: list[str] + ) -> Generator[str | Exception, None, None]: if not data or len(args) != 1: yield FabricException('Incorrect arguments for "filter".') try: @@ -188,12 +241,19 @@ def mod_filter(self, data: Iterable[str], args: list[str]) -> Generator[str | Fa yield head else: yield '\n' - for line in filter(lambda x: regexp.search(x), data): - yield line.strip() + for elem in data: + if type(elem) is str and regexp.search(elem): + yield elem.strip() + elif type(elem) is Exception: + yield elem except StopIteration: - yield FabricException + yield FabricException() - def mod_save(self, data: Iterable[str], args: list[str]) -> Generator[str | FabricException, None, None]: + def mod_save( + self, + data: Iterator[str] | Generator[str | Exception, None, None], + args: list[str] + ) -> Generator[str | Exception, None, None]: # terminating modificator if len(args) == 1 and data: destination = args[0] @@ -215,11 +275,15 @@ def mod_save(self, data: Iterable[str], args: list[str]) -> Generator[str | Fabr except FileNotFoundError: yield FabricException(f'No such file or directory for "save": {place_to_save}.') except StopIteration: - yield FabricException + yield FabricException() else: yield FabricException('Incorrect arguments for "save".') - def mod_count(self, data: Iterable[str], args: list[str]) -> Generator[str | FabricException, None, None]: + def mod_count( + self, + data: Iterator[str] | Generator[str | Exception, None, None], + args: list[str] + ) -> Generator[str | Exception, None, None]: # terminating modificator if args: raise FabricException('Incorrect arguments for "count".') @@ -234,12 +298,13 @@ def mod_count(self, data: Iterable[str], args: list[str]) -> Generator[str | Fab yield '\n' yield f'Count: {counter}.' except StopIteration: - yield FabricException + yield FabricException() def on_enter(self, value: str) -> Response: + self.add_input_to_log(value) try: - args = reduce( - lambda acc, x: acc[:-1] + [acc[-1] + [x]] if x != '|' else acc + [[]], + args = reduce( # type: ignore + lambda acc, x: acc[:-1] + [acc[-1] + [x]] if x != '|' else acc + [[]], # type: ignore shlex.split(value), [[]] ) @@ -252,7 +317,7 @@ def on_enter(self, value: str) -> Response: elif command in self.keywords['top']: return self.command_top(head, args[1:]) elif command in self.keywords['up']: - return self.command_up(head) + return self.command_up(head, args[1:]) elif command in self.keywords['set']: return self.command_set(head) else: @@ -264,9 +329,11 @@ def on_enter(self, value: str) -> Response: except NotImplementedError: return AlertResponse.error(f'The method for "{command}" is not implemented yet.') + @abstractmethod def update_virtual_cursor(self, value: str) -> Generator[str, None, None]: raise NotImplementedError + @abstractmethod def get_virtual_from(self, value: str) -> str: raise NotImplementedError diff --git a/thymus/contexts/ios.py b/thymus/contexts/ios.py index 7a345db..807d1e9 100644 --- a/thymus/contexts/ios.py +++ b/thymus/contexts/ios.py @@ -1,48 +1,46 @@ from __future__ import annotations -from typing import TYPE_CHECKING +import sys +import re + +from typing import TYPE_CHECKING, Optional from collections import deque from itertools import chain from difflib import Differ from copy import copy +if sys.version_info.major == 3 and sys.version_info.minor >= 9: + from collections.abc import Generator, Iterable, Iterator +else: + from typing import Generator, Iterable, Iterator + +from thymus_ast.ios import ( # type: ignore + Root, + Node, + construct_tree, + analyze_heuristics, + lazy_provide_config, + search_node, + search_h_node, +) + from .context import ( Context, FabricException, UP_LIMIT, ) from ..responses import ( + Response, ContextResponse, AlertResponse, ) -from ..parsers.ios import ( - construct_tree, - analyze_heuristics, - lazy_provide_config, - search_node, - search_h_node, -) from ..lexers import IOSLexer from ..misc import find_common -import sys -import re - if TYPE_CHECKING: - from typing import Optional - if sys.version_info.major == 3 and sys.version_info.minor >= 9: - from collections.abc import Generator, Iterable - else: - from typing import Generator, Iterable from logging import Logger - from ..responses import Response - from ..parsers.ios import ( - Root, - Node, - ) - class IOSContext(Context): __slots__ = ( @@ -56,7 +54,7 @@ class IOSContext(Context): '__is_promisc', ) __store: list[IOSContext] = [] - lexer: IOSLexer = IOSLexer + lexer: type[IOSLexer] = IOSLexer @property def prompt(self) -> str: @@ -77,18 +75,6 @@ def nos_type(self) -> str: def heuristics(self) -> bool: return self.__is_heuristics - @property - def base_heuristics(self) -> bool: - return self.__is_base_heuristics - - @property - def crop(self) -> bool: - return self.__is_crop - - @property - def promisc(self) -> bool: - return self.__is_promisc - @heuristics.setter def heuristics(self, value: str | int | bool) -> None: self_name = self.__class__.__name__ @@ -119,6 +105,10 @@ def heuristics(self, value: str | int | bool) -> None: else: raise TypeError(f'Incorrect type for heuristics: {type(value)}.') + @property + def base_heuristics(self) -> bool: + return self.__is_base_heuristics + @base_heuristics.setter def base_heuristics(self, value: str | int | bool) -> None: self_name = self.__class__.__name__ @@ -147,6 +137,10 @@ def base_heuristics(self, value: str | int | bool) -> None: if hasattr(self, f'_{self_name}__tree') and self.__tree: self.__rebuild_tree() + @property + def crop(self) -> bool: + return self.__is_crop + @crop.setter def crop(self, value: str | int | bool) -> None: self_name = self.__class__.__name__ @@ -173,6 +167,10 @@ def crop(self, value: str | int | bool) -> None: if hasattr(self, f'_{self_name}__tree') and self.__tree and self.__is_heuristics: self.__rebuild_tree() + @property + def promisc(self) -> bool: + return self.__is_promisc + @promisc.setter def promisc(self, value: str | int | bool) -> None: if type(value) is bool: @@ -244,9 +242,9 @@ def __rebuild_tree(self) -> None: self.logger.debug(f'The tree was rebuilt. {self.nos_type}.') def __get_node_content(self, node: Root | Node) -> Generator[str, None, None]: - return lazy_provide_config(self.content, node, self.spaces) + return lazy_provide_config(self.content, node, alignment=self.spaces, is_started=True) - def __prepand_nop(self, data: Iterable[str]) -> Generator[str, None, None]: + def __prepand_nop(self, data: Iterable[str]) -> Generator[str | Exception, None, None]: ''' This method simply adds a blank line to a head of the stream. If the stream is not lazy, it also converts it. The blank line is then eaten by __process_fabric method or __mod methods. Final stream does not contain it. @@ -254,22 +252,20 @@ def __prepand_nop(self, data: Iterable[str]) -> Generator[str, None, None]: yield '\n' yield from data - def __inspect_children( - self, - node: Root | Node, - parent_path: str, - *, - is_pair: bool = False - ) -> Generator[str | tuple[str, Node], None, None]: + def __inspect_children_pair(self, node: Root | Node, parent_path: str) -> Generator[tuple[str, Node], None, None]: for child in node.children: if child.is_accessible: path = child.path.replace(parent_path, '').replace(self.delimiter, ' ').strip() - if is_pair: - yield path, child - else: - yield path + yield path, child else: - yield from self.__inspect_children(child, parent_path, is_pair=is_pair) + yield from self.__inspect_children_pair(child, parent_path) + + def __inspect_children_path(self, node: Root | Node, parent_path: str) -> Generator[str, None, None]: + for child in node.children: + if child.is_accessible: + yield child.path.replace(parent_path, '').replace(self.delimiter, ' ').strip() + else: + yield from self.__inspect_children_path(child, parent_path) def __update_virtual_cursor(self, parts: deque[str], *, is_heuristics: bool = False) -> Generator[str, None, None]: # is_heuristics here is a marker that sports which cursor and its nodes to use @@ -327,6 +323,20 @@ def update_virtual_cursor(self, value: str) -> Generator[str, None, None]: command = sub_command else: return + elif command in self.keywords['up']: + if len(parts) < 3: + return + sub_command = parts[1] # sub_command must be show or go + if sub_command in self.keywords['show'] or sub_command in self.keywords['go']: + temp = self.__cursor + self.command_up(deque(), []) + self.__virtual_cursor = self.__cursor + self.__virtual_h_cursor = self.__cursor + self.__cursor = temp + offset = 2 + command = sub_command + else: + return elif command in self.keywords['show'] or command in self.keywords['go']: if len(parts) < 2: return @@ -352,10 +362,12 @@ def get_virtual_from(self, value: str) -> str: if not value: return '' parts: list[str] = value.split() + first = '' command = parts[0] - if command in self.keywords['top']: + if command in self.keywords['top'] or command in self.keywords['up']: if len(parts) < 3: return '' + first = command parts = parts[2:] elif command in self.keywords['show'] or command in self.keywords['go']: if len(parts) < 2: @@ -367,13 +379,18 @@ def get_virtual_from(self, value: str) -> str: current_path = self.__cursor.path.replace(self.delimiter, ' ') virtual_path = self.__virtual_cursor.path.replace(self.delimiter, ' ') hvirtual_path = self.__virtual_h_cursor.path.replace(self.delimiter, ' ') + temp = self.__cursor + if first == 'up': + self.command_up(deque(), []) + current_path = self.__cursor.path.replace(self.delimiter, ' ') if current_path: # shorten the virtual paths virtual_path = virtual_path.replace(current_path, '', 1) hvirtual_path = hvirtual_path.replace(current_path, '', 1) + self.__cursor = temp virtual_path = virtual_path.strip().lower() hvirtual_path = hvirtual_path.strip().lower() - # here we need to find out which the virtual path have more in common with the input + # here we need to find out which virtual path has more in common with the input first = find_common([virtual_path, input]) second = find_common([hvirtual_path, input]) if len(first) == len(second) or len(first) > len(second): @@ -381,26 +398,26 @@ def get_virtual_from(self, value: str) -> str: else: return input.replace(second, '', 1) - def mod_stubs(self, jump_node: Optional[Node] = None) -> Generator[str | FabricException, None, None]: + def mod_stubs(self, jump_node: Optional[Node] = None) -> Generator[str | Exception, None, None]: node = self.__cursor if not jump_node else jump_node if not node.stubs: yield FabricException('No stubs at this level.') yield '\n' # nop yield from node.stubs - def mod_sections(self, jump_node: Optional[Node] = None) -> Generator[str | FabricException, None, None]: + def mod_sections(self, jump_node: Optional[Node] = None) -> Generator[str | Exception, None, None]: node = self.__cursor if not jump_node else jump_node if not node.children: yield FabricException('No sections at this level.') yield '\n' - yield from self.__inspect_children(node, node.path) + yield from self.__inspect_children_path(node, node.path) def mod_wildcard( self, - data: Iterable[str], + data: Iterator[str] | Generator[str | Exception, None, None], args: list[str], jump_node: Optional[Node] = None - ) -> Generator[str | FabricException, None, None]: + ) -> Generator[str | Exception, None, None]: if not data or len(args) != 1: yield FabricException('Incorrect arguments for "wildcard".') try: @@ -417,18 +434,18 @@ def mod_wildcard( if not node.children: yield FabricException('No sections at this level.') yield '\n' - for path, child in self.__inspect_children(node, node.path, is_pair=True): + for path, child in self.__inspect_children_pair(node, node.path): self.logger.debug(f'{path} {child.name}') if re.search(regexp, path): yield from self.__get_node_content(child) except StopIteration: - yield FabricException + yield FabricException() def mod_diff( self, args: list[str], jump_node: Optional[Node] = None - ) -> Generator[str | FabricException, None, None]: + ) -> Generator[str | Exception, None, None]: if len(args) != 1: yield FabricException('There must be one argument for "diff".') if not self.name: @@ -438,13 +455,14 @@ def mod_diff( context_name = args[0] if self.name == context_name: yield FabricException('You can\'t compare the same context.') - remote_context: Context = None + remote_context: Optional[Context] = None for elem in self.__store: if elem.name == context_name and type(elem) is type(self): remote_context = elem break else: yield FabricException('Remote context has not been found.') + assert remote_context is not None # mypy's satisfier target: Root | Node = None peer: Root | Node = None if jump_node: @@ -470,7 +488,7 @@ def mod_contains( self, args: list[str], jump_node: Optional[Node] = [] - ) -> Generator[str | FabricException, None, None]: + ) -> Generator[str | Exception, None, None]: def replace_path(source: str, head: str) -> str: return source.replace(head, '').replace(self.delimiter, ' ').strip() @@ -513,38 +531,38 @@ def __check_leading_mod(name: str, position: int, args_count: int, args_limit: i raise FabricException(f'Incorrect position of "{name}".') if args_count != args_limit: raise FabricException(f'Incorrect number of arguments for "{name}". Must be {args_limit}.') - data = self.__prepand_nop(data) + recol_data = self.__prepand_nop(data) try: for number, elem in enumerate(mods): command = elem[0] if command in self.keywords['filter']: - data = self.mod_filter(data, elem[1:]) + recol_data = self.mod_filter(recol_data, elem[1:]) elif command in self.keywords['stubs']: __check_leading_mod(command, number, len(elem[1:])) - data = self.mod_stubs(jump_node) + recol_data = self.mod_stubs(jump_node) elif command in self.keywords['sections']: __check_leading_mod(command, number, len(elem[1:])) - data = self.mod_sections(jump_node) + recol_data = self.mod_sections(jump_node) elif command in self.keywords['save']: - data = self.mod_save(data, elem[1:]) + recol_data = self.mod_save(recol_data, elem[1:]) break elif command in self.keywords['count']: - data = self.mod_count(data, elem[1:]) + recol_data = self.mod_count(recol_data, elem[1:]) break elif command in self.keywords['wildcard']: - data = self.mod_wildcard(data, elem[1:], jump_node) + recol_data = self.mod_wildcard(recol_data, elem[1:], jump_node) elif command in self.keywords['diff']: __check_leading_mod(command, number, len(elem[1:]), 1) - data = self.mod_diff(elem[1:], jump_node) + recol_data = self.mod_diff(elem[1:], jump_node) elif command in self.keywords['contains']: __check_leading_mod(command, number, len(elem[1:]), 1) - data = self.mod_contains(elem[1:], jump_node) + recol_data = self.mod_contains(elem[1:], jump_node) else: raise FabricException(f'Unknown modificator "{command}".') - head = next(data) + head = next(recol_data) if isinstance(head, Exception): raise head - return ContextResponse.success(data) + return ContextResponse.success(recol_data) except (AttributeError, IndexError) as err: return AlertResponse.error(f'Unknown error from the fabric #001: {err}') except FabricException as err: @@ -552,7 +570,7 @@ def __check_leading_mod(name: str, position: int, args_count: int, args_limit: i except StopIteration: return AlertResponse.error('Unknown error from the fabric #002.') - def command_show(self, args: deque[str] = [], mods: list[list[str]] = []) -> Response: + def command_show(self, args: deque[str], mods: list[list[str]]) -> Response: if args: first_arg = args[0] if first_arg in self.keywords['version']: @@ -587,7 +605,7 @@ def command_go(self, args: deque[str]) -> Response: return AlertResponse.success() return AlertResponse.error('This path is not correct.') - def command_top(self, args: deque[str] = [], mods: list[list[str]] = []) -> Response: + def command_top(self, args: deque[str], mods: list[list[str]]) -> Response: if args: sub_command = args.popleft() temp = self.__cursor @@ -609,21 +627,21 @@ def command_top(self, args: deque[str] = [], mods: list[list[str]] = []) -> Resp self.__cursor = self.__tree return AlertResponse.success() - def command_up(self, args: deque[str]) -> Response: + def command_up(self, args: deque[str], mods: list[list[str]]) -> Response: steps: int = 1 if args: - if len(args) != 1: - return AlertResponse.error('There must be one argument for "up".') arg = args.popleft() if arg in self.keywords['show']: if self.__cursor.name == 'root': return AlertResponse.error('You can\'t do a negative lookahead from the top.') temp = self.__cursor - self.__cursor = self.__cursor.parent - result = self.command_show() + self.command_up(deque(), []) + result = self.command_show(args, mods) self.__cursor = temp return result elif arg.isdigit(): + if len(args) != 1: + return AlertResponse.error('There must be one argument for "up".') steps = min(int(arg), UP_LIMIT) else: return AlertResponse.error(f'Incorrect argument for "up": {arg}.') diff --git a/thymus/contexts/junos.py b/thymus/contexts/junos.py index 57187e6..ffe6322 100644 --- a/thymus/contexts/junos.py +++ b/thymus/contexts/junos.py @@ -1,18 +1,19 @@ from __future__ import annotations -from typing import TYPE_CHECKING +import sys +import re + +from typing import TYPE_CHECKING, Optional from collections import deque -from .context import ( - Context, - FabricException, - UP_LIMIT, -) -from ..responses import ( - ContextResponse, - AlertResponse, -) -from ..parsers.junos import ( +if sys.version_info.major == 3 and sys.version_info.minor >= 9: + from collections.abc import Generator, Iterable, Iterator +else: + from typing import Generator, Iterable, Iterator + +from thymus_ast.junos import ( # type: ignore + Root, + Node, construct_tree, lazy_parser, lazy_provide_config, @@ -23,26 +24,24 @@ draw_inactive_tree, draw_diff_tree, ) -from ..lexers import JunosLexer -import sys -import re +from .context import ( + Context, + FabricException, + UP_LIMIT, +) +from ..responses import ( + Response, + ContextResponse, + AlertResponse, +) +from ..lexers import JunosLexer +from ..misc import find_common if TYPE_CHECKING: - from typing import Optional - if sys.version_info.major == 3 and sys.version_info.minor >= 9: - from collections.abc import Generator, Iterable - else: - from typing import Generator, Iterable from logging import Logger - from ..responses import Response - from ..parsers.junos import ( - Root, - Node, - ) - class JunOSContext(Context): __slots__: tuple[str, ...] = ( @@ -51,7 +50,7 @@ class JunOSContext(Context): '__virtual_cursor', ) __store: list[JunOSContext] = [] - lexer: JunosLexer = JunosLexer + lexer: type[JunosLexer] = JunosLexer @property def prompt(self) -> str: @@ -111,7 +110,7 @@ def get_heads(node: Root | Node, comp: str) -> Generator[str, None, None]: if name.startswith('inactive: '): name = name.replace('inactive: ', '') if name.startswith(comp): - yield name + yield child['name'] if not parts or not self.__virtual_cursor['children']: return @@ -144,7 +143,7 @@ def get_heads(node: Root | Node, comp: str) -> Generator[str, None, None]: # showing all sections that names start with the head yield from get_heads(self.__virtual_cursor, head) - def __prepand_nop(self, data: Iterable[str]) -> Generator[str, None, None]: + def __prepand_nop(self, data: Iterable[str]) -> Generator[str | Exception, None, None]: ''' This method simply adds a blank line to a head of the stream. If the stream is not lazy, it also converts it. The blank line is then eaten by __process_fabric method or __mod methods. Final stream does not contain it. @@ -164,22 +163,29 @@ def update_virtual_cursor(self, value: str) -> Generator[str, None, None]: if len(parts) > 2 and (parts[1] in self.keywords['show'] or parts[1] in self.keywords['go']): self.__virtual_cursor = self.__tree yield from self.__update_virtual_cursor(deque(parts[2:])) + elif parts[0] in self.keywords['up']: + if len(parts) > 2 and (parts[1] in self.keywords['show'] or parts[1] in self.keywords['go']): + if self.__cursor['name'] != 'root': + self.__virtual_cursor = self.__cursor['parent'] + yield from self.__update_virtual_cursor(deque(parts[2:])) elif parts[0] in self.keywords['show'] or parts[0] in self.keywords['go']: if len(parts) != 1: self.__virtual_cursor = self.__cursor yield from self.__update_virtual_cursor(deque(parts[1:])) def get_virtual_from(self, value: str) -> str: - # little bit hacky here value = value.lower() + # little bit hacky here if m := re.search(r'([-a-z0-9\/]+)\.(\d+)', value, re.I): value = value.replace(f'{m.group(1)}.{m.group(2)}', f'{m.group(1)} unit {m.group(2)}') parts = value.split() if len(parts) < 2: return '' - if parts[0] == 'top': + first = '' + if parts[0] == 'top' or parts[0] == 'up': if len(parts) < 3: return '' + first = parts[0] parts = parts[1:] if parts[0] not in self.keywords['show'] and parts[0] not in self.keywords['go']: return '' @@ -188,15 +194,39 @@ def get_virtual_from(self, value: str) -> str: return new_value path = self.__virtual_cursor['path'] path = path.replace('inactive: ', '') - if self.__cursor['name'] != 'root': + rpath = self.__cursor['path'] if self.__cursor['name'] != 'root' else '' + if not first and self.__cursor['name'] != 'root': path = path.replace(self.__cursor['path'], '', 1) - path = path.replace(self.delimiter, ' ') - path = path.strip().lower() - if new_value.startswith(path): - return new_value.replace(path, '', 1) + spath = path.replace(self.delimiter, ' ') + spath = spath.strip().lower() + if first == 'up': + if path.startswith(rpath): + # when a user tries to do `up show x y` + # and the user is in `x` already + xparts = spath.split() + for index, xpart in enumerate(xparts): + if new_value.startswith(xpart): + break + return new_value.replace(' '.join(xparts[index:]), '', 1) + else: + common = find_common([path, rpath]) + common = common.replace(self.delimiter, ' ') + common = common.strip() + xpath = path.replace(common, '', 1) + xpath = xpath.replace(self.delimiter, ' ') + xpath = xpath.strip() + if new_value.startswith(xpath): + return new_value.replace(xpath, '', 1) + else: + if new_value.startswith(spath): + return new_value.replace(spath, '', 1) return new_value - def mod_wildcard(self, data: Iterable[str], args: list[str]) -> Generator[str | FabricException, None, None]: + def mod_wildcard( + self, + data: Iterator[str] | Generator[str | Exception, None, None], + args: list[str] + ) -> Generator[str | Exception, None, None]: # passthrough modificator if not data or len(args) != 1: yield FabricException('Incorrect arguments for "wildcard".') @@ -213,13 +243,13 @@ def mod_wildcard(self, data: Iterable[str], args: list[str]) -> Generator[str | yield '\n' yield from lazy_wc_parser(data, '', args[0], self.delimiter) except StopIteration: - yield FabricException + yield FabricException() def mod_diff( self, args: list[str], jump_node: Optional[Node] = None - ) -> Generator[str | FabricException, None, None]: + ) -> Generator[str | Exception, None, None]: if len(args) != 1: yield FabricException('There must be one argument for "diff".') if not self.name: @@ -229,13 +259,14 @@ def mod_diff( context_name = args[0] if self.name == context_name: yield FabricException('You can\'t compare the same context.') - remote_context: JunOSContext = None + remote_context: Optional[JunOSContext] = None for elem in self.__store: if elem.name == context_name: remote_context = elem break else: yield FabricException('Remote context has not been found.') + assert remote_context is not None # mypy's satisfier target: Root | Node = None peer: Root | Node = None if jump_node: @@ -257,20 +288,20 @@ def mod_diff( yield '\n' yield from draw_diff_tree(tree, tree['name']) - def mod_inactive(self, jump_node: Optional[Node] = []) -> Generator[str | FabricException, None, None]: + def mod_inactive(self, jump_node: Optional[Node] = []) -> Generator[str | Exception, None, None]: node = self.__cursor if not jump_node else jump_node tree = search_inactives(node) yield '\n' yield from draw_inactive_tree(tree, tree['name']) - def mod_stubs(self, jump_node: Optional[Node] = []) -> Generator[str | FabricException, None, None]: + def mod_stubs(self, jump_node: Optional[Node] = []) -> Generator[str | Exception, None, None]: node = self.__cursor if not jump_node else jump_node if not node['stubs']: yield FabricException('No stubs at this level.') yield '\n' yield from node['stubs'] - def mod_sections(self, jump_node: Optional[Node] = []) -> Generator[str | FabricException, None, None]: + def mod_sections(self, jump_node: Optional[Node] = []) -> Generator[str | Exception, None, None]: node = self.__cursor if not jump_node else jump_node if not node['children']: yield FabricException('No sections at this level.') @@ -281,7 +312,7 @@ def mod_contains( self, args: list[str], jump_node: Optional[Node] = [] - ) -> Generator[str | FabricException, None, None]: + ) -> Generator[str | Exception, None, None]: def replace_path(source: str, path: str) -> str: return source.replace(path, '').replace(self.delimiter, ' ').strip() @@ -311,7 +342,8 @@ def __process_fabric( data: Iterable[str], mods: list[list[str]], *, - jump_node: Optional[Node] = None + jump_node: Optional[Node] = None, + banned: list[str] = [] ) -> Response: def __check_leading_mod(name: str, position: int, args_count: int, args_limit: int = 0) -> None: @@ -321,46 +353,48 @@ def __check_leading_mod(name: str, position: int, args_count: int, args_limit: i raise FabricException(f'Incorrect number of arguments for "{name}". Must be {args_limit}.') is_flat_out: bool = True - data = self.__prepand_nop(data) + recol_data = self.__prepand_nop(data) try: for number, elem in enumerate(mods): command = elem[0] + if command in banned: + raise FabricException(f'You cannot use the "{command}" with this main command.') if command in self.keywords['filter']: - data = self.mod_filter(data, elem[1:]) + recol_data = self.mod_filter(recol_data, elem[1:]) elif command in self.keywords['wildcard']: - data = self.mod_wildcard(data, elem[1:]) + recol_data = self.mod_wildcard(recol_data, elem[1:]) is_flat_out = False elif command in self.keywords['save']: - data = lazy_provide_config(data, block=' ' * self.spaces) - data = self.mod_save(data, elem[1:]) + recol_data = lazy_provide_config(recol_data, block=' ' * self.spaces) + recol_data = self.mod_save(recol_data, elem[1:]) break elif command in self.keywords['count']: - data = self.mod_count(data, elem[1:]) + recol_data = self.mod_count(recol_data, elem[1:]) break elif command in self.keywords['diff']: __check_leading_mod(command, number, len(elem[1:]), 1) - data = self.mod_diff(elem[1:], jump_node) + recol_data = self.mod_diff(elem[1:], jump_node) elif command in self.keywords['inactive']: __check_leading_mod(command, number, len(elem[1:])) - data = self.mod_inactive(jump_node) + recol_data = self.mod_inactive(jump_node) is_flat_out = False elif command in self.keywords['stubs']: __check_leading_mod(command, number, len(elem[1:])) - data = self.mod_stubs(jump_node) + recol_data = self.mod_stubs(jump_node) elif command in self.keywords['sections']: __check_leading_mod(command, number, len(elem[1:])) - data = self.mod_sections(jump_node) + recol_data = self.mod_sections(jump_node) elif command in self.keywords['contains']: __check_leading_mod(command, number, len(elem[1:]), 1) - data = self.mod_contains(elem[1:], jump_node) + recol_data = self.mod_contains(elem[1:], jump_node) else: raise FabricException(f'Unknown modificator "{command}".') - head: str | FabricException = next(data) + head = next(recol_data) if isinstance(head, Exception): raise head if is_flat_out: - return ContextResponse.success(map(lambda x: x.strip(), data)) - return ContextResponse.success(lazy_provide_config(data, block=' ' * self.spaces)) + return ContextResponse.success(map(lambda x: x.strip() if type(x) is str else x, recol_data)) + return ContextResponse.success(lazy_provide_config(recol_data, block=' ' * self.spaces)) except FabricException as err: return AlertResponse.error(f'{err}') except (AttributeError, IndexError) as err: @@ -368,7 +402,7 @@ def __check_leading_mod(name: str, position: int, args_count: int, args_limit: i except StopIteration: return AlertResponse.error('Unknown error from the fabric #002.') - def command_show(self, args: deque[str] = [], mods: list[list[str]] = []) -> Response: + def command_show(self, args: deque[str], mods: list[list[str]]) -> Response: if args: first_arg = args[0] if first_arg in self.keywords['version']: @@ -430,21 +464,21 @@ def command_top(self, args: deque[str], mods: list[list[str]]) -> Response: self.__cursor = self.__tree return AlertResponse.success() - def command_up(self, args: deque[str]) -> Response: + def command_up(self, args: deque[str], mods: list[list[str]]) -> Response: steps: int = 1 if args: - if len(args) != 1: - return AlertResponse.error('There must be one argument for "up".') arg = args.popleft() if arg in self.keywords['show']: if self.__cursor['name'] == 'root': return AlertResponse.error('You can\'t do a negative lookahead from the top.') temp = self.__cursor self.__cursor = self.__cursor['parent'] - result = self.command_show() + result = self.command_show(args, mods) self.__cursor = temp return result elif arg.isdigit(): + if len(args) != 1: + return AlertResponse.error('There must be one argument for "up".') steps = min(int(arg), UP_LIMIT) else: return AlertResponse.error(f'Incorrect argument for "up": {arg}.') diff --git a/thymus/lexers/__init__.py b/thymus/lexers/__init__.py index ab69043..f6d09b3 100644 --- a/thymus/lexers/__init__.py +++ b/thymus/lexers/__init__.py @@ -8,10 +8,11 @@ ) -from .common.regexps import ( +from .common import ( IPV4_REGEXP, IPV6_REGEXP, + CommonLexer, + SyslogLexer ) -from .common.common import CommonLexer, SyslogLexer -from .junos.junos import JunosLexer -from .ios.ios import IOSLexer +from .junos import JunosLexer +from .ios import IOSLexer diff --git a/thymus/lexers/common/__init__.py b/thymus/lexers/common/__init__.py new file mode 100644 index 0000000..a0bd26e --- /dev/null +++ b/thymus/lexers/common/__init__.py @@ -0,0 +1,16 @@ +__all__ = ( + 'IPV4_REGEXP', + 'IPV6_REGEXP', + 'CommonLexer', + 'SyslogLexer', +) + + +from .regexps import ( + IPV4_REGEXP, + IPV6_REGEXP, +) +from .common import ( + CommonLexer, + SyslogLexer, +) diff --git a/thymus/lexers/common/common.py b/thymus/lexers/common/common.py index a6dad72..42c2a90 100644 --- a/thymus/lexers/common/common.py +++ b/thymus/lexers/common/common.py @@ -1,34 +1,22 @@ from __future__ import annotations from re import MULTILINE, IGNORECASE -from typing import TYPE_CHECKING - -import sys - -from pygments.lexer import RegexLexer, bygroups -from pygments.token import ( +from typing import Any +from pygments.lexer import RegexLexer, bygroups # type: ignore +from pygments.token import ( # type: ignore Generic, Whitespace, Keyword, Comment, + _TokenType, ) -if TYPE_CHECKING: - if sys.version_info.major == 3 and sys.version_info.minor >= 9: - from collections.abc import Callable - else: - from typing import Callable - from typing import Optional, Any - - from pygments.token import _TokenType - - def wr( reg_exp: str, *args: tuple[_TokenType, ...], - **kwargs: dict[str, str] -) -> tuple[str, Callable[[tuple[Any, ...]], Any], Optional[str]]: + **kwargs: Any +) -> tuple[Any, ...]: if 'stage' in kwargs and kwargs['stage']: if len(args) > 1: return reg_exp, bygroups(*args), kwargs['stage'] diff --git a/thymus/lexers/ios/__init__.py b/thymus/lexers/ios/__init__.py new file mode 100644 index 0000000..8b86388 --- /dev/null +++ b/thymus/lexers/ios/__init__.py @@ -0,0 +1,5 @@ +__all__ = ( + 'IOSLexer', +) + +from .ios import IOSLexer diff --git a/thymus/lexers/ios/ios.py b/thymus/lexers/ios/ios.py index ea2dd06..b9d2ab7 100644 --- a/thymus/lexers/ios/ios.py +++ b/thymus/lexers/ios/ios.py @@ -1,7 +1,7 @@ from __future__ import annotations -from pygments.lexer import RegexLexer -from pygments.token import ( +from pygments.lexer import RegexLexer # type: ignore +from pygments.token import ( # type: ignore Text, Whitespace, Comment, diff --git a/thymus/lexers/junos/__init__.py b/thymus/lexers/junos/__init__.py new file mode 100644 index 0000000..59a4f30 --- /dev/null +++ b/thymus/lexers/junos/__init__.py @@ -0,0 +1,5 @@ +__all__ = ( + 'JunosLexer', +) + +from .junos import JunosLexer diff --git a/thymus/lexers/junos/junos.py b/thymus/lexers/junos/junos.py index 5c1ca19..a62dc5c 100644 --- a/thymus/lexers/junos/junos.py +++ b/thymus/lexers/junos/junos.py @@ -2,8 +2,8 @@ from ..common.regexps import IPV4_REGEXP, IPV6_REGEXP -from pygments.lexer import RegexLexer, bygroups -from pygments.token import ( +from pygments.lexer import RegexLexer, bygroups # type: ignore +from pygments.token import ( # type: ignore Text, Generic, Whitespace, diff --git a/thymus/parsers/ios/__init__.py b/thymus/parsers/ios/__init__.py deleted file mode 100644 index 52ae558..0000000 --- a/thymus/parsers/ios/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -__all__ = ( - 'construct_tree', - 'analyze_heuristics', - 'lazy_provide_config', - 'search_node', - 'search_h_node', - 'Root', - 'Node', -) - -__version__ = '0.1' - -from .ios import ( - construct_tree, - analyze_heuristics, - lazy_provide_config, - search_node, - search_h_node, - Root, - Node, -) diff --git a/thymus/parsers/ios/ios.py b/thymus/parsers/ios/ios.py deleted file mode 100644 index 174a474..0000000 --- a/thymus/parsers/ios/ios.py +++ /dev/null @@ -1,365 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass -from typing import TYPE_CHECKING -from collections import deque - -import sys -import re - -if TYPE_CHECKING: - if sys.version_info.major == 3 and sys.version_info.minor >= 9: - from collections.abc import Generator, Callable - else: - from typing import Generator, Callable - from typing import Optional, Any - -STOP_LIST = ( - 'exit-address-family', - 'end', -) -SA_REGEXP = r'^route-map\s|^interface\s' -SA_SECTIONS = ( - 'route-map', - 'interface', -) - - -@dataclass -class Root: - name: str - version: str - delimiter: str - path: str - children: list[Node] - heuristics: list[Node] - stubs: list[str] - begin: int - end: int - is_accessible: bool - -@dataclass -class Node: - name: str - path: str - parent: Node | Root - children: list[Node] - heuristics: list[Node] - stubs: list[str] - begin: int - end: int - is_accessible: bool - - -def read_config(filename: str, encoding='utf-8-sig') -> list[str]: - try: - with open(filename, encoding=encoding) as f: - return f.readlines() - except FileNotFoundError: - return [] - -def get_spaces(line: str) -> int: - if m := re.search(r'^(\s+)', line): - return len(m.group(1)) - return 0 - -def check_child(name: str, children: list[Node]) -> Optional[Node]: - for child in children: - if child.name == name: - return child - -def make_nodes(path: str, parent: Root | Node, delimiter: str) -> Node: - parts = path.split() - current: Root | Node = parent - for number, elem in enumerate(parts): - child = check_child(elem, current.children) - if not child: - xpath = f'{delimiter}'.join(parts[:number + 1]) - if parent.name != 'root': - xpath = f'{parent.path}{delimiter}{xpath}' - new_node = Node(elem, xpath, current, [], [], [], 0, 0, False) - current.children.append(new_node) - current = new_node - else: - current = child - current.is_accessible = True - return current - -def step_back(node: Node, steps: int) -> Node: - current: Node | Root = node - reverse: list[Node] = [] - while True: - if current.name == 'root': - break - current = current.parent - if current.is_accessible: - reverse.append(current) - if len(reverse) == 1 and reverse[0].name == 'root': - return reverse[0] - if len(reverse) <= steps: - return reverse[-1] - offset = (steps + 1) * -1 - return reverse[offset] - -def chop_tree(node: Node) -> None: - marked: list[int] = [] - for number, child in enumerate(node.heuristics): - if len(child.stubs) <= 1: - marked.append(number) - for number in reversed(marked): - del node.heuristics[number] - -def heuristics_parse(node: Node, delimiter: str, is_crop: bool) -> None: - if not node.stubs: - return - for stub in node.stubs: - parts: list[str] = [] - if stub.startswith('no '): - parts = stub[3:].split() - elif re.match(r'^\d+\s', stub): - parts = re.sub(r'^\d+\s', '', stub).split() - else: - parts = stub.split() - current = node - # a little bit the same as the make_nodes() - # but I don't want to mix them together for clarity purposes - for number, elem in enumerate(parts): - child = check_child(elem, current.heuristics) - if not child: - xpath = f'{delimiter}'.join(parts[:number + 1]) - if node.name != 'root': - xpath = f'{node.path}{delimiter}{xpath}' - new_node = Node(elem, xpath, current, [], [], [], 0, 0, False) - if is_crop: - new_node.stubs.append(' '.join(parts[number + 1:])) - else: - new_node.stubs.append(stub) - current.heuristics.append(new_node) - current = new_node - else: - if is_crop: - child.stubs.append(' '.join(parts[number + 1:])) - else: - child.stubs.append(stub) - current = child - chop_tree(node) - -def recursive_node_lookup( - node: Root | Node, - is_child: bool, - callback: Callable[[Node, Any], None], - **kwargs: Any -) -> None: - target: list[Node] = node.children if is_child else node.heuristics - for child in target: - inner_target: list[Node] = child.children if is_child else child.heuristics - if inner_target: - recursive_node_lookup(child, is_child, callback, **kwargs) - callback(child, **kwargs) - -def lazy_provide_config(config: list[str], node: Root | Node, alignment: int) -> Generator[str, None, None]: - if not node.is_accessible: - return - try: - begin: int = 0 - end: int = node.end + 1 - if node.name != 'root': - begin = node.begin - 1 - if config[end - 1].strip() != '!': - end -= 1 - depth: int = 0 - prev_spaces: int = 0 - is_started: bool = False - for pos in range(begin, end): - if not config[pos]: - continue - elif config[pos] == '\n': - yield config[pos] - continue - spaces = get_spaces(config[pos]) - if not is_started: - if spaces > 0: - yield config[pos].strip() - elif spaces == 0: - is_started = True - yield config[pos].strip() - else: - if spaces > prev_spaces: - depth += 1 - prev_spaces = spaces - yield f'{" " * alignment * depth}{config[pos].strip()}' - elif spaces == prev_spaces: - yield f'{" " * alignment * depth}{config[pos].strip()}' - else: - if spaces > 0: - depth -= 1 - yield f'{" " * alignment * depth}{config[pos].strip()}' - prev_spaces = spaces - else: - depth = 0 - prev_spaces = 0 - yield config[pos].strip() - except IndexError: - return - -def search_node(path: deque[str], node: Root | Node) -> Optional[Node]: - ''' - This function searches for a node based on the path argument. - It also eats the path from its head. - ''' - step = path.popleft() - for child in node.children: - if child.name.lower() == step.lower(): - if not path: - if child.is_accessible: - return child - else: - return - return search_node(path, child) - -def search_h_node(path: deque[str], node: Root | Node) -> Optional[Node]: - ''' - This function searches for a heuristic node based on the path argument. - It also eats the path from its head. - ''' - step = path.popleft() - for child in node.heuristics: - if child.name.lower() == step.lower(): - if not path: - return child - return search_h_node(path, child) - -def analyze_heuristics(root: Root, delimiter: str, is_crop: bool) -> None: - ''' - This function analyzes all stubs lists from the root down to the bottom and aggregates common parts - to new sections inside heuristics list. - `is_crop` allows a user to save only the unique parts of a stub string. - ''' - heuristics_parse(root, delimiter=delimiter, is_crop=is_crop) - recursive_node_lookup(root, is_child=False, callback=chop_tree) - recursive_node_lookup(root, is_child=True, callback=heuristics_parse, delimiter=delimiter, is_crop=is_crop) - -def analyze_sections(root: Root, delimiter: str, cache: list[tuple[int, str]]) -> None: - - def __get_begin_end(children: list[Node]) -> tuple[int, int]: - begin: int = -1 - end: int = -1 - for child in children: - if child.is_accessible: - if begin == -1: - begin = child.begin - else: - begin = min(begin, child.begin) - end = max(end, child.end) - else: - x, y = __get_begin_end(child.children) - if begin == -1: - begin = x - else: - begin = min(begin, x) - end = max(end, y) - return begin, end - - if root.name != 'root': - return - for number, line in cache: - node = make_nodes(line, root, delimiter) - node.begin = number - node.end = number - for child in root.children: - if child.name in SA_SECTIONS: - begin, end = __get_begin_end(child.children) - child.begin = begin - child.end = end - child.is_accessible = True - if child.name == 'route-map': - for rm in child.children: - begin, end = __get_begin_end(rm.children) - rm.begin = begin - rm.end = end - rm.is_accessible = True - -def construct_tree( - config: list[str], - *, - delimiter: str = '^', - is_heuristics: bool = False, - is_base_heuristics: bool = False, - is_crop: bool = False, - is_promisc: bool = False -) -> Optional[Root]: - current: Root | Node = Root( - name='root', - version='', - delimiter=delimiter, - path='', - children=[], - heuristics=[], - stubs=[], - begin=0, - end=0, - is_accessible=True - ) - prev_line: str = '' - step: int = 0 # step tells how deep the next section is - final: int = 0 - if not is_promisc: - for index in range(len(config) - 1, 0, -1): - if config[index] == '\n': - continue - elif config[index].strip() == 'end': - break - else: - return - config.append('!\n') # for the cases when the last section is not properly closed - s_cache: list[tuple[int, str]] = [] - # LOOKAHEAD ALGO - for number, line in enumerate(config): - final = number - line = line.rstrip() - if not line: - continue - if not prev_line: - prev_line = line - continue - prev_spaces = get_spaces(prev_line) - spaces = get_spaces(line) - if spaces > prev_spaces: - step = spaces - prev_spaces - current = make_nodes(prev_line.strip(), current, delimiter) - current.begin = number - current.depth = spaces - prev_spaces - elif spaces < prev_spaces: - if not step: - continue - stripped = prev_line.strip() - if not stripped.startswith('!') and stripped not in STOP_LIST: - current.stubs.append(stripped) - current.end = number - temp = current - current = step_back(current, (prev_spaces - spaces) // step) - if current.name == 'root': - last_end: int = temp.end - while temp.name != 'root': - if temp.is_accessible and not temp.end: - temp.end = last_end - temp = temp.parent - else: - stripped = prev_line.strip() - if not stripped.startswith('!') and stripped not in STOP_LIST: - current.stubs.append(stripped) - if current.name == 'root' and is_base_heuristics and re.search(SA_REGEXP, stripped): - s_cache.append((number, stripped)) - current.stubs = current.stubs[:-1] - if current.name == 'root' and stripped.startswith('version '): - if len((parts := stripped.split())) == 2: - current.version = parts[1] - prev_line = line - current.end = final - if current.name != 'root': - return - if is_heuristics: - analyze_heuristics(current, delimiter, is_crop) - if is_base_heuristics: - analyze_sections(current, delimiter, s_cache) - return current diff --git a/thymus/parsers/junos/__init__.py b/thymus/parsers/junos/__init__.py deleted file mode 100644 index 10bbee9..0000000 --- a/thymus/parsers/junos/__init__.py +++ /dev/null @@ -1,39 +0,0 @@ -__all__ = ( - 'parser', - 'lazy_parser', - 'lazy_provide_config', - 'lazy_wc_parser', - 'wc_parser', - 'provide_config', - 'construct_path', - 'construct_tree', - 'search_node', - 'compare_nodes', - 'draw_diff_tree', - 'search_inactives', - 'draw_inactive_tree', - 'make_path', - 'Root', - 'Node', -) - -__version__ = '0.1' - -from .junos import ( - parser, - lazy_parser, - lazy_provide_config, - lazy_wc_parser, - wc_parser, - provide_config, - construct_path, - construct_tree, - search_node, - compare_nodes, - draw_diff_tree, - search_inactives, - draw_inactive_tree, - make_path, - Root, - Node, -) diff --git a/thymus/parsers/junos/junos.py b/thymus/parsers/junos/junos.py deleted file mode 100644 index 41ff209..0000000 --- a/thymus/parsers/junos/junos.py +++ /dev/null @@ -1,337 +0,0 @@ -from __future__ import annotations - -import sys -import re - -from typing import TypedDict, Optional -from collections import deque -from copy import copy - -if sys.version_info.major == 3 and sys.version_info.minor >= 9: - from collections.abc import Generator, Iterable -else: - from typing import Generator, Iterable - - -class Root(TypedDict): - name: str - version: str - children: list[Node] - stubs: list[str] - delimiter: str - -class Node(TypedDict): - name: str - parent: Node | Root - children: list[Node] - stubs: list[str] - path: str - is_closed: bool - is_inactive: bool - - -def parser(data: list[str], path: str, *, delimiter='^', is_greedy=False) -> tuple[list[str], list[str]]: - sections: list[str] = [] - params: list[str] = [] - container: list[str] = [] - parts: list[str] = path.split(delimiter) if path else [] - plen = len(parts) - start: int = 0 - end: int = 0 - for number, line in enumerate(data): - stripped = line.strip() - if '{' in stripped and '}' not in stripped and ';' not in stripped: - sections.append(stripped) - elif '}' in stripped and '{' not in stripped and ';' not in stripped: - if parts == [x[:-2] for x in sections]: - if container: - container.append('}') - end = number - if is_greedy: - del data[start:end + 1] - return container, params - sections.pop() - elif ';' in stripped and '{' not in stripped and '}' not in stripped: - if parts == [x[:-2] for x in sections]: - params.append(stripped) - if parts == [x[:-2] for x in sections[:plen]]: - if stripped and not ('{' in stripped and '}' in stripped): - if not start: - start = number - container.append(stripped) - return container, params - -def lazy_parser(data: Iterable[str], path: str, delimiter='^') -> Generator[str, None, None]: - sections: list[str] = [] - parts: list[str] = path.split(delimiter) if path else [] - plen = len(parts) - for line in data: - stripped = line.strip() - if '{' in stripped and '}' not in stripped and ';' not in stripped: - sections.append(stripped) - elif '}' in stripped and '{' not in stripped and ';' not in stripped: - if parts == [x[:-2] for x in sections]: - return - sections.pop() - if parts == [x[:-2] for x in sections[:plen]]: - if stripped and not ('{' in stripped and '}' in stripped): - yield stripped - -def wc_parser( - data: list[str], - path: str, - pattern: str, - delimiter='^' -) -> tuple[dict[str, list[str]], dict[str, list[str]]]: - sections: list[str] = [] - container: dict[str, list[str]] = {} - params: dict[str, list[str]] = {} - parts = path.split(delimiter) if path else [] - plen = len(parts) - for line in data: - stripped = line.strip() - if '{' in stripped and '}' not in stripped and ';' not in stripped: - sections.append(stripped) - if parts == [x[:-2] for x in sections[:-1]]: - if re.match(pattern, sections[-1], re.I): - key = sections[-1][:-2] - container[key] = [] - params[key] = [] - elif '}' in stripped and '{' not in stripped and ';' not in stripped: - if parts == [x[:-2] for x in sections[:-1]]: - if re.match(pattern, sections[-1], re.I): - key = sections[-1][:-2] - if r := container.get(key): - r.append('}') - return container, params - sections.pop() - elif ';' in stripped and '{' not in stripped and '}' not in stripped: - if len(sections) == plen + 1 and \ - parts == [x[:-2] for x in sections[:plen]] and \ - re.match(pattern, sections[plen], re.I): - key = sections[plen][:-2] - params[key].append(stripped) - if stripped and parts == [x[:-2] for x in sections[:plen]]: - if len(sections) > plen and re.match(pattern, sections[plen], re.I): - container[sections[plen][:-2]].append(stripped) - return container, params - -def lazy_wc_parser(data: Iterable[str], path: str, pattern: str, delimiter='^') -> Generator[str, None, None]: - sections: list[str] = [] - parts: list[str] = path.split(delimiter) if path else [] - plen = len(parts) - for line in data: - stripped = line.strip() - if '{' in stripped and '}' not in stripped and ';' not in stripped: - sections.append(stripped) - elif '}' in stripped and '{' not in stripped and ';' not in stripped: - if parts == [x[:-2] for x in sections[:-1]]: - if re.match(pattern, sections[-1][:-2], re.S): - yield '}' - sections.pop() - if stripped and parts == [x[:-2] for x in sections[:plen]]: - if len(sections) > plen and re.match(pattern, sections[plen][:-2], re.S): - yield stripped - -def provide_config(data: Iterable[str], block=' ' * 2) -> str: - depth = 0 - result = '' - flag = False - for line in data: - stripped = line.strip() - if '{' in stripped and ';' not in stripped: - depth += 1 - flag = True - elif '}' in stripped and ';' not in stripped: - if depth: - depth -= 1 - prepend = block * depth - if flag: - prepend = block * (depth - 1) - flag = False - result += f'{prepend}{stripped}\n' - return result - -def lazy_provide_config(data: Iterable[str], block=' ' * 2) -> Generator[str, None, None]: - depth = 0 - flag = False - for line in data: - stripped = line.strip() - if '{' in stripped and '}' not in stripped and ';' not in stripped: - depth += 1 - flag = True - elif '}' in stripped and '{' not in stripped and ';' not in stripped: - if depth: - depth -= 1 - prepend = block * depth - if flag: - prepend = block * (depth - 1) - flag = False - yield f'{prepend}{stripped}' - -def construct_path(node: Node, delimiter='^') -> str: - name = node.get('name') - if not name: - raise Exception('This node is without a name.') - while True: - parent = node.get('parent') - if not parent: - break - extra = construct_path(parent, delimiter) - return f'{extra}{delimiter}{name}' - return name - -def construct_tree(data: list[str], delimiter='^') -> Root: - ''' - This function goes through the config file and constructs the tree every leaf of which is a config section. - ''' - root = Root(name='root', version='', children=[], stubs=[], delimiter=delimiter) - current_node = root - section_regexp = r'^[^{]+{(?:\s##\s[^\n]+)?$' - for line in data: - stripped = line.strip() - if '{' in stripped and '}' not in stripped and ';' not in stripped: - if not re.match(section_regexp, stripped, re.I) and not re.search(r'is not defined$', stripped): - raise Exception('Incorrect configuration format detected.') - section_name = stripped[:-2] # skip ' {' in the end of the line - node = Node( - name=section_name, - parent=current_node, - children=[], - stubs=[], - is_closed=False, - is_inactive=False - ) - node['path'] = construct_path(node, delimiter).replace(f'root{delimiter}', '') - if section_name.startswith('inactive:'): - node['is_inactive'] = True - current_node['children'].append(node) - current_node = node - elif '}' in stripped and '{' not in stripped and ';' not in stripped: - current_node['is_closed'] = True - current_node = current_node['parent'] - elif ';' in stripped and '{' not in stripped and '}' not in stripped: - current_node['stubs'].append(stripped) - if stripped.startswith('version ') and current_node['name'] == 'root': - parts = stripped.split() - if len(parts) >= 2 and parts[1][-1] == ';': - root['version'] = parts[1][:-1] # skip ';' in the end of version - return root - -def search_node(path: deque[str], node: Node) -> Optional[Node]: - step = path.popleft() - step = step.lower() - if '.' in step and node['name'] == 'interfaces': - try: - ifd, ifl = step.split('.') - except ValueError: - return - if not ifl.isdigit(): - return - step = ifd - path.appendleft(f'unit {ifl}') - children = node.get('children', []) - if not children: - return - for child in children: - name = child['name'] - name = name.lower() - if name.startswith('inactive: '): - name = name.replace('inactive: ', '') - if name == step: - if not path: - return child - return search_node(path, child) - else: - if not path: - return - extra_step = path.popleft() - path.appendleft(f'{step} {extra_step}') - return search_node(path, node) - -def compare_nodes(target: Root | Node, peer: Root | Node) -> Root | Node: - - def copy_node(type: str, origin: Root | Node, parent: Root | Node) -> Node: - node: Node = copy(origin) - node['type'] = type - node['parent'] = parent - node['children'] = [] - return node - if target['name'] != peer['name']: - raise Exception('Nodes are not on the same level.') - new_target = copy(target) - new_target['children'] = [] - new_target['type'] = 'common' - peers_children = copy(peer['children']) - for child in target['children']: - for peer_child in peer['children']: - if child['name'] == peer_child['name']: - peers_children.remove(peer_child) - if next_node := compare_nodes(child, peer_child): - next_node['parent'] = new_target - new_target['children'].append(next_node) - break - else: - copied_child = copy_node('new', child, new_target) - new_target['children'].append(copied_child) - for child in peers_children: - copied_child = copy_node('lost', child, new_target) - new_target['children'].append(copied_child) - ts = set(target['stubs']) - ps = set(peer['stubs']) - new_target['diff'] = [] - new_target['diff'].extend([('+', x) for x in ts - ps]) - new_target['diff'].extend([('-', x) for x in ps - ts]) - if not new_target['diff'] and not new_target['children']: - return {} - return new_target - -def search_inactives(tree: Root | Node) -> Root | Node: - new_tree = copy(tree) - new_tree['children'] = [] - for child in tree['children']: - if node := search_inactives(child): - new_tree['children'].append(node) - node['parent'] = new_tree - inactives = [x for x in filter(lambda x: x.startswith('inactive:'), tree['stubs'])] - if tree['name'] != 'root' and not tree['is_inactive'] and not inactives: - if new_tree['children']: - return new_tree - return {} - new_tree['inactives'] = inactives - return new_tree - -def draw_diff_tree(tree: Root | Node, start: str) -> Generator[str, None, None]: - if tree['name'] != start: - if tree['type'] in ('new', 'lost'): - sign = '+' if tree['type'] == 'new' else '-' - yield f'{sign} {tree["name"]} {{' - yield '...' - else: - yield f'{tree["name"]} {{' - for child in tree['children']: - for x in draw_diff_tree(child, start): - yield x - if 'diff' in tree and tree['diff']: - for x, y in tree['diff']: - yield f'{x} {y}' - if tree['type'] in ('new', 'lost'): - sign = '+' if tree['type'] == 'new' else '-' - yield f'{sign} }}' - else: - if tree['name'] != start: - yield '}' - -def draw_inactive_tree(tree: Root | Node, start: str) -> Generator[str, None, None]: - if tree['name'] != start: - yield f'{tree["name"]} {{' - for child in tree['children']: - for x in draw_inactive_tree(child, start): - yield x - for stub in tree.get('inactives', []): - yield stub - if tree['name'] != start: - yield '}' - -def make_path(line_path: str) -> deque[str]: - return deque(line_path.split()) diff --git a/thymus/responses/responses.py b/thymus/responses/responses.py index f9a0711..68703e6 100644 --- a/thymus/responses/responses.py +++ b/thymus/responses/responses.py @@ -1,15 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING - -import sys - - -if TYPE_CHECKING: - if sys.version_info.major == 3 and sys.version_info.minor >= 9: - from collections.abc import Iterable - else: - from typing import Iterable +from typing import Any class Response: @@ -19,7 +10,7 @@ class Response: ) rtype: str = 'system' - def __init__(self, status: str, value: str | Iterable[str]) -> None: + def __init__(self, status: str, value: Any) -> None: self.__status: str = status if type(value) is str: self.__value = iter([value]) @@ -37,15 +28,15 @@ def is_ok(self) -> bool: return True if self.__status == 'success' else False @property - def value(self) -> Iterable[str]: + def value(self) -> Any: return self.__value @classmethod - def error(cls, value: str | Iterable[str]) -> Response: + def error(cls, value: Any) -> Response: return cls('error', value) @classmethod - def success(cls, value: str | Iterable[str] = '') -> Response: + def success(cls, value: Any = '') -> Response: return cls('success', value) class SettingsResponse(Response): @@ -59,6 +50,3 @@ class ContextResponse(Response): class RichResponse(Response): rtype: str = 'rich' - - def __init__(self, value: str | Iterable[str]) -> None: - super().__init__('success', value) diff --git a/thymus/styles/main.css b/thymus/styles/main.css new file mode 100644 index 0000000..9a0573e --- /dev/null +++ b/thymus/styles/main.css @@ -0,0 +1,190 @@ +#main-welcome-out { + content-align: center middle; + height: 1fr; +} + +/* OPEN DIALOG DATA */ + +.od-disabled { + display: none; +} + +.od-labels { + width: 15; +} + +.od-hor-con { + height: auto; +} + +.od-inputs { + width: 35; +} + +.od-inputs.-valid { + border: tall $success 60%; +} +.od-inputs.-valid:focus { + border: tall $success; +} + +#od-right-middle-block { + height: auto; + margin-bottom: 1; +} + +#od-tabs { + dock: top; +} + +#od-hor-con-butns { + margin-top: 1; +} + +#od-cap { + margin-bottom: 1; +} + +#od-left-block { + dock: left; + width: 30; + height: 100%; + border-right: #0f2b30; +} + +#od-directory-tree { + scrollbar-gutter: stable; +} + +#od-main-in { + dock: bottom; + } + +#od-error-caption { + margin-left: 1; + color: red; +} + +#od-top-container { + height: auto; + dock: top; +} + +#od-open-button { + margin-left: 1; +} + +#od-refresh-button { + margin-left: 1; +} + +#od-nt-loading { + margin-left: 1; + content-align: left middle; +} + +/* QUIT MODAL DATA */ + +QuitApp { + align: center middle; + background: $primary 30%; +} + +QuitScreen { + align: center middle; + background: $primary 30%; +} + +#qs-dialog { + grid-size: 2; + grid-gutter: 1 2; + grid-rows: 1fr 3; + padding: 0 1; + width: 60; + height: 11; + border: thick $background 80%; + background: $surface; +} + +#qs-question { + column-span: 2; + height: 1fr; + width: 1fr; + content-align: center middle; +} + +#qs-quit { + width: 100%; +} + +#qs-cancel { + width: 100%; +} + +/* CONTEXT LIST SCREEN DATA */ +ContextListScreen { + align: center middle; + background: $primary 30%; +} + +#cm-options { + width: 70%; + height: 80%; +} + +/* ERROR SCREEN DATA */ +ErrorScreen { + align: center middle; + background: $primary 30%; +} + +#es-dialog { + grid-size: 2; + grid-gutter: 1 2; + grid-rows: 1fr 3; + padding: 0 1; + width: 60; + height: 11; + border: thick $background 80%; + background: $surface; +} + +#es-err-msg { + column-span: 2; + height: 1fr; + width: 1fr; + content-align: center middle; +} + +#es-quit { + width: 100%; + content-align: center middle; +} + +/* WS DATA */ + +#ws-main-in { + border: none; + padding-top: 1; + padding-bottom: 1; +} + +#ws-path-line { + background: $secondary-background-lighten-3; +} + +#ws-status-bar { + background: $accent; +} + +#ws-left-sidebar { + dock: left; + width: 30; + height: 100%; + border-right: #0f2b30; +} + +#ws-right-bottom-controls { + height: auto; + dock: bottom; +} diff --git a/thymus/tui/__init__.py b/thymus/tui/__init__.py new file mode 100644 index 0000000..78a515f --- /dev/null +++ b/thymus/tui/__init__.py @@ -0,0 +1,7 @@ +__all__ = ( + 'OpenDialog', +) + +from .open_dialog import ( + OpenDialog, +) diff --git a/thymus/tui/modals/__init__.py b/thymus/tui/modals/__init__.py new file mode 100644 index 0000000..e5bac92 --- /dev/null +++ b/thymus/tui/modals/__init__.py @@ -0,0 +1,21 @@ +__all__ = ( + 'ContextListScreen', + 'ErrorScreen', + 'LogsScreen', + 'QuitScreen', + 'QuitApp', +) + +from .contexts_modal import ( + ContextListScreen, +) +from .error_modal import ( + ErrorScreen, +) +from .logs_modal import ( + LogsScreen, +) +from .quit_modal import ( + QuitScreen, + QuitApp, +) diff --git a/thymus/tui/modals/contexts_modal.py b/thymus/tui/modals/contexts_modal.py index bc63014..6f8449d 100644 --- a/thymus/tui/modals/contexts_modal.py +++ b/thymus/tui/modals/contexts_modal.py @@ -11,7 +11,7 @@ from textual.app import ComposeResult from ...tuier import TThymus - from ..working_screen.working_screen import WorkingScreen + from ..working_screen import WorkingScreen class ContextListScreen(ModalScreen): @@ -33,8 +33,9 @@ def on_show(self) -> None: header = control.get_option_at_index(0) header.disabled = True for screen_name in self.app.working_screens: - screen: WorkingScreen = self.app.get_screen(screen_name) + screen: WorkingScreen = self.app.get_screen(screen_name) # type: ignore if hasattr(screen, 'filename') and hasattr(screen, 'nos_type'): + assert screen.context if screen.context.name: control.add_option(Option(f'{screen.nos_type.upper()}: {screen.context.name}', id=screen.name)) else: @@ -42,6 +43,7 @@ def on_show(self) -> None: def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None: screen = event.option.id + assert screen self.app.pop_screen() # pops itself if len(self.app.screen_stack) == 1: self.app.push_screen(screen) diff --git a/thymus/tui/modals/error_modal.py b/thymus/tui/modals/error_modal.py index 0684988..53f50de 100644 --- a/thymus/tui/modals/error_modal.py +++ b/thymus/tui/modals/error_modal.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from textual import on from textual.containers import Grid @@ -9,8 +9,6 @@ if TYPE_CHECKING: - from typing import Optional - from textual.app import ComposeResult diff --git a/thymus/tui/modals/logs_modal.py b/thymus/tui/modals/logs_modal.py index ed22f41..aef41e6 100644 --- a/thymus/tui/modals/logs_modal.py +++ b/thymus/tui/modals/logs_modal.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING from logging.handlers import BufferingHandler -from pygments.token import ( +from pygments.token import ( # type: ignore Whitespace, Keyword, Generic, @@ -64,6 +64,8 @@ def on_show(self) -> None: theme = ANSISyntaxTheme(SYSLOG_DARK_STYLES) if not self.app.dark: theme = ANSISyntaxTheme(SYSLOG_LIGHT_STYLES) + if not self.app.logger: + return for handler in self.app.logger.handlers: if type(handler) is not BufferingHandler: continue diff --git a/thymus/tui/modals/quit_modal.py b/thymus/tui/modals/quit_modal.py index e5f52dc..01e387c 100644 --- a/thymus/tui/modals/quit_modal.py +++ b/thymus/tui/modals/quit_modal.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from textual import on from textual.containers import Grid @@ -9,12 +9,10 @@ if TYPE_CHECKING: - from typing import Optional - from textual.app import ComposeResult from ...tuier import TThymus - from ..working_screen.working_screen import WorkingScreen + from ..working_screen import WorkingScreen class QuitApp(ModalScreen): diff --git a/thymus/tui/net_loader.py b/thymus/tui/net_loader.py index 7eac88f..3e47392 100644 --- a/thymus/tui/net_loader.py +++ b/thymus/tui/net_loader.py @@ -1,6 +1,6 @@ from __future__ import annotations -from netmiko import ConnectHandler +from netmiko import ConnectHandler # type: ignore P_MAP = { diff --git a/thymus/tui/open_dialog.py b/thymus/tui/open_dialog.py index e06e98c..7a4a16d 100644 --- a/thymus/tui/open_dialog.py +++ b/thymus/tui/open_dialog.py @@ -28,8 +28,8 @@ LoadingIndicator, ) -from .working_screen.working_screen import WorkingScreen -from .modals.error_modal import ErrorScreen +from .working_screen import WorkingScreen +from .modals import ErrorScreen from .net_loader import NetLoader @@ -142,10 +142,13 @@ def open_from_network(self) -> None: self.lock = False def compose(self) -> ComposeResult: + platform_index = 0 + if platform := self.app.settings.globals.get('open_dialog_platform', ''): + platform_index = self.app.settings.platforms.index(str(platform)) with Horizontal(id='od-main-container'): with Vertical(id='od-left-block'): yield Static('Select platform:') - with ListView(id='od-nos-switch'): + with ListView(id='od-nos-switch', initial_index=platform_index): yield ListItem(Label('Juniper JunOS', name='junos')) yield ListItem(Label('Cisco IOS', name='ios')) yield ListItem(Label('Cisco NX-OS', name='nxos')) @@ -263,6 +266,10 @@ def on_radio_set_changed(self, event: RadioSet.Changed) -> None: if not control.value or control.value == '22' or not control.value.isdigit(): control.value = '23' + def on_list_view_highlighted(self, event: ListView.Highlighted) -> None: + if event.item and event.item.children: + self.app.settings.process_command(f'global set open_dialog_platform {event.item.children[0].name}') + def action_focus(self, target: str) -> None: if target == 'platform': self.query_one('#od-nos-switch').focus() diff --git a/thymus/tui/working_screen/__init__.py b/thymus/tui/working_screen/__init__.py new file mode 100644 index 0000000..857274a --- /dev/null +++ b/thymus/tui/working_screen/__init__.py @@ -0,0 +1,7 @@ +__all__ = ( + 'WorkingScreen', +) + +from .working_screen import ( + WorkingScreen, +) diff --git a/thymus/tui/working_screen/extended_input.py b/thymus/tui/working_screen/extended_input.py index bb13da6..3c7922a 100644 --- a/thymus/tui/working_screen/extended_input.py +++ b/thymus/tui/working_screen/extended_input.py @@ -26,7 +26,7 @@ def action_submit(self) -> None: out = self.app.settings.process_command(self.value) self.screen.draw(out) elif self.value.strip() == 'help': - out = self.screen.print_help() + self.screen.print_help() elif out := self.screen.context.on_enter(self.value): self.screen.draw(out) self.screen.query_one('#ws-sections-list', LeftSidebar).clear() @@ -39,6 +39,7 @@ async def on_input_changed(self, message: Input.Changed) -> None: sidebar = self.screen.query_one('#ws-sections-list', LeftSidebar) if self.value: param = message.value + # pipe here is for the auto-filling of the left sidebar if message.value[-1] == ' ': param += '|' sidebar.update(param) @@ -75,4 +76,12 @@ def _on_key(self, event: Key) -> None: sidebar.action_cursor_down() else: textlog.action_scroll_down() + elif event.key == 'ctrl+up': + if self.screen.context and (prev_cmd := self.screen.context.get_input_from_log()): + self.value = prev_cmd + self.cursor_position = len(self.value) + elif event.key == 'ctrl+down': + if self.screen.context and (next_cmd := self.screen.context.get_input_from_log(forward=False)): + self.value = next_cmd + self.cursor_position = len(self.value) super()._on_key(event) diff --git a/thymus/tui/working_screen/working_screen.py b/thymus/tui/working_screen/working_screen.py index c690dcd..f8e2c33 100644 --- a/thymus/tui/working_screen/working_screen.py +++ b/thymus/tui/working_screen/working_screen.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from itertools import islice from textual.screen import Screen @@ -18,6 +18,7 @@ from rich.syntax import Syntax from ...contexts import ( + Context, JunOSContext, IOSContext, EOSContext, @@ -28,22 +29,18 @@ from .status_bar import StatusBar from .path_bar import PathBar from .left_sidebar import LeftSidebar -from ..modals.quit_modal import QuitScreen -from ...responses import RichResponse +from ..modals import QuitScreen +from ...responses import Response, RichResponse from ... import CONTEXT_HELP if TYPE_CHECKING: - from typing import Optional - from textual.app import ComposeResult from ...tuier import TThymus - from ...contexts import Context - from ...responses import Response -PLATFORMS: dict[str, Context] = { +PLATFORMS: dict[str, type[Context]] = { 'junos': JunOSContext, 'ios': IOSContext, 'eos': EOSContext, @@ -62,7 +59,7 @@ class WorkingScreen(Screen): filename: var[str] = var('') nos_type: var[str] = var('') encoding: var[str] = var('') - context: var[Context] = var(None) + context: var[Optional[Context]] = var(None) draw_data: var[Optional[Response]] = var(None) def __init__( @@ -75,33 +72,36 @@ def __init__( **kwargs ) -> None: super().__init__(*args, **kwargs) - if nos_type not in PLATFORMS: - err_msg = f'Unsupported platform: {nos_type}.' - self.app.logger.error(err_msg) - raise Exception(err_msg, 'logged') self.nos_type = nos_type self.encoding = encoding if filename: self.filename = filename try: content = open(filename, encoding=encoding, errors='replace').readlines() + if not content: + err_msg = f'File "{self.filename}" is empty. Platform: {nos_type}.' + self.app.logger.error(err_msg) + raise Exception(err_msg, 'logged') except FileNotFoundError: err_msg = f'Cannot open the file "{filename}", it does not exist. Platform: {nos_type}.' self.app.logger.error(err_msg) raise Exception(err_msg, 'logged') - else: + elif self.screen.name: self.filename = self.screen.name - if not content: - err_msg = f'File "{self.filename}" is empty. Platform: {nos_type}.' + else: + self.filename = 'unset' + if context := PLATFORMS.get(nos_type, None): + self.context = context( + name='', + content=content, + encoding=encoding, + settings=getattr(self.app.settings, nos_type), + logger=self.app.logger + ) + if not self.context: + err_msg = f'Failed to load context for "{self.filename}".' self.app.logger.error(err_msg) raise Exception(err_msg, 'logged') - self.context = PLATFORMS[nos_type]( - name='', - content=content, - encoding=encoding, - settings=getattr(self.app.settings, nos_type), - logger=self.app.logger - ) self.app.logger.info(f'File "{self.filename}" for the platform "{nos_type}" was opened.') def compose(self) -> ComposeResult: @@ -169,8 +169,6 @@ def draw(self, data: Optional[Response] = None) -> None: self.__draw(multiplier) def print_help(self) -> None: - if not self.context: - return try: body: list[str] = [] body.append(CONTEXT_HELP['header'].format(NOS=self.nos_type.upper())) @@ -182,10 +180,10 @@ def print_help(self) -> None: if k in self.context.keywords and self.context.keywords[k]: body.append(v.format(CMDS=', '.join(self.context.keywords[k]))) body.append(CONTEXT_HELP['footer']) - r = RichResponse(body) + r = RichResponse.success(body) self.draw(r) except Exception as err: - self.app.logger.debug(f'Error has occurred with print_help: {err}.') + self.app.logger.error(f'Error has occurred with print_help: {err}.') def action_request_quit(self) -> None: self.app.logger.debug(f'Exit was requested: {self.filename}.') diff --git a/thymus/tuier.py b/thymus/tuier.py index a21084e..7e93be3 100644 --- a/thymus/tuier.py +++ b/thymus/tuier.py @@ -1,8 +1,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional -from textual.app import App +from textual.app import App, ComposeResult from textual.widgets import ( Footer, Static, @@ -17,21 +17,22 @@ SCREENS_SAVES_DIR, ) from .app_settings import AppSettings -from .tui.open_dialog import OpenDialog -from .tui.modals.quit_modal import QuitApp -from .tui.modals.contexts_modal import ContextListScreen -from .tui.modals.logs_modal import LogsScreen +from .tui import OpenDialog +from .tui.modals import ( + QuitApp, + ContextListScreen, + LogsScreen, +) if TYPE_CHECKING: - from textual.app import ComposeResult from textual.events import Resize import logging class TThymus(App): - CSS_PATH = 'tui/styles/main.css' + CSS_PATH = 'styles/main.css' SCREENS = { 'open_file': OpenDialog() } @@ -45,9 +46,9 @@ class TThymus(App): ] working_screens: var[list[str]] = var([]) settings: var[AppSettings] = var(AppSettings()) - logger: var[logging.Logger] = var(None) + logger: var[Optional[logging.Logger]] = var(None) is_logo_downscaled: var[bool] = var(False) - logo: var[Static] = var(None) + logo: var[Optional[Static]] = var(None) def __scale_logo(self, is_down: bool) -> None: try: