diff --git a/Pipfile b/Pipfile index 38b352d2..3d2cffcf 100644 --- a/Pipfile +++ b/Pipfile @@ -24,13 +24,14 @@ graphlib-backport = ">=1.0.3" jinja2 = ">=3.0.1" munch = ">=2.4.0" pyyaml = ">=5.4.1" -rapyuta-io = ">=1.10.0" +rapyuta-io = ">=1.11.1" tabulate = ">=0.8.0" pyrfc3339 = ">=1.1" directory-tree = ">=0.0.3.1" yaspin = ">=2.3.0" jsonschema = ">=4.0.0" waiting = ">=1.4.1" +semver = ">=3.0.0" [requires] python_version = "3" diff --git a/Pipfile.lock b/Pipfile.lock index ce274f17..9014e58f 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "d2b508e7ec711785728167bd84bb29cebb7ea4d5cf5bb56ba7e67b20b8cf41dc" + "sha256": "f989df10a0b0659c30e617e0f2aa66d29c4d8cc3b9fe38d112373b7d14abe665" }, "pipfile-spec": 6, "requires": { @@ -34,100 +34,100 @@ }, "certifi": { "hashes": [ - "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7", - "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716" + "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082", + "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9" ], "markers": "python_version >= '3.6'", - "version": "==2023.5.7" + "version": "==2023.7.22" }, "charset-normalizer": { "hashes": [ - "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6", - "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1", - "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e", - "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373", - "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62", - "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230", - "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be", - "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c", - "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0", - "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448", - "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f", - "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649", - "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d", - "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0", - "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706", - "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a", - "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59", - "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23", - "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5", - "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb", - "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e", - "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e", - "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c", - "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28", - "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d", - "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41", - "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974", - "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce", - "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f", - "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1", - "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d", - "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8", - "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017", - "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31", - "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7", - "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8", - "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e", - "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14", - "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd", - "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d", - "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795", - "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b", - "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b", - "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b", - "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203", - "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f", - "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19", - "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1", - "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a", - "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac", - "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9", - "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0", - "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137", - "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f", - "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6", - "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5", - "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909", - "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f", - "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0", - "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324", - "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755", - "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb", - "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854", - "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c", - "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60", - "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84", - "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0", - "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b", - "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1", - "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531", - "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1", - "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11", - "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326", - "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df", - "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab" + "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96", + "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c", + "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710", + "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706", + "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020", + "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252", + "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad", + "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329", + "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a", + "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f", + "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6", + "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4", + "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a", + "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46", + "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2", + "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23", + "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace", + "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd", + "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982", + "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10", + "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2", + "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea", + "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09", + "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5", + "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149", + "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489", + "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9", + "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80", + "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592", + "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3", + "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6", + "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed", + "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c", + "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200", + "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a", + "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e", + "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d", + "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6", + "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623", + "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669", + "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3", + "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa", + "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9", + "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2", + "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f", + "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1", + "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4", + "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a", + "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8", + "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3", + "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029", + "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f", + "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959", + "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22", + "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7", + "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952", + "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346", + "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e", + "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d", + "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299", + "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd", + "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a", + "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3", + "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037", + "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94", + "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c", + "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858", + "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a", + "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449", + "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c", + "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918", + "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1", + "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c", + "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac", + "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa" ], "markers": "python_full_version >= '3.7.0'", - "version": "==3.1.0" + "version": "==3.2.0" }, "click": { "hashes": [ - "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", - "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" + "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd", + "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5" ], "index": "pypi", - "version": "==8.1.3" + "version": "==8.1.6" }, "click-completion": { "hashes": [ @@ -154,11 +154,11 @@ }, "click-repl": { "hashes": [ - "sha256:94b3fbbc9406a236f176e0506524b2937e4b23b6f4c0c0b2a0a83f8a64e9194b", - "sha256:cd12f68d745bf6151210790540b4cb064c7b13e571bc64b6957d98d120dacfd8" + "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9", + "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812" ], "index": "pypi", - "version": "==0.2.0" + "version": "==0.3.0" }, "click-spinner": { "hashes": [ @@ -218,75 +218,75 @@ }, "jsonschema": { "hashes": [ - "sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d", - "sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6" + "sha256:bc51325b929171791c42ebc1c70b9713eb134d3bb8ebd5474c8b659b15be6d86", + "sha256:c773028c649441ab980015b5b622f4cd5134cf563daaf0235ca4b73cc3734f20" ], "index": "pypi", - "version": "==4.17.3" + "version": "==4.0.0" }, "markupsafe": { "hashes": [ - "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed", - "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc", - "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2", - "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460", - "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7", - "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0", - "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1", - "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa", - "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03", - "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323", - "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65", - "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013", - "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036", - "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f", - "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4", - "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419", - "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2", - "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619", - "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a", - "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a", - "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd", - "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7", - "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666", - "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65", - "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859", - "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625", - "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff", - "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156", - "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd", - "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba", - "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f", - "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1", - "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094", - "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a", - "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513", - "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed", - "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d", - "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3", - "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147", - "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c", - "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603", - "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601", - "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a", - "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1", - "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d", - "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3", - "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54", - "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2", - "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6", - "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58" + "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e", + "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e", + "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431", + "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686", + "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559", + "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc", + "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c", + "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0", + "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4", + "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9", + "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575", + "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba", + "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d", + "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3", + "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00", + "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155", + "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac", + "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52", + "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f", + "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8", + "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b", + "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24", + "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea", + "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198", + "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0", + "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee", + "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be", + "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2", + "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707", + "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6", + "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58", + "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779", + "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636", + "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c", + "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad", + "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee", + "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc", + "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2", + "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48", + "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7", + "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e", + "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b", + "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa", + "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5", + "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e", + "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb", + "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9", + "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57", + "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc", + "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2" ], "markers": "python_version >= '3.7'", - "version": "==2.1.2" + "version": "==2.1.3" }, "munch": { "hashes": [ - "sha256:0e4108418cfea898dcad01ff9569c30ff58f01d6f699331d04364f51623627c0", - "sha256:5284603030c00906d9d64d8108728c004fbeb91fc1c1e4caca342bc48f2a6dfd" + "sha256:542cb151461263216a4e37c3fd9afc425feeaf38aaa3025cd2a981fadb422235", + "sha256:71033c45db9fb677a0b7eb517a4ce70ae09258490e419b0e7f00d1e386ecb1b4" ], "index": "pypi", - "version": "==3.0.0" + "version": "==4.0.0" }, "pretty-traceback": { "hashes": [ @@ -298,11 +298,11 @@ }, "prompt-toolkit": { "hashes": [ - "sha256:23ac5d50538a9a38c8bde05fecb47d0b403ecd0662857a86f886f798563d5b9b", - "sha256:45ea77a2f7c60418850331366c81cf6b5b9cf4c7fd34616f733c5427e6abbb1f" + "sha256:04505ade687dc26dc4284b1ad19a83be2f2afe83e7a828ace0c72f3a1df72aac", + "sha256:9dffbe1d8acf91e3de75f3b544e4842382fc06c6babe903ac9acb74dc6e08d88" ], "markers": "python_full_version >= '3.7.0'", - "version": "==3.0.38" + "version": "==3.0.39" }, "pyrfc3339": { "hashes": [ @@ -362,57 +362,57 @@ }, "pyyaml": { "hashes": [ - "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf", - "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", - "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", - "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", - "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", - "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", - "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", - "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", - "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", - "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", - "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", - "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", - "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782", - "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", - "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", - "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", - "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", - "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", - "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1", - "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", - "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", - "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", - "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", - "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", - "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", - "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d", - "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", - "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", - "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7", - "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", - "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", - "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", - "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358", - "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", - "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", - "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", - "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", - "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f", - "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", - "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" + "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", + "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", + "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", + "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27", + "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", + "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", + "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98", + "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", + "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", + "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867", + "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47", + "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", + "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", + "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3", + "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", + "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", + "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c", + "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", + "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", + "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", + "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", + "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", + "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd", + "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", + "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", + "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", + "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c", + "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", + "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", + "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", + "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", + "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", + "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", + "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", + "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", + "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa", + "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", + "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585", + "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", + "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f" ], "index": "pypi", - "version": "==6.0" + "version": "==6.0.1" }, "rapyuta-io": { "hashes": [ - "sha256:874dbf669983dca0aa1787dca424a219d836cee17f01e4a821c85ae046cf79e6", - "sha256:e849632ff51a8a3baa03f9ab1f0f91a9f32e21960bde9c4a57b68d4a368d90cc" + "sha256:80da414b02cf277c6d2d9a7783eda5e420a321dcff142e2288228d86d5a19815", + "sha256:e769ffb964a5674c93f5c059e1c4e588f1aaeaa0a7aba418c58945ddecab65e5" ], "index": "pypi", - "version": "==1.10.0" + "version": "==1.11.1" }, "rapyuta-io-cli": { "path": ".", @@ -426,13 +426,21 @@ "markers": "python_version >= '3.7'", "version": "==2.31.0" }, + "semver": { + "hashes": [ + "sha256:2a23844ba1647362c7490fe3995a86e097bb590d16f0f32dfc383008f19e4cdf", + "sha256:9ec78c5447883c67b97f98c3b6212796708191d22e4ad30f4570f840171cbce1" + ], + "index": "pypi", + "version": "==3.0.1" + }, "setuptools": { "hashes": [ - "sha256:5df61bf30bb10c6f756eb19e7c9f3b473051f48db77fddbe06ff2ca307df9a6f", - "sha256:62642358adc77ffa87233bc4d2354c4b2682d214048f500964dbe760ccedf102" + "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f", + "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235" ], "markers": "python_version >= '3.7'", - "version": "==67.8.0" + "version": "==68.0.0" }, "shellingham": { "hashes": [ @@ -468,11 +476,11 @@ }, "urllib3": { "hashes": [ - "sha256:61717a1095d7e155cdb737ac7bb2f4324a858a1e2e6466f6d03ff630ca68d3cc", - "sha256:d055c2f9d38dc53c808f6fdc8eab7360b6fdbbde02340ed25cfbcd817c62469e" + "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11", + "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4" ], "markers": "python_version >= '3.7'", - "version": "==2.0.2" + "version": "==2.0.4" }, "waiting": { "hashes": [ @@ -560,100 +568,100 @@ }, "certifi": { "hashes": [ - "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7", - "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716" + "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082", + "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9" ], "markers": "python_version >= '3.6'", - "version": "==2023.5.7" + "version": "==2023.7.22" }, "charset-normalizer": { "hashes": [ - "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6", - "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1", - "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e", - "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373", - "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62", - "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230", - "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be", - "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c", - "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0", - "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448", - "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f", - "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649", - "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d", - "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0", - "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706", - "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a", - "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59", - "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23", - "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5", - "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb", - "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e", - "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e", - "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c", - "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28", - "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d", - "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41", - "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974", - "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce", - "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f", - "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1", - "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d", - "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8", - "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017", - "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31", - "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7", - "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8", - "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e", - "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14", - "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd", - "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d", - "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795", - "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b", - "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b", - "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b", - "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203", - "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f", - "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19", - "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1", - "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a", - "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac", - "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9", - "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0", - "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137", - "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f", - "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6", - "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5", - "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909", - "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f", - "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0", - "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324", - "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755", - "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb", - "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854", - "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c", - "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60", - "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84", - "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0", - "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b", - "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1", - "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531", - "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1", - "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11", - "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326", - "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df", - "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab" + "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96", + "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c", + "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710", + "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706", + "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020", + "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252", + "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad", + "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329", + "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a", + "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f", + "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6", + "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4", + "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a", + "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46", + "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2", + "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23", + "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace", + "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd", + "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982", + "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10", + "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2", + "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea", + "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09", + "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5", + "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149", + "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489", + "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9", + "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80", + "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592", + "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3", + "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6", + "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed", + "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c", + "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200", + "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a", + "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e", + "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d", + "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6", + "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623", + "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669", + "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3", + "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa", + "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9", + "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2", + "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f", + "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1", + "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4", + "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a", + "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8", + "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3", + "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029", + "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f", + "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959", + "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22", + "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7", + "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952", + "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346", + "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e", + "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d", + "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299", + "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd", + "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a", + "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3", + "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037", + "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94", + "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c", + "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858", + "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a", + "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449", + "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c", + "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918", + "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1", + "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c", + "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac", + "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa" ], "markers": "python_full_version >= '3.7.0'", - "version": "==3.1.0" + "version": "==3.2.0" }, "click": { "hashes": [ - "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", - "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" + "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd", + "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5" ], "index": "pypi", - "version": "==8.1.3" + "version": "==8.1.6" }, "colorama": { "hashes": [ @@ -665,10 +673,10 @@ }, "distlib": { "hashes": [ - "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46", - "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e" + "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057", + "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8" ], - "version": "==0.3.6" + "version": "==0.3.7" }, "docutils": { "hashes": [ @@ -712,59 +720,59 @@ }, "markupsafe": { "hashes": [ - "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed", - "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc", - "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2", - "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460", - "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7", - "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0", - "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1", - "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa", - "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03", - "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323", - "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65", - "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013", - "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036", - "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f", - "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4", - "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419", - "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2", - "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619", - "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a", - "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a", - "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd", - "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7", - "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666", - "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65", - "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859", - "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625", - "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff", - "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156", - "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd", - "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba", - "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f", - "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1", - "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094", - "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a", - "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513", - "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed", - "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d", - "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3", - "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147", - "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c", - "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603", - "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601", - "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a", - "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1", - "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d", - "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3", - "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54", - "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2", - "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6", - "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58" + "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e", + "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e", + "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431", + "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686", + "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559", + "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc", + "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c", + "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0", + "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4", + "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9", + "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575", + "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba", + "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d", + "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3", + "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00", + "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155", + "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac", + "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52", + "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f", + "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8", + "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b", + "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24", + "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea", + "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198", + "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0", + "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee", + "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be", + "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2", + "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707", + "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6", + "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58", + "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779", + "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636", + "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c", + "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad", + "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee", + "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc", + "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2", + "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48", + "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7", + "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e", + "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b", + "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa", + "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5", + "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e", + "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb", + "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9", + "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57", + "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc", + "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2" ], "markers": "python_version >= '3.7'", - "version": "==2.1.2" + "version": "==2.1.3" }, "orderedmultidict": { "hashes": [ @@ -799,11 +807,11 @@ }, "pip": { "hashes": [ - "sha256:0e7c86f486935893c708287b30bd050a36ac827ec7fe5e43fe7cb198dd835fba", - "sha256:3ef6ac33239e4027d9a5598a381b9d30880a1477e50039db2eac6e8a8f6d1b18" + "sha256:7ccf472345f20d35bdc9d1841ff5f313260c2c33fe417f48c30ac46cccabf5be", + "sha256:fb0bd5435b3200c602b5bf61d2d43c2f13c02e29c1707567ae7fbc514eb9faf2" ], "markers": "python_version >= '3.7'", - "version": "==23.1.2" + "version": "==23.2.1" }, "pip-shims": { "hashes": [ @@ -829,11 +837,11 @@ }, "platformdirs": { "hashes": [ - "sha256:412dae91f52a6f84830f39a8078cecd0e866cb72294a5c66808e74d5e88d251f", - "sha256:e2378146f1964972c03c085bb5662ae80b2b8c06226c54b2ff4aa9483e8a13a5" + "sha256:1b42b450ad933e981d56e59f1b97495428c9bd60698baab9f3eb3d00d5822421", + "sha256:ad8291ae0ae5072f66c16945166cb11c63394c7a3ad1b1bc9828ca3162da8c2f" ], "markers": "python_version >= '3.7'", - "version": "==3.5.1" + "version": "==3.9.1" }, "plette": { "extras": [ @@ -872,97 +880,97 @@ }, "regex": { "hashes": [ - "sha256:02f4541550459c08fdd6f97aa4e24c6f1932eec780d58a2faa2068253df7d6ff", - "sha256:0a69cf0c00c4d4a929c6c7717fd918414cab0d6132a49a6d8fc3ded1988ed2ea", - "sha256:0bbd5dcb19603ab8d2781fac60114fb89aee8494f4505ae7ad141a3314abb1f9", - "sha256:10250a093741ec7bf74bcd2039e697f519b028518f605ff2aa7ac1e9c9f97423", - "sha256:10374c84ee58c44575b667310d5bbfa89fb2e64e52349720a0182c0017512f6c", - "sha256:1189fbbb21e2c117fda5303653b61905aeeeea23de4a94d400b0487eb16d2d60", - "sha256:1307aa4daa1cbb23823d8238e1f61292fd07e4e5d8d38a6efff00b67a7cdb764", - "sha256:144b5b017646b5a9392a5554a1e5db0000ae637be4971c9747566775fc96e1b2", - "sha256:171c52e320fe29260da550d81c6b99f6f8402450dc7777ef5ced2e848f3b6f8f", - "sha256:18196c16a584619c7c1d843497c069955d7629ad4a3fdee240eb347f4a2c9dbe", - "sha256:18f05d14f14a812fe9723f13afafefe6b74ca042d99f8884e62dbd34dcccf3e2", - "sha256:1ecf3dcff71f0c0fe3e555201cbe749fa66aae8d18f80d2cc4de8e66df37390a", - "sha256:21e90a288e6ba4bf44c25c6a946cb9b0f00b73044d74308b5e0afd190338297c", - "sha256:23d86ad2121b3c4fc78c58f95e19173790e22ac05996df69b84e12da5816cb17", - "sha256:256f7f4c6ba145f62f7a441a003c94b8b1af78cee2cccacfc1e835f93bc09426", - "sha256:290fd35219486dfbc00b0de72f455ecdd63e59b528991a6aec9fdfc0ce85672e", - "sha256:2e9c4f778514a560a9c9aa8e5538bee759b55f6c1dcd35613ad72523fd9175b8", - "sha256:338994d3d4ca4cf12f09822e025731a5bdd3a37aaa571fa52659e85ca793fb67", - "sha256:33d430a23b661629661f1fe8395be2004006bc792bb9fc7c53911d661b69dd7e", - "sha256:385992d5ecf1a93cb85adff2f73e0402dd9ac29b71b7006d342cc920816e6f32", - "sha256:3d45864693351c15531f7e76f545ec35000d50848daa833cead96edae1665559", - "sha256:40005cbd383438aecf715a7b47fe1e3dcbc889a36461ed416bdec07e0ef1db66", - "sha256:4035d6945cb961c90c3e1c1ca2feb526175bcfed44dfb1cc77db4fdced060d3e", - "sha256:445d6f4fc3bd9fc2bf0416164454f90acab8858cd5a041403d7a11e3356980e8", - "sha256:48c9ec56579d4ba1c88f42302194b8ae2350265cb60c64b7b9a88dcb7fbde309", - "sha256:4a5059bd585e9e9504ef9c07e4bc15b0a621ba20504388875d66b8b30a5c4d18", - "sha256:4a6e4b0e0531223f53bad07ddf733af490ba2b8367f62342b92b39b29f72735a", - "sha256:4b870b6f632fc74941cadc2a0f3064ed8409e6f8ee226cdfd2a85ae50473aa94", - "sha256:50fd2d9b36938d4dcecbd684777dd12a407add4f9f934f235c66372e630772b0", - "sha256:53e22e4460f0245b468ee645156a4f84d0fc35a12d9ba79bd7d79bdcd2f9629d", - "sha256:586a011f77f8a2da4b888774174cd266e69e917a67ba072c7fc0e91878178a80", - "sha256:59597cd6315d3439ed4b074febe84a439c33928dd34396941b4d377692eca810", - "sha256:59e4b729eae1a0919f9e4c0fc635fbcc9db59c74ad98d684f4877be3d2607dd6", - "sha256:5a0f874ee8c0bc820e649c900243c6d1e6dc435b81da1492046716f14f1a2a96", - "sha256:5ac2b7d341dc1bd102be849d6dd33b09701223a851105b2754339e390be0627a", - "sha256:5e3f4468b8c6fd2fd33c218bbd0a1559e6a6fcf185af8bb0cc43f3b5bfb7d636", - "sha256:6164d4e2a82f9ebd7752a06bd6c504791bedc6418c0196cd0a23afb7f3e12b2d", - "sha256:6893544e06bae009916a5658ce7207e26ed17385149f35a3125f5259951f1bbe", - "sha256:690a17db524ee6ac4a27efc5406530dd90e7a7a69d8360235323d0e5dafb8f5b", - "sha256:6b8d0c153f07a953636b9cdb3011b733cadd4178123ef728ccc4d5969e67f3c2", - "sha256:72a28979cc667e5f82ef433db009184e7ac277844eea0f7f4d254b789517941d", - "sha256:72aa4746993a28c841e05889f3f1b1e5d14df8d3daa157d6001a34c98102b393", - "sha256:732176f5427e72fa2325b05c58ad0b45af341c459910d766f814b0584ac1f9ac", - "sha256:7918a1b83dd70dc04ab5ed24c78ae833ae8ea228cef84e08597c408286edc926", - "sha256:7923470d6056a9590247ff729c05e8e0f06bbd4efa6569c916943cb2d9b68b91", - "sha256:7d76a8a1fc9da08296462a18f16620ba73bcbf5909e42383b253ef34d9d5141e", - "sha256:811040d7f3dd9c55eb0d8b00b5dcb7fd9ae1761c454f444fd9f37fe5ec57143a", - "sha256:821a88b878b6589c5068f4cc2cfeb2c64e343a196bc9d7ac68ea8c2a776acd46", - "sha256:84397d3f750d153ebd7f958efaa92b45fea170200e2df5e0e1fd4d85b7e3f58a", - "sha256:844671c9c1150fcdac46d43198364034b961bd520f2c4fdaabfc7c7d7138a2dd", - "sha256:890a09cb0a62198bff92eda98b2b507305dd3abf974778bae3287f98b48907d3", - "sha256:8f08276466fedb9e36e5193a96cb944928301152879ec20c2d723d1031cd4ddd", - "sha256:8f5e06df94fff8c4c85f98c6487f6636848e1dc85ce17ab7d1931df4a081f657", - "sha256:921473a93bcea4d00295799ab929522fc650e85c6b9f27ae1e6bb32a790ea7d3", - "sha256:941b3f1b2392f0bcd6abf1bc7a322787d6db4e7457be6d1ffd3a693426a755f2", - "sha256:9b320677521aabf666cdd6e99baee4fb5ac3996349c3b7f8e7c4eee1c00dfe3a", - "sha256:9c3efee9bb53cbe7b285760c81f28ac80dc15fa48b5fe7e58b52752e642553f1", - "sha256:9fda3e50abad8d0f48df621cf75adc73c63f7243cbe0e3b2171392b445401550", - "sha256:a4c5da39bca4f7979eefcbb36efea04471cd68db2d38fcbb4ee2c6d440699833", - "sha256:a56c18f21ac98209da9c54ae3ebb3b6f6e772038681d6cb43b8d53da3b09ee81", - "sha256:a623564d810e7a953ff1357f7799c14bc9beeab699aacc8b7ab7822da1e952b8", - "sha256:a8906669b03c63266b6a7693d1f487b02647beb12adea20f8840c1a087e2dfb5", - "sha256:a99757ad7fe5c8a2bb44829fc57ced11253e10f462233c1255fe03888e06bc19", - "sha256:aa7d032c1d84726aa9edeb6accf079b4caa87151ca9fabacef31fa028186c66d", - "sha256:aad5524c2aedaf9aa14ef1bc9327f8abd915699dea457d339bebbe2f0d218f86", - "sha256:afb1c70ec1e594a547f38ad6bf5e3d60304ce7539e677c1429eebab115bce56e", - "sha256:b6365703e8cf1644b82104cdd05270d1a9f043119a168d66c55684b1b557d008", - "sha256:b8b942d8b3ce765dbc3b1dad0a944712a89b5de290ce8f72681e22b3c55f3cc8", - "sha256:ba73a14e9c8f9ac409863543cde3290dba39098fc261f717dc337ea72d3ebad2", - "sha256:bd7b68fd2e79d59d86dcbc1ccd6e2ca09c505343445daaa4e07f43c8a9cc34da", - "sha256:bd966475e963122ee0a7118ec9024388c602d12ac72860f6eea119a3928be053", - "sha256:c2ce65bdeaf0a386bb3b533a28de3994e8e13b464ac15e1e67e4603dd88787fa", - "sha256:c64d5abe91a3dfe5ff250c6bb267ef00dbc01501518225b45a5f9def458f31fb", - "sha256:c8c143a65ce3ca42e54d8e6fcaf465b6b672ed1c6c90022794a802fb93105d22", - "sha256:cd46f30e758629c3ee91713529cfbe107ac50d27110fdcc326a42ce2acf4dafc", - "sha256:ced02e3bd55e16e89c08bbc8128cff0884d96e7f7a5633d3dc366b6d95fcd1d6", - "sha256:cf123225945aa58b3057d0fba67e8061c62d14cc8a4202630f8057df70189051", - "sha256:d19e57f888b00cd04fc38f5e18d0efbd91ccba2d45039453ab2236e6eec48d4d", - "sha256:d1cbe6b5be3b9b698d8cc4ee4dee7e017ad655e83361cd0ea8e653d65e469468", - "sha256:db09e6c18977a33fea26fe67b7a842f706c67cf8bda1450974d0ae0dd63570df", - "sha256:de2f780c3242ea114dd01f84848655356af4dd561501896c751d7b885ea6d3a1", - "sha256:e2205a81f815b5bb17e46e74cc946c575b484e5f0acfcb805fb252d67e22938d", - "sha256:e645c757183ee0e13f0bbe56508598e2d9cd42b8abc6c0599d53b0d0b8dd1479", - "sha256:f2910502f718828cecc8beff004917dcf577fc5f8f5dd40ffb1ea7612124547b", - "sha256:f764e4dfafa288e2eba21231f455d209f4709436baeebb05bdecfb5d8ddc3d35", - "sha256:f83fe9e10f9d0b6cf580564d4d23845b9d692e4c91bd8be57733958e4c602956", - "sha256:fb2b495dd94b02de8215625948132cc2ea360ae84fe6634cd19b6567709c8ae2", - "sha256:fee0016cc35a8a91e8cc9312ab26a6fe638d484131a7afa79e1ce6165328a135" + "sha256:0385e73da22363778ef2324950e08b689abdf0b108a7d8decb403ad7f5191938", + "sha256:051da80e6eeb6e239e394ae60704d2b566aa6a7aed6f2890a7967307267a5dc6", + "sha256:05ed27acdf4465c95826962528f9e8d41dbf9b1aa8531a387dee6ed215a3e9ef", + "sha256:0654bca0cdf28a5956c83839162692725159f4cda8d63e0911a2c0dc76166525", + "sha256:09e4a1a6acc39294a36b7338819b10baceb227f7f7dbbea0506d419b5a1dd8af", + "sha256:0b49c764f88a79160fa64f9a7b425620e87c9f46095ef9c9920542ab2495c8bc", + "sha256:0b71e63226e393b534105fcbdd8740410dc6b0854c2bfa39bbda6b0d40e59a54", + "sha256:0c29ca1bd61b16b67be247be87390ef1d1ef702800f91fbd1991f5c4421ebae8", + "sha256:10590510780b7541969287512d1b43f19f965c2ece6c9b1c00fc367b29d8dce7", + "sha256:10cb847aeb1728412c666ab2e2000ba6f174f25b2bdc7292e7dd71b16db07568", + "sha256:12b74fbbf6cbbf9dbce20eb9b5879469e97aeeaa874145517563cca4029db65c", + "sha256:20326216cc2afe69b6e98528160b225d72f85ab080cbdf0b11528cbbaba2248f", + "sha256:2239d95d8e243658b8dbb36b12bd10c33ad6e6933a54d36ff053713f129aa536", + "sha256:25be746a8ec7bc7b082783216de8e9473803706723b3f6bef34b3d0ed03d57e2", + "sha256:271f0bdba3c70b58e6f500b205d10a36fb4b58bd06ac61381b68de66442efddb", + "sha256:29cdd471ebf9e0f2fb3cac165efedc3c58db841d83a518b082077e612d3ee5df", + "sha256:2d44dc13229905ae96dd2ae2dd7cebf824ee92bc52e8cf03dcead37d926da019", + "sha256:3676f1dd082be28b1266c93f618ee07741b704ab7b68501a173ce7d8d0d0ca18", + "sha256:36efeba71c6539d23c4643be88295ce8c82c88bbd7c65e8a24081d2ca123da3f", + "sha256:3e5219bf9e75993d73ab3d25985c857c77e614525fac9ae02b1bebd92f7cecac", + "sha256:43e1dd9d12df9004246bacb79a0e5886b3b6071b32e41f83b0acbf293f820ee8", + "sha256:457b6cce21bee41ac292d6753d5e94dcbc5c9e3e3a834da285b0bde7aa4a11e9", + "sha256:463b6a3ceb5ca952e66550a4532cef94c9a0c80dc156c4cc343041951aec1697", + "sha256:4959e8bcbfda5146477d21c3a8ad81b185cd252f3d0d6e4724a5ef11c012fb06", + "sha256:4d3850beab9f527f06ccc94b446c864059c57651b3f911fddb8d9d3ec1d1b25d", + "sha256:5708089ed5b40a7b2dc561e0c8baa9535b77771b64a8330b684823cfd5116036", + "sha256:5c6b48d0fa50d8f4df3daf451be7f9689c2bde1a52b1225c5926e3f54b6a9ed1", + "sha256:61474f0b41fe1a80e8dfa70f70ea1e047387b7cd01c85ec88fa44f5d7561d787", + "sha256:6343c6928282c1f6a9db41f5fd551662310e8774c0e5ebccb767002fcf663ca9", + "sha256:65ba8603753cec91c71de423a943ba506363b0e5c3fdb913ef8f9caa14b2c7e0", + "sha256:687ea9d78a4b1cf82f8479cab23678aff723108df3edeac098e5b2498879f4a7", + "sha256:6b2675068c8b56f6bfd5a2bda55b8accbb96c02fd563704732fd1c95e2083461", + "sha256:7117d10690c38a622e54c432dfbbd3cbd92f09401d622902c32f6d377e2300ee", + "sha256:7178bbc1b2ec40eaca599d13c092079bf529679bf0371c602edaa555e10b41c3", + "sha256:72d1a25bf36d2050ceb35b517afe13864865268dfb45910e2e17a84be6cbfeb0", + "sha256:742e19a90d9bb2f4a6cf2862b8b06dea5e09b96c9f2df1779e53432d7275331f", + "sha256:74390d18c75054947e4194019077e243c06fbb62e541d8817a0fa822ea310c14", + "sha256:74419d2b50ecb98360cfaa2974da8689cb3b45b9deff0dcf489c0d333bcc1477", + "sha256:824bf3ac11001849aec3fa1d69abcb67aac3e150a933963fb12bda5151fe1bfd", + "sha256:83320a09188e0e6c39088355d423aa9d056ad57a0b6c6381b300ec1a04ec3d16", + "sha256:837328d14cde912af625d5f303ec29f7e28cdab588674897baafaf505341f2fc", + "sha256:841d6e0e5663d4c7b4c8099c9997be748677d46cbf43f9f471150e560791f7ff", + "sha256:87b2a5bb5e78ee0ad1de71c664d6eb536dc3947a46a69182a90f4410f5e3f7dd", + "sha256:890e5a11c97cf0d0c550eb661b937a1e45431ffa79803b942a057c4fb12a2da2", + "sha256:8abbc5d54ea0ee80e37fef009e3cec5dafd722ed3c829126253d3e22f3846f1e", + "sha256:8e3f1316c2293e5469f8f09dc2d76efb6c3982d3da91ba95061a7e69489a14ef", + "sha256:8f56fcb7ff7bf7404becdfc60b1e81a6d0561807051fd2f1860b0d0348156a07", + "sha256:9427a399501818a7564f8c90eced1e9e20709ece36be701f394ada99890ea4b3", + "sha256:976d7a304b59ede34ca2921305b57356694f9e6879db323fd90a80f865d355a3", + "sha256:9a5bfb3004f2144a084a16ce19ca56b8ac46e6fd0651f54269fc9e230edb5e4a", + "sha256:9beb322958aaca059f34975b0df135181f2e5d7a13b84d3e0e45434749cb20f7", + "sha256:9edcbad1f8a407e450fbac88d89e04e0b99a08473f666a3f3de0fd292badb6aa", + "sha256:9edce5281f965cf135e19840f4d93d55b3835122aa76ccacfd389e880ba4cf82", + "sha256:a4c3b7fa4cdaa69268748665a1a6ff70c014d39bb69c50fda64b396c9116cf77", + "sha256:a8105e9af3b029f243ab11ad47c19b566482c150c754e4c717900a798806b222", + "sha256:a99b50300df5add73d307cf66abea093304a07eb017bce94f01e795090dea87c", + "sha256:aad51907d74fc183033ad796dd4c2e080d1adcc4fd3c0fd4fd499f30c03011cd", + "sha256:af4dd387354dc83a3bff67127a124c21116feb0d2ef536805c454721c5d7993d", + "sha256:b28f5024a3a041009eb4c333863d7894d191215b39576535c6734cd88b0fcb68", + "sha256:b4598b1897837067a57b08147a68ac026c1e73b31ef6e36deeeb1fa60b2933c9", + "sha256:b6192d5af2ccd2a38877bfef086d35e6659566a335b1492786ff254c168b1693", + "sha256:b862c2b9d5ae38a68b92e215b93f98d4c5e9454fa36aae4450f61dd33ff48487", + "sha256:b956231ebdc45f5b7a2e1f90f66a12be9610ce775fe1b1d50414aac1e9206c06", + "sha256:bb60b503ec8a6e4e3e03a681072fa3a5adcbfa5479fa2d898ae2b4a8e24c4591", + "sha256:bbb02fd4462f37060122e5acacec78e49c0fbb303c30dd49c7f493cf21fc5b27", + "sha256:bdff5eab10e59cf26bc479f565e25ed71a7d041d1ded04ccf9aee1d9f208487a", + "sha256:c123f662be8ec5ab4ea72ea300359023a5d1df095b7ead76fedcd8babbedf969", + "sha256:c2b867c17a7a7ae44c43ebbeb1b5ff406b3e8d5b3e14662683e5e66e6cc868d3", + "sha256:c5f8037000eb21e4823aa485149f2299eb589f8d1fe4b448036d230c3f4e68e0", + "sha256:c6a57b742133830eec44d9b2290daf5cbe0a2f1d6acee1b3c7b1c7b2f3606df7", + "sha256:ccf91346b7bd20c790310c4147eee6ed495a54ddb6737162a36ce9dbef3e4751", + "sha256:cf67ca618b4fd34aee78740bea954d7c69fdda419eb208c2c0c7060bb822d747", + "sha256:d2da3abc88711bce7557412310dfa50327d5769a31d1c894b58eb256459dc289", + "sha256:d4f03bb71d482f979bda92e1427f3ec9b220e62a7dd337af0aa6b47bf4498f72", + "sha256:d54af539295392611e7efbe94e827311eb8b29668e2b3f4cadcfe6f46df9c777", + "sha256:d77f09bc4b55d4bf7cc5eba785d87001d6757b7c9eec237fe2af57aba1a071d9", + "sha256:d831c2f8ff278179705ca59f7e8524069c1a989e716a1874d6d1aab6119d91d1", + "sha256:dbbbfce33cd98f97f6bffb17801b0576e653f4fdb1d399b2ea89638bc8d08ae1", + "sha256:dcba6dae7de533c876255317c11f3abe4907ba7d9aa15d13e3d9710d4315ec0e", + "sha256:e0bb18053dfcfed432cc3ac632b5e5e5c5b7e55fb3f8090e867bfd9b054dbcbf", + "sha256:e2fbd6236aae3b7f9d514312cdb58e6494ee1c76a9948adde6eba33eb1c4264f", + "sha256:e5087a3c59eef624a4591ef9eaa6e9a8d8a94c779dade95d27c0bc24650261cd", + "sha256:e8915cc96abeb8983cea1df3c939e3c6e1ac778340c17732eb63bb96247b91d2", + "sha256:ea353ecb6ab5f7e7d2f4372b1e779796ebd7b37352d290096978fea83c4dba0c", + "sha256:ee2d1a9a253b1729bb2de27d41f696ae893507c7db224436abe83ee25356f5c1", + "sha256:f415f802fbcafed5dcc694c13b1292f07fe0befdb94aa8a52905bd115ff41e88", + "sha256:fb5ec16523dc573a4b277663a2b5a364e2099902d3944c9419a40ebd56a118f9", + "sha256:fea75c3710d4f31389eed3c02f62d0b66a9da282521075061ce875eb5300cf23" ], "markers": "python_version >= '3.6'", - "version": "==2023.5.5" + "version": "==2023.6.3" }, "requests": { "hashes": [ @@ -982,11 +990,11 @@ }, "setuptools": { "hashes": [ - "sha256:5df61bf30bb10c6f756eb19e7c9f3b473051f48db77fddbe06ff2ca307df9a6f", - "sha256:62642358adc77ffa87233bc4d2354c4b2682d214048f500964dbe760ccedf102" + "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f", + "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235" ], "markers": "python_version >= '3.7'", - "version": "==67.8.0" + "version": "==68.0.0" }, "six": { "hashes": [ @@ -1101,41 +1109,58 @@ }, "typed-ast": { "hashes": [ - "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2", - "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1", - "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6", - "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62", - "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac", - "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d", - "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc", - "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2", - "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97", - "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35", - "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6", - "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1", - "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4", - "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c", - "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e", - "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec", - "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f", - "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72", - "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47", - "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72", - "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe", - "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6", - "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3", - "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66" + "sha256:042eb665ff6bf020dd2243307d11ed626306b82812aba21836096d229fdc6a10", + "sha256:045f9930a1550d9352464e5149710d56a2aed23a2ffe78946478f7b5416f1ede", + "sha256:0635900d16ae133cab3b26c607586131269f88266954eb04ec31535c9a12ef1e", + "sha256:118c1ce46ce58fda78503eae14b7664163aa735b620b64b5b725453696f2a35c", + "sha256:16f7313e0a08c7de57f2998c85e2a69a642e97cb32f87eb65fbfe88381a5e44d", + "sha256:1efebbbf4604ad1283e963e8915daa240cb4bf5067053cf2f0baadc4d4fb51b8", + "sha256:2188bc33d85951ea4ddad55d2b35598b2709d122c11c75cffd529fbc9965508e", + "sha256:2b946ef8c04f77230489f75b4b5a4a6f24c078be4aed241cfabe9cbf4156e7e5", + "sha256:335f22ccb244da2b5c296e6f96b06ee9bed46526db0de38d2f0e5a6597b81155", + "sha256:381eed9c95484ceef5ced626355fdc0765ab51d8553fec08661dce654a935db4", + "sha256:429ae404f69dc94b9361bb62291885894b7c6fb4640d561179548c849f8492ba", + "sha256:44f214394fc1af23ca6d4e9e744804d890045d1643dd7e8229951e0ef39429b5", + "sha256:48074261a842acf825af1968cd912f6f21357316080ebaca5f19abbb11690c8a", + "sha256:4bc1efe0ce3ffb74784e06460f01a223ac1f6ab31c6bc0376a21184bf5aabe3b", + "sha256:57bfc3cf35a0f2fdf0a88a3044aafaec1d2f24d8ae8cd87c4f58d615fb5b6311", + "sha256:597fc66b4162f959ee6a96b978c0435bd63791e31e4f410622d19f1686d5e769", + "sha256:5f7a8c46a8b333f71abd61d7ab9255440d4a588f34a21f126bbfc95f6049e686", + "sha256:5fe83a9a44c4ce67c796a1b466c270c1272e176603d5e06f6afbc101a572859d", + "sha256:61443214d9b4c660dcf4b5307f15c12cb30bdfe9588ce6158f4a005baeb167b2", + "sha256:622e4a006472b05cf6ef7f9f2636edc51bda670b7bbffa18d26b255269d3d814", + "sha256:6eb936d107e4d474940469e8ec5b380c9b329b5f08b78282d46baeebd3692dc9", + "sha256:7f58fabdde8dcbe764cef5e1a7fcb440f2463c1bbbec1cf2a86ca7bc1f95184b", + "sha256:83509f9324011c9a39faaef0922c6f720f9623afe3fe220b6d0b15638247206b", + "sha256:8c524eb3024edcc04e288db9541fe1f438f82d281e591c548903d5b77ad1ddd4", + "sha256:94282f7a354f36ef5dbce0ef3467ebf6a258e370ab33d5b40c249fa996e590dd", + "sha256:b445c2abfecab89a932b20bd8261488d574591173d07827c1eda32c457358b18", + "sha256:be4919b808efa61101456e87f2d4c75b228f4e52618621c77f1ddcaae15904fa", + "sha256:bfd39a41c0ef6f31684daff53befddae608f9daf6957140228a08e51f312d7e6", + "sha256:c631da9710271cb67b08bd3f3813b7af7f4c69c319b75475436fcab8c3d21bee", + "sha256:cc95ffaaab2be3b25eb938779e43f513e0e538a84dd14a5d844b8f2932593d88", + "sha256:d09d930c2d1d621f717bb217bf1fe2584616febb5138d9b3e8cdd26506c3f6d4", + "sha256:d40c10326893ecab8a80a53039164a224984339b2c32a6baf55ecbd5b1df6431", + "sha256:d41b7a686ce653e06c2609075d397ebd5b969d821b9797d029fccd71fdec8e04", + "sha256:d5c0c112a74c0e5db2c75882a0adf3133adedcdbfd8cf7c9d6ed77365ab90a1d", + "sha256:e1a976ed4cc2d71bb073e1b2a250892a6e968ff02aa14c1f40eba4f365ffec02", + "sha256:e48bf27022897577d8479eaed64701ecaf0467182448bd95759883300ca818c8", + "sha256:ed4a1a42df8a3dfb6b40c3d2de109e935949f2f66b19703eafade03173f8f437", + "sha256:f0aefdd66f1784c58f65b502b6cf8b121544680456d1cebbd300c2c813899274", + "sha256:fc2b8c4e1bc5cd96c1a823a885e6b158f8451cf6f5530e1829390b4d27d0807f", + "sha256:fd946abf3c31fb50eee07451a6aedbfff912fcd13cf357363f5b4e834cc5e71a", + "sha256:fe58ef6a764de7b4b36edfc8592641f56e69b7163bba9f9c8089838ee596bfb2" ], "markers": "python_version >= '3.6'", - "version": "==1.5.4" + "version": "==1.5.5" }, "urllib3": { "hashes": [ - "sha256:61717a1095d7e155cdb737ac7bb2f4324a858a1e2e6466f6d03ff630ca68d3cc", - "sha256:d055c2f9d38dc53c808f6fdc8eab7360b6fdbbde02340ed25cfbcd817c62469e" + "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11", + "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4" ], "markers": "python_version >= '3.7'", - "version": "==2.0.2" + "version": "==2.0.4" }, "vistir": { "hashes": [ @@ -1147,11 +1172,11 @@ }, "wheel": { "hashes": [ - "sha256:cd1196f3faee2b31968d626e1731c94f99cbdb67cf5a46e4f5656cbee7738873", - "sha256:d236b20e7cb522daf2390fa84c55eea81c5c30190f90f29ae2ca1ad8355bf247" + "sha256:55a0f0a5a84869bce5ba775abfd9c462e3a6b1b7b7ec69d72c0b83d673a5114d", + "sha256:7e9be3bbd0078f6147d82ed9ed957e323e7708f57e134743d2edef3a7b7972a9" ], "markers": "python_version >= '3.7'", - "version": "==0.40.0" + "version": "==0.41.0" } } } diff --git a/docs/source/index.rst b/docs/source/index.rst index 716e56cb..baacb80a 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -74,6 +74,7 @@ Rapyuta CLI has commands for all rapyuta.io resources. You can read more about t Rosbag Secret Static Route + User Group VPN diff --git a/docs/source/usergroup.rst b/docs/source/usergroup.rst new file mode 100644 index 00000000..b0f1e5e6 --- /dev/null +++ b/docs/source/usergroup.rst @@ -0,0 +1,10 @@ +User Group +============ + +.. toctree:: + :maxdepth: 3 + :caption: Contents: + +.. click:: riocli.usergroup:usergroup + :prog: rio usergroup + :nested: full diff --git a/riocli/apply/__init__.py b/riocli/apply/__init__.py index 92a87c63..d2392e5b 100644 --- a/riocli/apply/__init__.py +++ b/riocli/apply/__init__.py @@ -21,13 +21,14 @@ from riocli.apply.parse import Applier from riocli.apply.template import template from riocli.apply.util import process_files_values_secrets +from riocli.constants import Colors @click.command( 'apply', cls=HelpColorsCommand, - help_headers_color='yellow', - help_options_color='green', + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, ) @click.option('--dryrun', '-d', is_flag=True, default=False, help='dry run the yaml files without applying any change') @@ -47,11 +48,17 @@ @click.option('-f', '--force', '--silent', 'silent', is_flag=True, type=click.BOOL, default=False, help="Skip confirmation") +@click.option('--retry-count', '-rc', type=int, default=50, + help="Number of retries before a resource creation times out status, defaults to 50") +@click.option('--retry-interval', '-ri', type=int, default=6, + help="Interval between retries defaults to 6") @click.argument('files', nargs=-1) def apply( values: str, secrets: str, files: Iterable[str], + retry_count: int, + retry_interval: int, dryrun: bool = False, workers: int = 6, silent: bool = False, @@ -64,12 +71,12 @@ def apply( files, values, secrets) if len(glob_files) == 0: - click.secho('no files specified', fg='red') + click.secho('No files specified', fg=Colors.RED) raise SystemExit(1) - click.secho("----- Files Processed ----", fg="yellow") + click.secho("----- Files Processed ----", fg=Colors.YELLOW) for file in glob_files: - click.secho(file, fg="yellow") + click.secho(file, fg=Colors.YELLOW) rc = Applier(glob_files, abs_values, abs_secrets) rc.parse_dependencies() @@ -86,24 +93,34 @@ def apply( if not silent and not dryrun: click.confirm("Do you want to proceed?", default=True, abort=True) - rc.apply(dryrun=dryrun, workers=workers) + rc.apply(dryrun=dryrun, workers=workers, retry_count=retry_count, retry_interval=retry_interval) @click.command( 'delete', cls=HelpColorsCommand, - help_headers_color='yellow', - help_options_color='green', + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, ) -@click.option('--dryrun', '-d', is_flag=True, default=False, help='dry run the yaml files without applying any change') +@click.option('--dryrun', '-d', is_flag=True, default=False, + help='dry run the yaml files without applying any change') @click.option('--values', '-v', - help="path to values yaml file. key/values specified in the values file can be used as variables in template yamls") + help="Path to values yaml file. key/values specified in the" + " values file can be used as variables in template YAMLs") @click.option('--secrets', '-s', - help="secret files are sops encoded value files. rio-cli expects sops to be authorized for decoding files on this computer") -@click.option('-f', '--force', '--silent', 'silent', is_flag=True, type=click.BOOL, default=False, + help="Secret files are sops encoded value files. riocli expects " + "sops to be authorized for decoding files on this computer") +@click.option('-f', '--force', '--silent', 'silent', is_flag=True, + type=click.BOOL, default=False, help="Skip confirmation") @click.argument('files', nargs=-1) -def delete(values: str, secrets: str, files: Iterable[str], dryrun: bool = False, silent: bool = False) -> None: +def delete( + values: str, + secrets: str, + files: Iterable[str], + dryrun: bool = False, + silent: bool = False +) -> None: """ Removes resources defined in the manifest """ @@ -111,7 +128,7 @@ def delete(values: str, secrets: str, files: Iterable[str], dryrun: bool = False files, values, secrets) if len(glob_files) == 0: - click.secho('no files specified', fg='red') + click.secho('no files specified', fg=Colors.RED) raise SystemExit(1) rc = Applier(glob_files, abs_values, abs_secrets) diff --git a/riocli/apply/explain.py b/riocli/apply/explain.py index 603a549d..daff5fc8 100644 --- a/riocli/apply/explain.py +++ b/riocli/apply/explain.py @@ -16,12 +16,14 @@ import click from click_help_colors import HelpColorsCommand +from riocli.constants import Colors, Symbols + @click.command( 'explain', cls=HelpColorsCommand, - help_headers_color='yellow', - help_options_color='green', + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, help='Generates a sample resource manifest for the given type' ) @click.option('--templates', help='Alternate root for templates', @@ -39,5 +41,6 @@ def explain(resource: str, templates: str = None) -> None: click.echo_via_pager(f.readlines()) raise SystemExit(0) - click.secho("[Err] Resource \"{}\" not found".format(resource), fg='red') + click.secho('{} Resource "{}" not found'.format(Symbols.ERROR, resource), + fg=Colors.RED) raise SystemExit(1) diff --git a/riocli/apply/manifests/usergroup.yaml b/riocli/apply/manifests/usergroup.yaml new file mode 100644 index 00000000..c63734f9 --- /dev/null +++ b/riocli/apply/manifests/usergroup.yaml @@ -0,0 +1,18 @@ +--- +apiVersion: api.rapyuta.io/v2 +kind: UserGroup +metadata: + name: usergroup_name + organization: org-bqgpmsafgnvnawlkuvxtxohs + labels: + key: value +spec: + description: This is a sample user group description + members: + - emailID: qa.rapyuta+e2e@gmail.com + - emailID: random.user@rapyuta-robotics.com + admins: + - emailID: admin.user@rapyuta-robotics.com + projects: + - name: project01 + - name: project02 diff --git a/riocli/apply/parse.py b/riocli/apply/parse.py index a13fb6fe..0703ad81 100644 --- a/riocli/apply/parse.py +++ b/riocli/apply/parse.py @@ -25,10 +25,10 @@ from riocli.apply.resolver import ResolverCache from riocli.config import Configuration -from riocli.utils import dump_all_yaml -from riocli.utils import print_separator -from riocli.utils import run_bash +from riocli.constants import Colors, Symbols +from riocli.utils import dump_all_yaml, print_separator, run_bash from riocli.utils.mermaid import mermaid_link, mermaid_safe +from riocli.utils.spinner import with_spinner class Applier(object): @@ -79,13 +79,22 @@ def __init__(self, files: typing.List, values, secrets): def order(self): return self.graph.static_order() + @with_spinner(text='Applying...', timer=True) def apply(self, *args, **kwargs): + spinner = kwargs.get('spinner') kwargs['workers'] = int(kwargs.get('workers') or self.DEFAULT_MAX_WORKERS) + apply_func = self.apply_async if kwargs['workers'] == 1: - return self.apply_sync(*args, **kwargs) + apply_func = self.apply_sync - return self.apply_async(*args, **kwargs) + try: + apply_func(*args, **kwargs) + spinner.text = 'Apply successful.' + spinner.green.ok(Symbols.SUCCESS) + except Exception as e: + spinner.text = 'Apply failed. Error: {}'.format(e) + spinner.red.fail(Symbols.ERROR) def apply_async(self, *args, **kwargs): workers = int(kwargs.get('workers') or self.DEFAULT_MAX_WORKERS) @@ -95,12 +104,14 @@ def apply_async(self, *args, **kwargs): def worker(): while True: o = task_queue.get() - if o in self.resolved_objects and 'manifest' in self.resolved_objects[o]: + if o in self.resolved_objects and 'manifest' in \ + self.resolved_objects[o]: try: self._apply_manifest(o, *args, **kwargs) except Exception as ex: click.secho( - '[Err] Object "{}" apply failed. Apply will not progress further.'.format(o, str(ex))) + '[Err] Object "{}" apply failed. Apply will not progress further.'.format( + o, str(ex))) done_queue.put(ex) continue @@ -129,17 +140,26 @@ def apply_sync(self, *args, **kwargs): self.graph.prepare() while self.graph.is_active(): for obj in self.graph.get_ready(): - if obj in self.resolved_objects and 'manifest' in self.resolved_objects[obj]: + if (obj in self.resolved_objects and + 'manifest' in self.resolved_objects[obj]): self._apply_manifest(obj, *args, **kwargs) self.graph.done(obj) + @with_spinner(text='Deleting...', timer=True) def delete(self, *args, **kwargs): + spinner = kwargs.get('spinner') delete_order = list(self.graph.static_order()) delete_order.reverse() - for obj in delete_order: - if obj in self.resolved_objects and 'manifest' in \ - self.resolved_objects[obj]: - self._delete_manifest(obj, *args, **kwargs) + try: + for obj in delete_order: + if (obj in self.resolved_objects and + 'manifest' in self.resolved_objects[obj]): + self._delete_manifest(obj, *args, **kwargs) + spinner.text = 'Delete successful.' + spinner.green.ok(Symbols.SUCCESS) + except Exception as e: + spinner.text = 'Delete failed. Error: {}'.format(e) + spinner.red.fail(Symbols.ERROR) def print_resolved_manifests(self): manifests = [o for _, o in self.objects.items()] @@ -149,7 +169,7 @@ def parse_dependencies( self, check_missing=True, delete=False, - template=False, + template=False ): number_of_objects = 0 for f, data in self.files.items(): @@ -163,21 +183,22 @@ def parse_dependencies( total_time = 0 for node in copy.deepcopy(self.graph).static_order(): - - action = 'CREATE' if not self.resolved_objects[node]['src'] == 'remote' else 'UPDATE' - if delete: + action = 'UPDATE' + if not self.resolved_objects[node]['src'] == 'remote': + action = 'CREATE' + elif delete: action = 'DELETE' kind = node.split(":")[0] expected_time = round( self.EXPECTED_TIME.get(kind.lower(), 5) / 60, 2) - total_time = total_time + expected_time + total_time += expected_time resource_list.append([node, action, expected_time]) if not template: self._display_context( total_time=total_time, total_objects=number_of_objects, - resource_list=resource_list, + resource_list=resource_list ) if check_missing: @@ -187,9 +208,12 @@ def parse_dependencies( missing_resources.append(key) if missing_resources: - click.secho("missing resources found in yaml. " + - "Please ensure the following are either available in your yaml" + - "or created on the server. {}".format(set(missing_resources)), fg="red") + click.secho( + "Missing resources found in yaml. Please ensure the " + "following are either available in your YAML or created" + " on the server. {}".format( + set(missing_resources)), fg=Colors.RED) + raise SystemExit(1) # Manifest Operations via base.py @@ -224,7 +248,7 @@ def _register_object(self, data): self.objects[key] = data self.resolved_objects[key] = {'src': 'local', 'manifest': data} except KeyError: - click.secho("Key error {}".format(data), fg='red') + click.secho("Key error {}".format(data), fg=Colors.RED) return def _load_file_content(self, file_name, is_value=False, is_secret=False): @@ -285,7 +309,8 @@ def _add_graph_node(self, key): def _add_graph_edge(self, dependent_key, key): self.graph.add(dependent_key, key) self.diagram.append('\t{}[{}] --> {}[{}] '.format(mermaid_safe(key), - key, mermaid_safe(dependent_key), dependent_key)) + key, mermaid_safe( + dependent_key), dependent_key)) # Dependency Resolution def _parse_dependency(self, dependent_key, model): @@ -328,7 +353,8 @@ def _resolve_dependency(self, dependent_key, dependency): dependent_key, obj_guid, dependency, obj) if (name_or_guid == obj_name) and ( - 'version' in dependency and obj['packageVersion'] == dependency.get('version')): + 'version' in dependency and obj[ + 'packageVersion'] == dependency.get('version')): self._add_remote_object_to_resolve_tree( dependent_key, obj_guid, dependency, obj) @@ -348,7 +374,8 @@ def _resolve_dependency(self, dependent_key, dependency): if key not in self.resolved_objects: self.resolved_objects[key] = {'src': 'missing'} - def _add_remote_object_to_resolve_tree(self, dependent_key, guid, dependency, obj): + def _add_remote_object_to_resolve_tree(self, dependent_key, guid, + dependency, obj): kind = dependency.get('kind') name_or_guid = dependency.get('nameOrGUID') key = '{}:{}'.format(kind, name_or_guid) @@ -385,8 +412,8 @@ def _display_context( total_objects: int, resource_list: typing.List ) -> None: - # Display context - headers = [click.style('Resource Context', bold=True, fg='yellow')] + headers = [ + click.style('Resource Context', bold=True, fg=Colors.YELLOW)] context = [ ['Expected Time (mins)', round(total_time, 2)], ['Files', len(self.files)], @@ -398,7 +425,7 @@ def _display_context( # Display Resource Inventory headers = [] for header in ['Resource', 'Action', 'Expected Time (mins)']: - headers.append(click.style(header, fg='yellow', bold=True)) + headers.append(click.style(header, fg=Colors.YELLOW, bold=True)) print_separator() click.echo(tabulate(resource_list, headers=headers, diff --git a/riocli/apply/resolver.py b/riocli/apply/resolver.py index ec6929c6..79fb9772 100644 --- a/riocli/apply/resolver.py +++ b/riocli/apply/resolver.py @@ -32,6 +32,7 @@ from riocli.project.model import Project from riocli.secret.model import Secret from riocli.static_route.model import StaticRoute +from riocli.usergroup.model import UserGroup class _Singleton(type): @@ -55,7 +56,8 @@ class ResolverCache(object, metaclass=_Singleton): 'Package': Package, 'Disk': Disk, 'Deployment': Deployment, - "ManagedService": ManagedService + "ManagedService": ManagedService, + 'UserGroup': UserGroup, } KIND_REGEX = { @@ -99,7 +101,12 @@ def find_depends(self, depends, *args): elif 'guid' in depends and depends['kind'] not in ('network', 'managedservice'): return depends['guid'], None elif 'nameOrGUID' in depends: - obj_list = self._list_functors(depends['kind'])() + if depends['kind'] == 'usergroup': + org_guid = depends['organization'] + obj_list = self._list_functors(depends['kind'])(org_guid) + else: + obj_list = self._list_functors(depends['kind'])() + obj_match = list(self._find_functors(depends['kind'])( depends['nameOrGUID'], obj_list, *args)) if not obj_list or (isinstance(obj_list, list) and len(obj_list) == 0): @@ -118,11 +125,12 @@ def _guid_functor(self, kind): "staticroute": lambda x: munchify(x)['guid'], "build": lambda x: munchify(x)['guid'], "deployment": lambda x: munchify(x)['deploymentId'], - "network": lambda x: munchify(x)['guid'], + "network": lambda x: munchify(x).guid, # This is only temporarily like this "disk": lambda x: munchify(x)['internalDeploymentGUID'], "device": lambda x: munchify(x)['uuid'], "managedservice": lambda x: munchify(x)['metadata']['name'], + "usergroup": lambda x: munchify(x).guid } return mapping[kind] @@ -140,6 +148,7 @@ def _list_functors(self, kind): "disk": self._list_disks, "device": self.client.get_all_devices, "managedservice": self._list_managedservices, + "usergroup": self.client.list_usergroups } return mapping[kind] @@ -158,6 +167,7 @@ def _find_functors(self, kind): "disk": self._generate_find_guid_functor(), "device": self._generate_find_guid_functor(), "managedservice": lambda name, instances: filter(lambda i: i.metadata.name == name, instances), + "usergroup": lambda name, groups: filter(lambda i: i.name == name, groups), } return mapping[kind] diff --git a/riocli/apply/template.py b/riocli/apply/template.py index b9bbde2f..82909219 100644 --- a/riocli/apply/template.py +++ b/riocli/apply/template.py @@ -19,18 +19,21 @@ from riocli.apply.parse import Applier from riocli.apply.util import process_files_values_secrets +from riocli.constants import Colors @click.command( 'template', cls=HelpColorsCommand, - help_headers_color='yellow', - help_options_color='green', + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, ) @click.option('--values', '-v', - help="path to values yaml file. key/values specified in the values file can be used as variables in template yamls") + help='Path to values yaml file. key/values specified in the ' + 'values file can be used as variables in template YAMLs') @click.option('--secrets', '-s', - help="secret files are sops encoded value files. rio-cli expects sops to be authorized for decoding files on this computer") + help='Secret files are sops encoded value files. riocli ' + 'expects sops to be authorized for decoding files on this computer') @click.argument('files', nargs=-1) def template(values: str, secrets: str, files: Iterable[str]) -> None: """ @@ -40,7 +43,7 @@ def template(values: str, secrets: str, files: Iterable[str]) -> None: files, values, secrets) if len(glob_files) == 0: - click.secho('no files specified', fg='red') + click.secho('No files specified', fg=Colors.RED) raise SystemExit(1) rc = Applier(glob_files, abs_values, abs_secrets) diff --git a/riocli/auth/__init__.py b/riocli/auth/__init__.py index 2e29ccce..7211d180 100644 --- a/riocli/auth/__init__.py +++ b/riocli/auth/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,13 +22,14 @@ from riocli.auth.status import status from riocli.auth.token import token from riocli.config import new_client +from riocli.constants import Colors @click.group( invoke_without_command=False, cls=HelpColorsGroup, - help_headers_color="yellow", - help_options_color="green", + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, ) def auth(): """ @@ -43,7 +44,7 @@ def get_rio_client() -> Client: auth.add_command(login) auth.add_command(logout) -auth.add_command(status) -auth.add_command(refresh_token) auth.add_command(token) +auth.add_command(status) auth.add_command(environment) +auth.add_command(refresh_token) diff --git a/riocli/auth/login.py b/riocli/auth/login.py index 227e8f50..dac8a7a5 100644 --- a/riocli/auth/login.py +++ b/riocli/auth/login.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,15 +20,17 @@ select_project, validate_token, ) +from riocli.constants import Colors, Symbols from riocli.utils.context import get_root_context -LOGIN_SUCCESS = click.style('Logged in successfully!', fg='green') +LOGIN_SUCCESS = click.style('{} Logged in successfully!'.format(Symbols.SUCCESS), fg=Colors.GREEN) @click.command( + 'login', cls=HelpColorsCommand, - help_headers_color='yellow', - help_options_color='green', + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, ) @click.option('--email', type=str, help='Email of the rapyuta.io account') @@ -88,9 +90,10 @@ def login( if not ctx.obj.exists or not interactive: ctx.obj.save() else: - click.secho("[Warning] rio already has a config file present", - fg='yellow') - click.confirm('Do you want to override the config?', abort=True) + click.confirm( + '{} Config already exists. Do you want to override' + ' the existing config?'.format(Symbols.WARNING), + abort=True) if not interactive: # When just the email and password are provided @@ -105,7 +108,7 @@ def login( if project and not organization: click.secho( 'Please specify an organization. See `rio auth login --help`', - fg='yellow') + fg=Colors.YELLOW) raise SystemExit(1) # When just the organization is provided, we save the @@ -114,7 +117,7 @@ def login( if organization and not project: select_organization(ctx.obj, organization=organization) click.secho("Your organization is set to '{}'".format( - ctx.obj.data['organization_name']), fg='green') + ctx.obj.data['organization_name']), fg=Colors.CYAN) ctx.obj.save() click.echo(LOGIN_SUCCESS) return diff --git a/riocli/auth/logout.py b/riocli/auth/logout.py index 7c71c351..ee47dd35 100644 --- a/riocli/auth/logout.py +++ b/riocli/auth/logout.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,9 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. import click +from click_help_colors import HelpColorsCommand +from riocli.constants import Colors, Symbols -@click.command() + +@click.command( + 'logout', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.pass_context def logout(ctx: click.Context): """ @@ -30,4 +38,4 @@ def logout(ctx: click.Context): ctx.obj.data.pop('project_id', None) ctx.obj.save() - click.secho('Logged out successfully!', fg='green') + click.secho('{} Logged out successfully.'.format(Symbols.SUCCESS), fg=Colors.GREEN) diff --git a/riocli/auth/refresh_token.py b/riocli/auth/refresh_token.py index f18141d2..fdd594b6 100644 --- a/riocli/auth/refresh_token.py +++ b/riocli/auth/refresh_token.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,24 +12,33 @@ # See the License for the specific language governing permissions and # limitations under the License. import click +from click_help_colors import HelpColorsCommand from riocli.auth.util import get_token +from riocli.constants import Colors, Symbols from riocli.exceptions import LoggedOut -@click.command() +@click.command( + 'refresh-token', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.pass_context def refresh_token(ctx: click.Context): """ - Refreshes the authentication Token after it expires + Refreshes the authentication token after it expires """ - email = ctx.obj.data.get('email_id', None) password = ctx.obj.data.get('password', None) + if not ctx.obj.exists or not email or not password: raise LoggedOut ctx.obj.data['auth_token'] = get_token(email, password) ctx.obj.save() - click.echo('Token refreshed successfully!') + + click.secho('{} Token refreshed successfully!'.format(Symbols.SUCCESS), + fg=Colors.GREEN) diff --git a/riocli/auth/staging.py b/riocli/auth/staging.py index 74698fc2..c206fb24 100644 --- a/riocli/auth/staging.py +++ b/riocli/auth/staging.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ from riocli.auth.login import select_project, select_organization from riocli.auth.util import get_token from riocli.config import Configuration +from riocli.constants import Colors, Symbols from riocli.utils.context import get_root_context _STAGING_ENVIRONMENT_SUBDOMAIN = "apps.okd4v2.okd4beta.rapyuta.io" @@ -29,7 +30,6 @@ def environment(ctx: click.Context, name: str): """ Sets the Rapyuta.io environment to use (Internal use) """ - ctx = get_root_context(ctx) if name == 'ga': @@ -50,18 +50,16 @@ def environment(ctx: click.Context, name: str): organization = select_organization(ctx.obj) select_project(ctx.obj, organization=organization) - ctx.obj.save() - -def _validate_environment(name: str) -> bool: - valid = name in _NAMED_ENVIRONMENTS or name.startswith('pr') - if not valid: - click.secho('Invalid staging environment!', fg='red') - raise SystemExit(1) + ctx.obj.save() def _configure_environment(config: Configuration, name: str) -> None: - _validate_environment(name) + is_valid_env = name in _NAMED_ENVIRONMENTS or name.startswith('pr') + + if not is_valid_env: + click.secho('{} Invalid environment: {}'.format(Symbols.ERROR, name), fg=Colors.RED) + raise SystemExit(1) catalog = 'https://{}catalog.{}'.format(name, _STAGING_ENVIRONMENT_SUBDOMAIN) core = 'https://{}apiserver.{}'.format(name, _STAGING_ENVIRONMENT_SUBDOMAIN) diff --git a/riocli/auth/status.py b/riocli/auth/status.py index 2b1517b9..7844be4e 100644 --- a/riocli/auth/status.py +++ b/riocli/auth/status.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,18 +12,25 @@ # See the License for the specific language governing permissions and # limitations under the License. import click +from click_help_colors import HelpColorsCommand +from riocli.constants import Colors -@click.command() + +@click.command( + 'status', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.pass_context def status(ctx: click.Context): """ - Shows the Login status of the CLI + Shows the login status of the CLI """ - if not ctx.obj.exists: - click.secho('Logged out 🔒', fg='red') + click.secho('🔒You are logged out', fg=Colors.YELLOW) raise SystemExit(1) if 'auth_token' in ctx.obj.data: - click.secho('Logged in 🎉', fg='green') + click.secho('🎉 You are logged in', fg=Colors.GREEN) diff --git a/riocli/auth/token.py b/riocli/auth/token.py index 53a12ddc..c76cb006 100644 --- a/riocli/auth/token.py +++ b/riocli/auth/token.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,13 +12,20 @@ # See the License for the specific language governing permissions and # limitations under the License. import click +from click_help_colors import HelpColorsCommand from riocli.auth.util import get_token, TOKEN_LEVELS from riocli.config import Configuration +from riocli.constants import Colors from riocli.exceptions import LoggedOut -@click.command() +@click.command( + 'token', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.option("--email", default=None, help="Email of the Rapyuta.io account") @click.option("--password", default=None, hide_input=True, help="Password for the Rapyuta.io account") @@ -31,8 +38,9 @@ def token(email: str, password: str, level: int = 0): config = Configuration() if level not in TOKEN_LEVELS: - click.secho('Invalid token level. Valid levels are {0}'.format( - list(TOKEN_LEVELS.keys())), fg='red') + click.secho( + 'Invalid token level. Valid levels are {0}'.format( + list(TOKEN_LEVELS.keys())), fg=Colors.RED) raise SystemExit(1) if not email: @@ -44,4 +52,5 @@ def token(email: str, password: str, level: int = 0): if not config.exists or not email or not password: raise LoggedOut - click.echo(get_token(email, password, level)) + new_token = get_token(email, password) + click.echo(new_token) diff --git a/riocli/auth/util.py b/riocli/auth/util.py index ae007bfa..0abdd366 100644 --- a/riocli/auth/util.py +++ b/riocli/auth/util.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,18 +14,32 @@ import os import click -from click_spinner import spinner from rapyuta_io import Client from rapyuta_io.clients.rip_client import AuthTokenLevel from rapyuta_io.utils import UnauthorizedError from riocli.config import Configuration +from riocli.constants import Colors, Symbols from riocli.project.util import find_project_guid, find_organization_guid from riocli.utils.selector import show_selection +from riocli.utils.spinner import with_spinner + +TOKEN_LEVELS = { + 0: AuthTokenLevel.LOW, + 1: AuthTokenLevel.MED, + 2: AuthTokenLevel.HIGH +} -def select_organization(config: Configuration, - organization: str = None) -> str: +def select_organization( + config: Configuration, + organization: str = None, +) -> str: + """ + Launches the org selection prompt by listing all the orgs that the user is a part of. + + Sets the choice in the given configuration. + """ client = config.new_client(with_project=False) org_guid = None @@ -44,7 +58,7 @@ def select_organization(config: Configuration, org_guid = show_selection(org_map, "Select an organization:") if org_guid and org_guid not in org_map: - click.secho('invalid organization guid', fg='red') + click.secho('invalid organization guid', fg=Colors.RED) raise SystemExit(1) config.data['organization_id'] = org_guid @@ -53,10 +67,14 @@ def select_organization(config: Configuration, return org_guid -def select_project(config: Configuration, project: str = None, - organization: str = None) -> None: +def select_project( + config: Configuration, + project: str = None, + organization: str = None, +) -> None: """ Launches the project selection prompt by listing all the projects. + Sets the choice in the given configuration. """ client = config.new_v2_client(with_project=False) @@ -71,8 +89,8 @@ def select_project(config: Configuration, project: str = None, if len(projects) == 0: config.data['project_id'] = "" config.data['project_name'] = "" - click.secho("There are no projects in this organization", fg='black', - bg='white') + click.secho("There are no projects in this organization", + fg=Colors.BLACK, bg=Colors.WHITE) return # Sort projects based on their names for an easier selection @@ -93,17 +111,16 @@ def select_project(config: Configuration, project: str = None, config.data['project_name'], config.data['organization_name'], ) - click.secho(confirmation, fg='green') + click.secho(confirmation, fg=Colors.GREEN) -TOKEN_LEVELS = { - 0: AuthTokenLevel.LOW, - 1: AuthTokenLevel.MED, - 2: AuthTokenLevel.HIGH -} - - -def get_token(email: str, password: str, level: int = 1) -> str: +@with_spinner(text='Fetching token...') +def get_token( + email: str, + password: str, + level: int = 1, + spinner=None, +) -> str: """ Generates a new token using email and password. """ @@ -112,19 +129,21 @@ def get_token(email: str, password: str, level: int = 1) -> str: os.environ['RIO_CONFIG'] = config.filepath try: - with spinner(): - token = Client.get_auth_token( - email, password, TOKEN_LEVELS[level]) + token = Client.get_auth_token( + email, password, TOKEN_LEVELS[level]) return token - except UnauthorizedError: - click.secho("✘ incorrect email/password", fg='red') - raise SystemExit(1) + except UnauthorizedError as e: + spinner.text = click.style("incorrect email/password", fg=Colors.RED) + spinner.red.fail(Symbols.ERROR) + raise SystemExit(1) from e except Exception as e: - click.secho(e, fg='red') - raise SystemExit(1) + click.style(str(e), fg=Colors.RED) + spinner.red.fail(Symbols.ERROR) + raise SystemExit(1) from e -def validate_token(token: str) -> bool: +@with_spinner(text='Validating token...') +def validate_token(token: str, spinner=None) -> bool: """Validates an auth token.""" config = Configuration() if 'environment' in config.data: @@ -134,12 +153,16 @@ def validate_token(token: str) -> bool: try: user = client.get_authenticated_user() - click.secho('Token belongs to user {}'.format(user.email_id), - fg='cyan') + spinner.text = click.style( + 'Token belongs to user {}'.format(user.email_id), + fg=Colors.CYAN) + spinner.ok(Symbols.INFO) return True except UnauthorizedError: - click.secho("✘ incorrect auth token", fg='red') + spinner.text = click.style("incorrect auth token", fg=Colors.RED) + spinner.red.fail(Symbols.ERROR) return False except Exception as e: - click.secho(e, fg='red') + spinner.text = click.style(str(e), fg=Colors.RED) + spinner.red.fail(Symbols.ERROR) return False diff --git a/riocli/bootstrap.py b/riocli/bootstrap.py index f0fbd510..dfebd07b 100644 --- a/riocli/bootstrap.py +++ b/riocli/bootstrap.py @@ -28,6 +28,7 @@ from riocli.chart import chart from riocli.completion import completion from riocli.config import Configuration +from riocli.constants import Colors, Symbols from riocli.deployment import deployment from riocli.device import device from riocli.disk import disk @@ -41,6 +42,13 @@ from riocli.secret import secret from riocli.shell import shell, deprecated_repl from riocli.static_route import static_route +from riocli.usergroup import usergroup +from riocli.utils import ( + check_for_updates, + pip_install_cli, + is_pip_installation, + update_appimage, +) from riocli.vpn import vpn @@ -48,8 +56,8 @@ @click.group( invoke_without_command=False, cls=HelpColorsGroup, - help_headers_color="yellow", - help_options_color="green", + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, ) @click.pass_context def cli(ctx: Context, config: str = None): @@ -74,6 +82,38 @@ def version(): return +@cli.command('update') +@click.option('-f', '--force', '--silent', 'silent', is_flag=True, + type=click.BOOL, default=False, + help="Skip confirmation") +def update(silent: bool) -> None: + """ + Update the CLI to the latest version + """ + available, latest = check_for_updates(__version__) + if not available: + click.secho('🎉 You are using the latest version', fg=Colors.GREEN) + return + + click.secho('🎉 A newer version ({}) is available.'.format(latest), + fg=Colors.GREEN) + + if not silent: + click.confirm('Do you want to update?', abort=True, default=False) + + try: + if is_pip_installation(): + pip_install_cli(version=latest) + else: + update_appimage(version=latest) + except Exception as e: + click.secho('{} Failed to update the CLI'.format(e), fg=Colors.RED) + raise SystemExit(1) from e + + click.secho('{} Update successful!'.format(Symbols.SUCCESS), + fg=Colors.GREEN) + + cli.add_command(apply) cli.add_command(chart) cli.add_command(explain) @@ -97,3 +137,4 @@ def version(): cli.add_command(template) cli.add_command(organization) cli.add_command(vpn) +cli.add_command(usergroup) diff --git a/riocli/build/model.py b/riocli/build/model.py index 962fffba..5ef3714d 100644 --- a/riocli/build/model.py +++ b/riocli/build/model.py @@ -32,7 +32,7 @@ def find_object(self, client: Client) -> bool: return obj - def create_object(self, client: Client) -> v1Build: + def create_object(self, client: Client, **kwargs) -> v1Build: build = client.create_build(build=self.to_v1()) return build diff --git a/riocli/chart/__init__.py b/riocli/chart/__init__.py index f5a0f9c5..72e93fd8 100644 --- a/riocli/chart/__init__.py +++ b/riocli/chart/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2022 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,13 +19,14 @@ from riocli.chart.info import info_chart from riocli.chart.list import list_charts from riocli.chart.search import search_chart +from riocli.constants import Colors @click.group( invoke_without_command=False, cls=HelpColorsGroup, - help_headers_color='yellow', - help_options_color='green', + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, ) def chart() -> None: """ diff --git a/riocli/chart/apply.py b/riocli/chart/apply.py index ba0743c0..455cd4b0 100644 --- a/riocli/chart/apply.py +++ b/riocli/chart/apply.py @@ -1,4 +1,4 @@ -# Copyright 2022 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,13 +17,14 @@ from riocli.chart.chart import Chart from riocli.chart.util import find_chart +from riocli.constants import Colors @click.command( 'apply', cls=HelpColorsCommand, - help_headers_color='yellow', - help_options_color='green', + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, help='Apply a new Rapyuta Chart in the Project', ) @click.option('--dryrun', '-d', is_flag=True, default=False, @@ -54,7 +55,7 @@ def apply_chart( if len(versions) > 1: click.secho( 'More than one charts are available, please specify the version!', - fg='red') + fg=Colors.RED) chart = Chart(**versions[0]) chart.apply_chart(values, secrets, dryrun=dryrun, workers=workers, diff --git a/riocli/chart/chart.py b/riocli/chart/chart.py index d6496952..b6381cf1 100644 --- a/riocli/chart/chart.py +++ b/riocli/chart/chart.py @@ -1,4 +1,4 @@ -# Copyright 2022 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ from munch import Munch from riocli.apply import apply, delete +from riocli.constants import Colors class Chart(Munch): @@ -66,7 +67,7 @@ def delete_chart( def download_chart(self): self._create_temp_directory() click.secho('Downloading {}:{} chart in {}'.format( - self.name, self.version, self.tmp_dir.name), fg='cyan') + self.name, self.version, self.tmp_dir.name), fg=Colors.CYAN) chart_filepath = Path(self.tmp_dir.name, self._chart_filename()) with open(chart_filepath, 'wb') as f: diff --git a/riocli/chart/delete.py b/riocli/chart/delete.py index 26847291..c3ba804f 100644 --- a/riocli/chart/delete.py +++ b/riocli/chart/delete.py @@ -1,4 +1,4 @@ -# Copyright 2022 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,13 +16,14 @@ from riocli.chart.chart import Chart from riocli.chart.util import find_chart +from riocli.constants import Colors @click.command( 'delete', cls=HelpColorsCommand, - help_headers_color='yellow', - help_options_color='green', + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, help='Delete the Rapyuta Chart from the Project', ) @click.option('--dryrun', '-d', is_flag=True, default=False, @@ -47,7 +48,7 @@ def delete_chart( versions = find_chart(chart) if len(versions) > 1: click.secho('More than one charts are available, ' - 'please specify the version!', fg='yellow') + 'please specify the version!', fg=Colors.YELLOW) chart = Chart(**versions[0]) chart.delete_chart(values=values, secrets=secrets, diff --git a/riocli/chart/info.py b/riocli/chart/info.py index 89d29f63..492a8c55 100644 --- a/riocli/chart/info.py +++ b/riocli/chart/info.py @@ -1,4 +1,4 @@ -# Copyright 2022 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,14 +16,15 @@ from click_help_colors import HelpColorsCommand from riocli.chart.util import find_chart +from riocli.constants import Colors from riocli.utils import dump_all_yaml @click.command( 'info', cls=HelpColorsCommand, - help_headers_color='yellow', - help_options_color='green', + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, help='Describe the available chart with versions', ) @click.argument('chart', type=str) diff --git a/riocli/chart/list.py b/riocli/chart/list.py index 682e3038..1f55a910 100644 --- a/riocli/chart/list.py +++ b/riocli/chart/list.py @@ -1,4 +1,4 @@ -# Copyright 2022 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,13 +17,14 @@ from munch import munchify from riocli.chart.util import fetch_index, print_chart_entries +from riocli.constants import Colors @click.command( 'list', cls=HelpColorsCommand, - help_headers_color='yellow', - help_options_color='green', + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, ) @click.option('-w', '--wide', is_flag=True, default=False, help='Print more details') diff --git a/riocli/chart/search.py b/riocli/chart/search.py index 4e3d194f..d8d747f9 100644 --- a/riocli/chart/search.py +++ b/riocli/chart/search.py @@ -1,4 +1,4 @@ -# Copyright 2022 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,21 +14,32 @@ import click from click_help_colors import HelpColorsCommand +from yaspin.api import Yaspin from riocli.chart.util import find_chart, print_chart_entries +from riocli.constants import Colors, Symbols +from riocli.utils.spinner import with_spinner @click.command( 'search', cls=HelpColorsCommand, - help_headers_color='yellow', - help_options_color='green', + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, help='Search for available charts in the repository', ) @click.option('-w', '--wide', is_flag=True, default=False, help='Print more details') @click.argument('chart', type=str) -def search_chart(chart: str, wide: bool = False) -> None: +@with_spinner(text="Searching for chart...") +def search_chart(chart: str, wide: bool = False, + spinner: Yaspin = None) -> None: """Search for a chart in the chart repo.""" - versions = find_chart(chart) - print_chart_entries(versions, wide=wide) + try: + versions = find_chart(chart) + with spinner.hidden(): + print_chart_entries(versions, wide=wide) + except Exception as e: + spinner.text = click.style(str(e), fg=Colors.RED) + spinner.red.fail(Symbols.ERROR) + raise SystemExit(1) from e diff --git a/riocli/deployment/run.py b/riocli/constants/__init__.py similarity index 61% rename from riocli/deployment/run.py rename to riocli/constants/__init__.py index 7f5351dd..67fb31a4 100644 --- a/riocli/deployment/run.py +++ b/riocli/constants/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,12 +11,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import click +from riocli.constants.colors import Colors +from riocli.constants.symbols import Symbols -@click.command('run', hidden=True) -@click.argument('docker-image', type=str) -def run_deployment(docker_image: str) -> None: - # TODO(ankit): Implement `kubectl run` like command to instantly create a Deployment. - # Possibly implement --interactive --tty to SSH into the session - pass +__all__ = [Colors, Symbols] diff --git a/riocli/constants/colors.py b/riocli/constants/colors.py new file mode 100644 index 00000000..e2be344e --- /dev/null +++ b/riocli/constants/colors.py @@ -0,0 +1,41 @@ +# Copyright 2023 Rapyuta Robotics +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from enum import Enum + + +class Colors(str, Enum): + """ + Colors is a str enum based on the colors supported by click. + https://github.com/pallets/click/blob/main/examples/colors/colors.py + """ + def __str__(self): + return str(self.value).lower() + + BLACK = 'black' + RED = 'red' + GREEN = 'green' + YELLOW = 'yellow' + BLUE = 'blue' + MAGENTA = 'magenta' + CYAN = 'cyan' + WHITE = 'white' + BRIGHT_BLACK = 'bright_black' + BRIGHT_RED = 'bright_red' + BRIGHT_GREEN = 'bright_green' + BRIGHT_YELLOW = 'bright_yellow' + BRIGHT_BLUE = 'bright_blue' + BRIGHT_MAGENTA = 'bright_magenta' + BRIGHT_CYAN = 'bright_cyan' + BRIGHT_WHITE = 'bright_white' diff --git a/riocli/constants/symbols.py b/riocli/constants/symbols.py new file mode 100644 index 00000000..3bc16ed5 --- /dev/null +++ b/riocli/constants/symbols.py @@ -0,0 +1,20 @@ +# Copyright 2023 Rapyuta Robotics +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +class Symbols: + INFO = 'ℹ️' + ERROR = '❌' + SUCCESS = '✅' + WARNING = '⚠️' + WAITING = '⏳' diff --git a/riocli/deployment/__init__.py b/riocli/deployment/__init__.py index f7d35fd9..be06553d 100644 --- a/riocli/deployment/__init__.py +++ b/riocli/deployment/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,21 +14,24 @@ import click from click_help_colors import HelpColorsGroup +from riocli.constants import Colors from riocli.deployment.delete import delete_deployment +from riocli.deployment.execute import execute_command +from riocli.deployment.execute import execute_command from riocli.deployment.inspect import inspect_deployment from riocli.deployment.list import list_deployments from riocli.deployment.logs import deployment_logs from riocli.deployment.ssh import ssh_init, ssh_deployment from riocli.deployment.status import status +from riocli.deployment.update import update_deployment from riocli.deployment.wait import wait_for_deployment -from riocli.deployment.execute import execute_command @click.group( invoke_without_command=False, cls=HelpColorsGroup, - help_headers_color='yellow', - help_options_color='green', + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, ) def deployment(): """ @@ -46,3 +49,4 @@ def deployment(): deployment.add_command(ssh_deployment) deployment.add_command(ssh_init) deployment.add_command(execute_command) +deployment.add_command(update_deployment) diff --git a/riocli/deployment/delete.py b/riocli/deployment/delete.py index 55759f2e..37839961 100644 --- a/riocli/deployment/delete.py +++ b/riocli/deployment/delete.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,30 +12,49 @@ # See the License for the specific language governing permissions and # limitations under the License. import click -from click_spinner import spinner +from click_help_colors import HelpColorsCommand from riocli.config import new_client +from riocli.constants import Colors, Symbols from riocli.deployment.util import name_to_guid +from riocli.utils.spinner import with_spinner -@click.command('delete') +@click.command( + 'delete', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.option('--force', '-f', '--silent', is_flag=True, default=False, help='Skip confirmation') @click.argument('deployment-name', type=str) @name_to_guid -def delete_deployment(force: bool, deployment_name: str, deployment_guid: str) -> None: +@with_spinner(text="Deleting deployment...") +def delete_deployment( + force: bool, + deployment_name: str, + deployment_guid: str, + spinner=None, +) -> None: """ - Delete the deployment from the Platform + Deletes a deployment """ - if not force: - click.confirm('Deleting {} ({}) deployment'.format(deployment_name, deployment_guid), abort=True) + with spinner.hidden(): + if not force: + click.confirm( + 'Deleting {} ({}) deployment'.format( + deployment_name, deployment_guid), abort=True) try: - with spinner(): - client = new_client() - deployment = client.get_deployment(deployment_guid) - deployment.deprovision() - click.secho('Deployment deleted successfully!', fg='green') + client = new_client() + deployment = client.get_deployment(deployment_guid) + deployment.deprovision() + spinner.text = click.style( + 'Deployment deleted successfully.', fg=Colors.GREEN) + spinner.green.ok(Symbols.SUCCESS) except Exception as e: - click.secho(str(e), fg='red') + spinner.text = click.style( + 'Failed to delete deployment: {}'.format(e), Colors.RED) + spinner.red.fail(Symbols.ERROR) raise SystemExit(1) diff --git a/riocli/deployment/execute.py b/riocli/deployment/execute.py index 8b801404..f484ba85 100644 --- a/riocli/deployment/execute.py +++ b/riocli/deployment/execute.py @@ -11,32 +11,51 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import os -import click import typing +import click +from click_help_colors import HelpColorsCommand +from yaspin import kbi_safe_yaspin + +from riocli.constants import Colors from riocli.deployment.util import name_to_guid, select_details from riocli.utils.execute import run_on_cloud -@click.command('execute') -@click.option('--component', 'component_name', default=None) -@click.option('--exec', 'exec_name', default=None) +@click.command( + 'execute', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) +@click.option('--component', 'component_name', default=None, + help='Name of the component in the deployment') +@click.option('--exec', 'exec_name', default=None, + help='Name of a executable in the component') @click.argument('deployment-name', type=str) @click.argument('command', nargs=-1) @name_to_guid -def execute_command(component_name: str, exec_name: str, deployment_name: str, deployment_guid: str, command: typing.List[str]) -> None: +def execute_command( + component_name: str, + exec_name: str, + deployment_name: str, + deployment_guid: str, + command: typing.List[str] +) -> None: """ Execute commands on cloud deployment """ try: comp_id, exec_id, pod_name = select_details(deployment_guid, component_name, exec_name) - stdout, stderr = run_on_cloud(deployment_guid, comp_id, exec_id, pod_name, command) + + with kbi_safe_yaspin(text='Executing command `{}`...'.format(command)) as spinner: + stdout, stderr = run_on_cloud(deployment_guid, comp_id, exec_id, pod_name, command) + if stderr: - click.secho(stderr, fg='red') + click.secho(stderr, fg=Colors.RED) if stdout: - click.secho(stdout, fg='yellow') + click.secho(stdout, fg=Colors.YELLOW) except Exception as e: - click.secho(e, fg='red') - raise SystemExit(1) \ No newline at end of file + click.secho(e, fg=Colors.RED) + raise SystemExit(1) diff --git a/riocli/deployment/inspect.py b/riocli/deployment/inspect.py index bc18c482..8dd8d0c7 100644 --- a/riocli/deployment/inspect.py +++ b/riocli/deployment/inspect.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,19 +12,30 @@ # See the License for the specific language governing permissions and # limitations under the License. import click +from click_help_colors import HelpColorsCommand from rapyuta_io.clients.deployment import Deployment from riocli.config import new_client +from riocli.constants import Colors from riocli.deployment.util import name_to_guid from riocli.utils import inspect_with_format -@click.command('inspect') +@click.command( + 'inspect', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.option('--format', '-f', 'format_type', default='yaml', type=click.Choice(['json', 'yaml'], case_sensitive=False)) @click.argument('deployment-name') @name_to_guid -def inspect_deployment(format_type: str, deployment_name: str, deployment_guid: str) -> None: +def inspect_deployment( + format_type: str, + deployment_name: str, + deployment_guid: str, +) -> None: """ Inspect the deployment resource """ @@ -34,7 +45,7 @@ def inspect_deployment(format_type: str, deployment_name: str, deployment_guid: data = make_deployment_inspectable(deployment) inspect_with_format(data, format_type) except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg=Colors.RED) raise SystemExit(1) diff --git a/riocli/deployment/list.py b/riocli/deployment/list.py index fa06d11d..8f539074 100644 --- a/riocli/deployment/list.py +++ b/riocli/deployment/list.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,19 +14,41 @@ import typing import click -from rapyuta_io.clients.deployment import Deployment +from click_help_colors import HelpColorsCommand +from rapyuta_io.clients.deployment import Deployment, DeploymentPhaseConstants from riocli.config import new_client +from riocli.constants import Colors from riocli.utils import tabulate_data +ALL_PHASES = [ + DeploymentPhaseConstants.INPROGRESS, + DeploymentPhaseConstants.PROVISIONING, + DeploymentPhaseConstants.SUCCEEDED, + DeploymentPhaseConstants.FAILED_TO_START, + DeploymentPhaseConstants.PARTIALLY_DEPROVISIONED, + DeploymentPhaseConstants.DEPLOYMENT_STOPPED, +] -@click.command('list') +DEFAULT_PHASES = [ + DeploymentPhaseConstants.INPROGRESS, + DeploymentPhaseConstants.PROVISIONING, + DeploymentPhaseConstants.SUCCEEDED, + DeploymentPhaseConstants.FAILED_TO_START, +] + + +@click.command( + 'list', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.option('--device', prompt_required=False, default='', type=str, help='Filter the Deployment list by Device ID') @click.option('--phase', prompt_required=False, multiple=True, - type=click.Choice(['In progress', 'Provisioning', 'Succeeded', 'Failed to start', - 'Partially deprovisioned', 'Deployment stopped']), - default=['In progress', 'Provisioning', 'Succeeded', 'Failed to start'], + type=click.Choice(ALL_PHASES), + default=DEFAULT_PHASES, help='Filter the Deployment list by Phases') def list_deployments(device: str, phase: typing.List[str]) -> None: """ @@ -38,7 +60,7 @@ def list_deployments(device: str, phase: typing.List[str]) -> None: deployments = sorted(deployments, key=lambda d: d.name.lower()) display_deployment_list(deployments, show_header=True) except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg=Colors.RED) raise SystemExit(1) diff --git a/riocli/deployment/logs.py b/riocli/deployment/logs.py index 4113c135..46704122 100644 --- a/riocli/deployment/logs.py +++ b/riocli/deployment/logs.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,27 +14,41 @@ import os import click +from click_help_colors import HelpColorsCommand from riocli.config import Configuration +from riocli.constants import Colors from riocli.deployment.util import name_to_guid, select_details _LOG_URL_FORMAT = '{}/deployment/logstream?tailLines={}&deploymentId={}&componentId={}&executableId={}&podName={}' -@click.command('logs') -@click.option('--component', 'component_name', default=None) -@click.option('--exec', 'exec_name', default=None) +@click.command( + 'logs', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) +@click.option('--component', 'component_name', default=None, + help='Name of the component in the deployment') +@click.option('--exec', 'exec_name', default=None, + help='Name of a executable in the component') @click.argument('deployment-name', type=str) @name_to_guid -def deployment_logs(component_name: str, exec_name: str, deployment_name: str, deployment_guid: str) -> None: +def deployment_logs( + component_name: str, + exec_name: str, + deployment_name: str, + deployment_guid: str, +) -> None: """ - Stream the live logs for the Deployment (only Cloud) + Stream live logs from cloud deployments (not supported for device deployments) """ try: comp_id, exec_id, pod_name = select_details(deployment_guid, component_name, exec_name) stream_deployment_logs(deployment_guid, comp_id, exec_id, pod_name) except Exception as e: - click.secho(e, fg='red') + click.secho(e, fg=Colors.RED) raise SystemExit(1) @@ -42,10 +56,13 @@ def stream_deployment_logs(deployment_id, component_id, exec_id, pod_name=None): # FIXME(ankit): The Upstream API ends up timing out when there is no log being written. # IMO the correct behaviour should be to not timeout and keep the stream open. config = Configuration() + url = get_log_stream_url(config, deployment_id, component_id, exec_id, pod_name) auth = config.get_auth_header() - curl = 'curl -H "project: {}" -H "Authorization: {}" "{}"'.format(auth['project'], auth['Authorization'], url) - click.secho(curl, fg='blue') + curl = 'curl -H "project: {}" -H "Authorization: {}" "{}"'.format( + auth['project'], auth['Authorization'], url) + click.echo(click.style(curl, fg=Colors.BLUE, italic=True)) + os.system(curl) diff --git a/riocli/deployment/model.py b/riocli/deployment/model.py index 25e67771..a6c48e90 100644 --- a/riocli/deployment/model.py +++ b/riocli/deployment/model.py @@ -1,4 +1,4 @@ -# Copyright 2022 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,7 +17,7 @@ import click from rapyuta_io import Client from rapyuta_io.clients.catalog_client import Package -from rapyuta_io.clients.deployment import DeploymentNotRunningException +from rapyuta_io.clients.deployment import DeploymentNotRunningException, DeploymentPhaseConstants from rapyuta_io.clients.native_network import NativeNetwork from rapyuta_io.clients.package import ProvisionConfiguration, RestartPolicy, \ ExecutableMount @@ -27,6 +27,7 @@ ROSBagUploadTypes, OverrideOptions, TopicOverrideInfo) from rapyuta_io.clients.routed_network import RoutedNetwork +from riocli.constants import Colors from riocli.deployment.errors import ERRORS from riocli.deployment.util import add_mount_volume_provision_config from riocli.jsonschema.validate import load_schema @@ -43,11 +44,14 @@ class Deployment(Model): } def find_object(self, client: Client) -> typing.Any: - guid, obj = self.rc.find_depends( - {"kind": "deployment", "nameOrGUID": self.metadata.name}) + guid, obj = self.rc.find_depends({ + "kind": "deployment", + "nameOrGUID": self.metadata.name, + }) + return obj if guid else False - def create_object(self, client: Client) -> typing.Any: + def create_object(self, client: Client, **kwargs) -> typing.Any: pkg_guid, pkg = self.rc.find_depends(self.metadata.depends, self.metadata.depends.version) @@ -63,13 +67,13 @@ def create_object(self, client: Client) -> typing.Any: executables = component['executables'] runtime = internal_component['runtime'] + retry_count = int(kwargs.get('retry_count')) + retry_interval = int(kwargs.get('retry_interval')) + if 'runtime' in self.spec and runtime != self.spec.runtime: - click.secho( - '>> runtime mismatch => ' + - 'deployment:{}.runtime !== package:{}.runtime '.format( + raise Exception('>> runtime mismatch => deployment:{}.runtime !== package:{}.runtime '.format( self.metadata.name, pkg['packageName'] - ), fg='red') - return + )) provision_config = pkg.get_provision_configuration(plan_id) @@ -90,7 +94,9 @@ def create_object(self, client: Client) -> typing.Any: dep_guid, dep = self.rc.find_depends(item) if dep is None and dep_guid: dep = client.get_deployment(dep_guid) - provision_config.add_dependent_deployment(dep) + provision_config.add_dependent_deployment(dep, ready_phases=[ + DeploymentPhaseConstants.PROVISIONING.value, + DeploymentPhaseConstants.SUCCEEDED.value]) # Add Network if 'rosNetworks' in self.spec: @@ -217,7 +223,9 @@ def create_object(self, client: Client) -> typing.Any: deployment = pkg.provision(self.metadata.name, provision_config) try: - deployment.poll_deployment_till_ready() + deployment.poll_deployment_till_ready(retry_count=retry_count, sleep_interval=retry_interval, + ready_phases=[DeploymentPhaseConstants.PROVISIONING.value, + DeploymentPhaseConstants.SUCCEEDED.value]) except DeploymentNotRunningException as e: raise Exception(process_deployment_errors(e)) from e except Exception as e: @@ -392,9 +400,9 @@ def process_deployment_errors(e: DeploymentNotRunningException): description = 'Internal rapyuta.io error' action = support_action - code = click.style(code, fg='yellow') - description = click.style(description, fg='red') - action = click.style(action, fg='green') + code = click.style(code, fg=Colors.YELLOW) + description = click.style(description, fg=Colors.RED) + action = click.style(action, fg=Colors.GREEN) msgs.append(err_fmt.format(code, description, action)) diff --git a/riocli/deployment/status.py b/riocli/deployment/status.py index f883484e..cd4cb93c 100644 --- a/riocli/deployment/status.py +++ b/riocli/deployment/status.py @@ -12,12 +12,19 @@ # See the License for the specific language governing permissions and # limitations under the License. import click +from click_help_colors import HelpColorsCommand from riocli.config import new_client +from riocli.constants import Colors from riocli.deployment.util import name_to_guid -@click.command('status') +@click.command( + 'status', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.argument('deployment-name', type=str) @name_to_guid def status(deployment_name: str, deployment_guid: str) -> None: @@ -29,5 +36,5 @@ def status(deployment_name: str, deployment_guid: str) -> None: deployment = client.get_deployment(deployment_guid) click.secho(deployment.status) except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg=Colors.RED) raise SystemExit(1) diff --git a/riocli/deployment/update.py b/riocli/deployment/update.py new file mode 100644 index 00000000..e16da2bf --- /dev/null +++ b/riocli/deployment/update.py @@ -0,0 +1,199 @@ +# Copyright 2023 Rapyuta Robotics +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import functools +import re +from concurrent.futures import ThreadPoolExecutor +from queue import Queue +from typing import List + +import click +from click_help_colors import HelpColorsCommand +from rapyuta_io import Client, DeploymentPhaseConstants +from rapyuta_io.clients.deployment import Deployment +from yaspin.api import Yaspin + +from riocli.config import new_client +from riocli.constants import Symbols, Colors +from riocli.utils import tabulate_data +from riocli.utils.spinner import with_spinner + + +@click.command( + 'update', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) +@click.option('--force', '-f', '--silent', is_flag=True, default=False, + help='Skip confirmation') +@click.option('--update-all', '-a', is_flag=True, default=False, + help='Updates all deployments') +@click.option('--workers', '-w', + help="number of parallel workers while running update deployment " + "command. defaults to 10.", type=int, default=10) +@click.argument('deployment-name-or-regex', type=str, default="") +@with_spinner(text="Updating...") +def update_deployment( + force: bool, + update_all: bool, + workers: int, + deployment_name_or_regex: str, + spinner: Yaspin = None, +) -> None: + """ + Updates one more deployments + """ + client = new_client() + if not (deployment_name_or_regex or update_all): + spinner.text = "Nothing to update" + spinner.green.ok(Symbols.SUCCESS) + return + + try: + deployments = fetch_deployments( + client, deployment_name_or_regex, update_all) + except Exception as e: + spinner.text = click.style( + 'Failed to update deployment(s): {}'.format(e), Colors.RED) + spinner.red.fail(Symbols.ERROR) + raise SystemExit(1) from e + + if not deployments: + spinner.text = "Nothing to update" + spinner.ok(Symbols.SUCCESS) + return + + headers = ['Name', 'GUID', 'Phase', 'Status'] + data = [[d.name, d.deploymentId, d.phase, d.status] for d in deployments] + + with spinner.hidden(): + tabulate_data(data, headers) + + spinner.write('') + + if not force: + with spinner.hidden(): + click.confirm('Do you want to update above deployment(s)?', + default=True, abort=True) + spinner.write('') + + try: + result = Queue() + func = functools.partial(_apply_update, client, result) + with ThreadPoolExecutor(max_workers=workers) as executor: + executor.map(func, deployments) + + result = sorted(list(result.queue), key=lambda x: x[0]) + + data, fg, statuses = [], Colors.GREEN, [] + for name, status in result: + fg = Colors.GREEN if status else Colors.RED + icon = Symbols.SUCCESS if status else Symbols.ERROR + statuses.append(status) + data.append([ + click.style(name, fg), + click.style(icon, fg) + ]) + + with spinner.hidden(): + tabulate_data(data, headers=['Name', 'Status']) + + icon = Symbols.SUCCESS if all(statuses) else Symbols.WARNING + fg = Colors.GREEN if all(statuses) else Colors.YELLOW + text = "successfully" if all(statuses) else "partially" + + spinner.write('') + spinner.text = click.style( + 'Deployment(s) updated {}.'.format(text), fg) + spinner.ok(click.style(icon, fg)) + except Exception as e: + spinner.text = click.style( + 'Failed to update deployment(s): {}'.format(e), Colors.RED) + spinner.red.fail(Symbols.ERROR) + raise SystemExit(1) from e + + +def fetch_deployments( + client: Client, + deployment_name_or_regex: str, + update_all: bool, +) -> List[Deployment]: + deployments = client.get_all_deployments( + phases=[DeploymentPhaseConstants.SUCCEEDED, + DeploymentPhaseConstants.PROVISIONING]) + result = [] + for deployment in deployments: + if (update_all or deployment.name == deployment_name_or_regex or + (deployment_name_or_regex not in deployment.name and + re.search(deployment_name_or_regex, deployment.name))): + result.append(deployment) + + return result + + +def get_component_context(component_info) -> dict: + result = {} + + for component in component_info: + comp = {} + executables = [] + exec_metadata = component.get("executableMetaData", []) or [] + + for e in exec_metadata: + # Component will be considered only if any of its executables is + # docker or build + if not (e.get("docker") or e.get("buildGUID")): + continue + + executable = {} + + if e.get("buildGUID"): + executable["buildGUID"] = e["buildGUID"] + + if e.get("docker"): + executable["docker"] = e["docker"] + + executable["id"] = e.get("id", "") + executable["name"] = e.get("name", "") + executables.append(executable) + + if len(executables) > 0: + result[component["componentID"]] = comp + comp["component"] = {"executables": executables} + comp["update_deployment"] = True + + return result + + +def _apply_update( + client: Client, + result: Queue, + deployment: Deployment, +) -> None: + try: + dep = client.get_deployment(deployment['deploymentId']) + component_context = get_component_context(dep.get("componentInfo", {})) + payload = { + "service_id": dep["packageId"], + "plan_id": dep["planId"], + "deployment_id": dep["deploymentId"], + "context": { + "component_context": component_context + } + } + client.update_deployment(payload) + result.put((deployment["name"], True)) + except Exception: + result.put((deployment["name"], False)) diff --git a/riocli/deployment/util.py b/riocli/deployment/util.py index 01bf3c9c..282a84f7 100644 --- a/riocli/deployment/util.py +++ b/riocli/deployment/util.py @@ -23,6 +23,7 @@ from rapyuta_io.utils.constants import DEVICE_ID from riocli.config import new_client +from riocli.constants import Colors from riocli.utils.selector import show_selection @@ -32,8 +33,8 @@ def decorated(**kwargs: typing.Any) -> None: try: client = new_client() except Exception as e: - click.secho(str(e), fg='red') - raise SystemExit(1) + click.secho(str(e), fg=Colors.RED) + raise SystemExit(1) from e name = kwargs.pop('deployment_name') guid = None @@ -42,11 +43,15 @@ def decorated(**kwargs: typing.Any) -> None: guid = name name = None - if name is None: - name = get_deployment_name(client, guid) + try: + if name is None: + name = get_deployment_name(client, guid) - if guid is None: - guid = find_deployment_guid(client, name) + if guid is None: + guid = find_deployment_guid(client, name) + except Exception as e: + click.secho(str(e), fg=Colors.RED) + raise SystemExit(1) from e kwargs['deployment_name'] = name kwargs['deployment_guid'] = guid @@ -122,8 +127,6 @@ def add_mount_volume_provision_config(provision_config, component_name, device, isinstance(mount, ExecutableMount) for mount in executable_mounts): raise InvalidParameterException( 'executable_mounts must be a list of rapyuta_io.clients.package.ExecutableMount') - if not device.is_online(): - raise OperationNotAllowedError('Device should be online') if device.get_runtime() != Device.DOCKER_COMPOSE and not device.is_docker_enabled(): raise OperationNotAllowedError('Device must be a {} device'.format(Device.DOCKER_COMPOSE)) component_params = provision_config.parameters.get(component_id) diff --git a/riocli/deployment/wait.py b/riocli/deployment/wait.py index 011a0f0e..b02de14f 100644 --- a/riocli/deployment/wait.py +++ b/riocli/deployment/wait.py @@ -12,36 +12,52 @@ # See the License for the specific language governing permissions and # limitations under the License. import click -from click_spinner import spinner +from click_help_colors import HelpColorsCommand from rapyuta_io.utils import RetriesExhausted, DeploymentNotRunningException from riocli.config import new_client +from riocli.constants import Colors +from riocli.constants import Symbols from riocli.deployment.util import name_to_guid +from riocli.utils.spinner import with_spinner -@click.command('wait') +@click.command( + 'wait', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.argument('deployment-name', type=str) @name_to_guid -def wait_for_deployment(deployment_name: str, deployment_guid: str) -> None: +@with_spinner(text="Waiting for deployment...", timer=True) +def wait_for_deployment( + deployment_name: str, + deployment_guid: str, + spinner=None, +) -> None: """ - Wait until the Deployment succeeds/fails + Wait until the deployment succeeds/fails """ try: client = new_client() - with spinner(): - deployment = client.get_deployment(deployment_guid) - # TODO(ankit): Fix the poll_deployment_till_ready for Runtime Error - status = deployment.poll_deployment_till_ready() - click.secho('Deployment status: {}'.format(status.status)) + deployment = client.get_deployment(deployment_guid) + # TODO(ankit): Fix the poll_deployment_till_ready for Runtime Error + status = deployment.poll_deployment_till_ready() + spinner.text = click.style('Deployment status: {}'.format(status.status), fg=Colors.GREEN) + spinner.green.ok(Symbols.SUCCESS) except RetriesExhausted as e: - click.secho(str(e), fg='red') - click.secho('Retry Again?', fg='red') + spinner.write(click.style(str(e), fg=Colors.RED)) + spinner.text = click.style('Try again?', Colors.RED) + spinner.red.fail(Symbols.ERROR) except DeploymentNotRunningException as e: if 'DEP_E151' in e.deployment_status.errors: - click.secho('Device is either offline or not reachable', fg='red') + spinner.text = click.style('Device is either offline or not reachable', fg=Colors.RED) else: - click.secho(str(e), fg='red') + spinner.text = click.style(str(e), fg=Colors.RED) + spinner.red.fail(Symbols.ERROR) raise SystemExit(1) except Exception as e: - click.secho(str(e), fg='red') + spinner.text = click.style(str(e), fg=Colors.RED) + spinner.red.fail(Symbols.ERROR) raise SystemExit(1) diff --git a/riocli/device/config.py b/riocli/device/config.py index 8b0c4c8a..09d9cf21 100644 --- a/riocli/device/config.py +++ b/riocli/device/config.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,21 +14,23 @@ import typing import click +from click_help_colors import HelpColorsCommand from click_help_colors import HelpColorsGroup -from click_spinner import spinner from rapyuta_io import DeviceConfig from riocli.config import new_client +from riocli.constants import Colors, Symbols from riocli.device.util import name_to_guid from riocli.utils import tabulate_data +from riocli.utils.spinner import with_spinner @click.group( 'config', invoke_without_command=False, cls=HelpColorsGroup, - help_headers_color='yellow', - help_options_color='green', + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, ) def device_config() -> None: """ @@ -37,7 +39,12 @@ def device_config() -> None: pass -@device_config.command('list') +@device_config.command( + 'list', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.argument('device-name', type=str) @name_to_guid def list_config(device_name: str, device_guid: str) -> None: @@ -54,59 +61,92 @@ def list_config(device_name: str, device_guid: str) -> None: raise SystemExit(1) -@device_config.command('create') +@device_config.command( + 'create', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.argument('device-name', type=str) @click.argument('key', type=str) @click.argument('value', type=str) @name_to_guid -def create_config(device_name: str, device_guid: str, key: str, value: str) -> None: +@with_spinner(text='Creating new config variable...') +def create_config( + device_name: str, + device_guid: str, + key: str, + value: str, + spinner=None, +) -> None: """ - Create a new config variable on the Device + Create a new config variable on the device """ try: - with spinner(): - client = new_client() - device = client.get_device(device_id=device_guid) - device.add_config_variable(key, value) - click.secho('Config Variable added successfully!', fg='green') + client = new_client() + device = client.get_device(device_id=device_guid) + device.add_config_variable(key, value) + spinner.text = click.style('Config variable added successfully.', fg=Colors.GREEN) + spinner.green.ok(Symbols.SUCCESS) except Exception as e: - click.secho(str(e), fg='red') - raise SystemExit(1) + spinner.text = click.style('Failed to add config variable: {}'.format(e), fg=Colors.RED) + spinner.red.fail(Symbols.ERROR) + raise SystemExit(1) from e -@device_config.command('update') +@device_config.command( + 'update', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.argument('device-name', type=str) @click.argument('key', type=str) @click.argument('value', type=str) @name_to_guid -def update_config(device_name: str, device_guid: str, key: str, value: str) -> None: +@with_spinner(text='Updating config variable...') +def update_config( + device_name: str, + device_guid: str, + key: str, + value: str, + spinner=None, +) -> None: """ - Update the config variable on the Device + Update the config variable on the device """ try: - with spinner(): - _update_config_variable(device_guid, key, value) - click.secho('Config variable updated successfully!', fg='green') + _update_config_variable(device_guid, key, value) + spinner.text = click.style('Config variable updated successfully.', fg=Colors.GREEN) + spinner.green.ok(Symbols.SUCCESS) except Exception as e: - click.secho(str(e), fg='red') - raise SystemExit(1) + spinner.text = click.style('Failed to update config variable: {}'.format(e), fg=Colors.RED) + spinner.red.fail(Symbols.ERROR) + raise SystemExit(1) from e -@device_config.command('delete') +@device_config.command( + 'delete', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.argument('device-name', type=str) @click.argument('key', type=str) @name_to_guid -def delete_config(device_name: str, device_guid: str, key: str) -> None: +@with_spinner(text='Deleting config variable...') +def delete_config(device_name: str, device_guid: str, key: str, spinner=None) -> None: """ - Delete the config variable on the Device + Delete the config variable on the device """ try: - with spinner(): - _delete_config_variable(device_guid, key) - click.secho('Config variable deleted successfully!', fg='green') + _delete_config_variable(device_guid, key) + spinner.text = click.style('Config variable deleted successfully.', fg=Colors.GREEN) + spinner.green.ok(Symbols.SUCCESS) except Exception as e: - click.secho(str(e), fg='red') - raise SystemExit(1) + spinner.text = click.style('Failed to delete config variable: {}'.format(e), fg=Colors.RED) + spinner.red.fail(Symbols.ERROR) + raise SystemExit(1) from e def _display_config_list(config_variables: typing.List[DeviceConfig], show_header: bool = True) -> None: diff --git a/riocli/device/delete.py b/riocli/device/delete.py index e25225a0..90e27a54 100644 --- a/riocli/device/delete.py +++ b/riocli/device/delete.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,28 +12,40 @@ # See the License for the specific language governing permissions and # limitations under the License. import click -import click_spinner +from click_help_colors import HelpColorsCommand from riocli.config import new_client +from riocli.constants import Colors, Symbols from riocli.device.util import name_to_guid +from riocli.utils.spinner import with_spinner -@click.command('delete') +@click.command( + 'delete', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.option('--force', '-f', 'force', is_flag=True, help='Skip confirmation') @click.argument('device-name', type=str) @name_to_guid -def delete_device(device_name: str, device_guid: str, force: bool): +@with_spinner(text='Deleting device...') +def delete_device(device_name: str, device_guid: str, force: bool, spinner=None): """ - Deletes the device from the Platform + Deletes a device """ - if not force: - click.confirm('Deleting device {} ({})'.format(device_name, device_guid), abort=True) + with spinner.hidden(): + if not force: + click.confirm( + 'Deleting device {} ({})'.format( + device_name, device_guid), abort=True) try: client = new_client(with_project=True) - with click_spinner.spinner(): - client.delete_device(device_id=device_guid) - click.secho('Device deleted successfully!', fg='green') + client.delete_device(device_id=device_guid) + spinner.text = click.style('Device deleted successfully', fg=Colors.GREEN) + spinner.green.ok(Symbols.SUCCESS) except Exception as e: - click.secho(str(e), fg='red') - raise SystemExit(1) + spinner.text = click.style('Failed to delete device: {}'.format(e), fg=Colors.RED) + spinner.red.fail(Symbols.ERROR) + raise SystemExit(1) from e diff --git a/riocli/device/deployment.py b/riocli/device/deployment.py index 8c834fa8..3ddac249 100644 --- a/riocli/device/deployment.py +++ b/riocli/device/deployment.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,13 +12,20 @@ # See the License for the specific language governing permissions and # limitations under the License. import click +from click_help_colors import HelpColorsCommand from riocli.config import new_client +from riocli.constants import Colors from riocli.deployment.list import display_deployment_list from riocli.device.util import name_to_guid -@click.command('deployments') +@click.command( + 'deployments', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.argument('device-name', type=str) @name_to_guid def list_deployments(device_name: str, device_guid: str) -> None: @@ -38,5 +45,5 @@ def list_deployments(device_name: str, device_guid: str) -> None: deployments.append(deployment) display_deployment_list(deployments, show_header=True) except Exception as e: - click.secho(str(e), fg='red') - raise SystemExit(1) + click.secho(str(e), fg=Colors.RED) + raise SystemExit(1) from e diff --git a/riocli/device/execute.py b/riocli/device/execute.py index 3064994f..0d4505a2 100644 --- a/riocli/device/execute.py +++ b/riocli/device/execute.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,24 +14,44 @@ import typing import click +from click_help_colors import HelpColorsCommand +from riocli.constants import Colors from riocli.device.util import name_to_guid from riocli.utils.execute import run_on_device -@click.command('execute') +@click.command( + 'execute', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.option('--user', default='root') @click.option('--shell', default='/bin/bash') @click.argument('device-name', type=str) @click.argument('command', nargs=-1) @name_to_guid -def execute_command(device_name: str, device_guid: str, user: str, shell: str, command: typing.List[str]): +def execute_command( + device_name: str, + device_guid: str, + user: str, + shell: str, + command: typing.List[str] +) -> None: """ Execute commands on the Device """ try: - response = run_on_device(device_guid=device_guid, user=user, shell=shell, command=command, background=False) + response = run_on_device( + device_guid=device_guid, + user=user, + shell=shell, + command=command, + background=False, + ) + click.secho(response) except Exception as e: - click.secho(str(e), fg='red') - raise SystemExit(1) + click.secho(str(e), fg=Colors.RED) + raise SystemExit(1) from e diff --git a/riocli/device/files.py b/riocli/device/files.py index c80a9529..85d3b8c1 100644 --- a/riocli/device/files.py +++ b/riocli/device/files.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,21 +14,23 @@ from datetime import datetime, timedelta import click +from click_help_colors import HelpColorsCommand from click_help_colors import HelpColorsGroup -from click_spinner import spinner from rapyuta_io.clients import LogsUploadRequest, LogUploads, SharedURL from riocli.config import new_client +from riocli.constants import Colors, Symbols from riocli.device.util import name_to_guid, name_to_request_id from riocli.utils import tabulate_data +from riocli.utils.spinner import with_spinner @click.group( 'uploads', invoke_without_command=False, cls=HelpColorsGroup, - help_headers_color='yellow', - help_options_color='green', + help_headers_color=Colors.YELLOW, + help_options_color=Colors.RED, ) def device_uploads() -> None: """ @@ -37,7 +39,10 @@ def device_uploads() -> None: pass -@device_uploads.command('list') +@device_uploads.command('list', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, ) @click.argument('device-name', type=str) @name_to_guid def list_uploads(device_name: str, device_guid: str) -> None: @@ -47,11 +52,16 @@ def list_uploads(device_name: str, device_guid: str) -> None: uploads = device.list_uploaded_files_for_device() _display_upload_list(uploads=uploads, show_header=True) except Exception as e: - click.secho(str(e), fg='red') - raise SystemExit(1) + click.secho(str(e), fg=Colors.RED) + raise SystemExit(1) from e -@device_uploads.command('create') +@device_uploads.command( + 'create', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.option('--max-upload-rate', type=int, default=1 * 1024 * 1024, help='Network bandwidth limit to be used for upload (Bytes per second)') @click.option('--override', is_flag=True, default=False, help='Flag to override destination file') @@ -60,6 +70,7 @@ def list_uploads(device_name: str, device_guid: str) -> None: @click.argument('upload-name', type=str) @click.argument('file-path', type=str) @name_to_guid +@with_spinner(text='Uploading...') def create_upload( device_name: str, device_guid: str, @@ -68,105 +79,174 @@ def create_upload( max_upload_rate: int, override: bool, purge: bool, + spinner=None, ) -> None: try: client = new_client() - with spinner(): - device = client.get_device(device_id=device_guid) - upload_request = LogsUploadRequest(device_path=file_path, file_name=upload_name, - max_upload_rate=max_upload_rate, override=override, - purge_after=purge) - device.upload_log_file(upload_request) - click.secho('File upload requested successfully!', fg='green') + device = client.get_device(device_id=device_guid) + upload_request = LogsUploadRequest( + device_path=file_path, + file_name=upload_name, + max_upload_rate=max_upload_rate, + override=override, + purge_after=purge + ) + device.upload_log_file(upload_request) + spinner.text = click.style('File upload requested successfully.', fg=Colors.GREEN) + spinner.green.ok(Symbols.SUCCESS) except Exception as e: - click.secho(str(e), fg='red') - raise SystemExit(1) + spinner.text = click.style('Failed to request upload: {}'.format(e), fg=Colors.RED) + spinner.red.fail(Symbols.ERROR) + raise SystemExit(1) from e -@device_uploads.command('status') +@device_uploads.command( + 'status', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.argument('device-name', type=str) @click.argument('file-name', type=str) @name_to_guid @name_to_request_id -def upload_status(device_name: str, device_guid: str, file_name: str, request_id: str): +def upload_status( + device_name: str, + device_guid: str, + file_name: str, + request_id: str, +) -> None: try: client = new_client() device = client.get_device(device_id=device_guid) status = device.get_log_upload_status(request_uuid=request_id) click.secho(status.status) except Exception as e: - click.secho(str(e), fg='red') - raise SystemExit(1) + click.secho(str(e), fg=Colors.RED) + raise SystemExit(1) from e -@device_uploads.command('delete') +@device_uploads.command( + 'delete', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.argument('device-name', type=str) @click.argument('file-name', type=str) @name_to_guid @name_to_request_id -def delete_upload(device_name: str, device_guid: str, file_name: str, request_id: str): +@with_spinner(text='Deleting upload...') +def delete_upload( + device_name: str, + device_guid: str, + file_name: str, + request_id: str, + spinner=None +) -> None: try: client = new_client() - with spinner(): - device = client.get_device(device_id=device_guid) - device.delete_uploaded_log_file(request_uuid=request_id) - click.secho('Deleted upload successfully!', fg='green') + device = client.get_device(device_id=device_guid) + device.delete_uploaded_log_file(request_uuid=request_id) + spinner.text = click.style('Deleted upload successfully.', fg=Colors.GREEN) + spinner.green.ok(Symbols.SUCCESS) except Exception as e: - click.secho(str(e), fg='red') - raise SystemExit(1) + spinner.text = click.style('Failed to delete upload: {}'.format(e), fg=Colors.RED) + spinner.red.fail(Symbols.ERROR) + raise SystemExit(1) from e -@device_uploads.command('download') +@device_uploads.command( + 'download', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.argument('device-name', type=str) @click.argument('file-name', type=str) @name_to_guid @name_to_request_id -def download_log(device_name: str, device_guid: str, file_name: str, request_id: str): +@with_spinner(text='Downloading file...') +def download_log( + device_name: str, + device_guid: str, + file_name: str, + request_id: str, + spinner=None +) -> None: try: client = new_client() - with spinner(): - device = client.get_device(device_id=device_guid) - url = device.download_log_file(request_uuid=request_id) - click.secho(url) + device = client.get_device(device_id=device_guid) + url = device.download_log_file(request_uuid=request_id) + spinner.text = click.style(url, fg=Colors.BLUE) + spinner.green.ok(Symbols.SUCCESS) except Exception as e: - click.secho(str(e), fg='red') - raise SystemExit(1) + spinner.text = click.style(str(e), fg=Colors.RED) + spinner.red.fail(Symbols.ERROR) + raise SystemExit(1) from e -@device_uploads.command('cancel') +@device_uploads.command( + 'cancel', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.argument('device-name', type=str) @click.argument('file-name', type=str) @name_to_guid @name_to_request_id -def cancel_upload(device_name: str, device_guid: str, file_name: str, request_id: str): +@with_spinner(text='Cancelling upload...') +def cancel_upload( + device_name: str, + device_guid: str, + file_name: str, + request_id: str, + spinner=None +) -> None: try: client = new_client() - with spinner(): - device = client.get_device(device_id=device_guid) - device.cancel_log_file_upload(request_uuid=request_id) - click.secho('Cancelled upload successfully!', fg='green') + device = client.get_device(device_id=device_guid) + device.cancel_log_file_upload(request_uuid=request_id) + spinner.text = click.style('Cancelled upload.', fg=Colors.GREEN) + spinner.green.ok(Symbols.SUCCESS) except Exception as e: - click.secho(str(e), fg='red') - raise SystemExit(1) + spinner.text = click.style(str(e), fg=Colors.RED) + spinner.red.fail(Symbols.ERROR) + raise SystemExit(1) from e -@device_uploads.command('share') +@device_uploads.command( + 'share', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.option('--expiry', help='Flag to set the expiry date for the Shared URL [default: 7 days]', default=7) @click.argument('device-name', type=str) @click.argument('file-name', type=str) @name_to_guid @name_to_request_id -def shared_url(device_name: str, device_guid: str, file_name: str, request_id: str, expiry: int) -> None: +@with_spinner(text='Creating shared URL...') +def shared_url( + device_name: str, + device_guid: str, + file_name: str, + request_id: str, + expiry: int, + spinner=None +) -> None: try: client = new_client() - with spinner(): - device = client.get_device(device_id=device_guid) - expiry_time = datetime.now() + timedelta(days=7) - public_url = device.create_shared_url(SharedURL(request_id, expiry_time=expiry_time)) - click.secho(public_url.url, fg='green') + device = client.get_device(device_id=device_guid) + expiry_time = datetime.now() + timedelta(days=expiry) + public_url = device.create_shared_url(SharedURL(request_id, expiry_time=expiry_time)) + spinner.text = click.style(public_url.url, fg=Colors.GREEN) + spinner.green.ok(Symbols.SUCCESS) except Exception as e: - click.secho(str(e), fg='red') - raise SystemExit(1) + spinner.text = click.style('Failed to create shared URL: {}'.format(e), fg=Colors.RED) + spinner.red.fail(Symbols.ERROR) + raise SystemExit(1) from e def _display_upload_list(uploads: LogUploads, show_header: bool = True) -> None: diff --git a/riocli/device/model.py b/riocli/device/model.py index 0ca25532..bd781338 100644 --- a/riocli/device/model.py +++ b/riocli/device/model.py @@ -1,4 +1,4 @@ -# Copyright 2022 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -21,19 +21,21 @@ class Device(Model): - def __init__(self, *args, **kwargs): self.update(*args, **kwargs) def find_object(self, client: Client) -> bool: - guid, obj = self.rc.find_depends( - {"kind": "device", "nameOrGUID": self.metadata.name}) + guid, obj = self.rc.find_depends({ + "kind": "device", + "nameOrGUID": self.metadata.name, + }) + if not guid: return False return obj - def create_object(self, client: Client) -> v1Device: + def create_object(self, client: Client, **kwargs) -> v1Device: device = client.create_device(self.to_v1()) return device @@ -56,10 +58,12 @@ def to_v1(self) -> v1Device: if preinstalled_enabled and self.spec.preinstalled.get('catkinWorkspace'): ros_workspace = self.spec.preinstalled.catkinWorkspace - return v1Device(name=self.metadata.name, description=self.spec.get('description'), - runtime_docker=docker_enabled, runtime_preinstalled=preinstalled_enabled, - ros_distro=self.spec.rosDistro, python_version=python_version, - rosbag_mount_path=rosbag_mount_path, ros_workspace=ros_workspace) + return v1Device( + name=self.metadata.name, description=self.spec.get('description'), + runtime_docker=docker_enabled, runtime_preinstalled=preinstalled_enabled, + ros_distro=self.spec.rosDistro, python_version=python_version, + rosbag_mount_path=rosbag_mount_path, ros_workspace=ros_workspace + ) @classmethod def pre_process(cls, client: Client, d: typing.Dict) -> None: diff --git a/riocli/device/tools/device_init.py b/riocli/device/tools/device_init.py index 8f61d703..dc1dfeef 100644 --- a/riocli/device/tools/device_init.py +++ b/riocli/device/tools/device_init.py @@ -14,31 +14,39 @@ import os import click -from click_spinner import spinner from riocli.config import Configuration +from riocli.constants import Symbols, Colors from riocli.device.util import name_to_guid from riocli.utils import run_bash from riocli.utils.execute import run_on_device +from riocli.utils.spinner import with_spinner @click.command('init') @click.argument('device-name', type=str) +@with_spinner(text="Initializing device...", timer=True) @name_to_guid -def device_init(device_name: str, device_guid: str) -> None: +def device_init(device_name: str, device_guid: str, spinner=None) -> None: """ Initialize device for use with device tools. This is required to be executed first before all tools sub-commands. """ try: - with spinner(): - _setup_device(device_guid=device_guid) - _setup_local() + _setup_device(device_guid=device_guid, spinner=spinner) + _setup_local(spinner=spinner) + + spinner.text = click.style( + "Initialized device {}".format(device_name), fg=Colors.GREEN) + spinner.green.ok(Symbols.SUCCESS) except Exception as e: - click.secho(str(e), fg='red') + spinner.text = click.style( + "Failed to initialize device. Error: {}".format(e), fg=Colors.RED) + spinner.red.fail(Symbols.ERROR) raise SystemExit(1) -def _setup_device(device_guid: str) -> None: +def _setup_device(device_guid: str, spinner=None) -> None: + spinner.write("> Installing pre-requisites on device") run_on_device(device_guid=device_guid, command=[ 'apt', 'install', '-y', 'socat', # TODO: Install piping-tunnel during onboarding itself @@ -51,14 +59,16 @@ def _setup_device(device_guid: str) -> None: ]) -def _setup_local() -> None: +def _setup_local(spinner=None) -> None: config = Configuration() path = os.path.join(os.path.dirname(config.filepath), 'tools') tunnel = os.path.join(path, 'piping-tunnel') if os.path.isfile(tunnel): + spinner.write("> Tools already installed on local machine") return # TODO: Add support for non-linux and non-amd64 machines + spinner.write("> Installing pre-requisites locally...") run_bash("""/bin/bash -c 'mkdir -p {}'""".format(path)) - run_bash("""/bin/bash -c 'pushd {} && curl -SLO https://github.com/nwtgck/go-piping-tunnel/releases/download/v0.10.1/piping-tunnel-0.10.1-linux-amd64.tar.gz && tar xf piping-tunnel-0.10.1-linux-amd64.tar.gz && rm CHANGELOG.md LICENSE piping-tunnel-0.10.1-linux-amd64.tar.gz README.md && popd' + run_bash("""/bin/bash -c 'pushd {} && curl -sSLO https://github.com/nwtgck/go-piping-tunnel/releases/download/v0.10.1/piping-tunnel-0.10.1-linux-amd64.tar.gz && tar xf piping-tunnel-0.10.1-linux-amd64.tar.gz && rm CHANGELOG.md LICENSE piping-tunnel-0.10.1-linux-amd64.tar.gz README.md && popd' """.format(path)) diff --git a/riocli/device/tools/ssh.py b/riocli/device/tools/ssh.py index 3122efba..76cd7d1c 100644 --- a/riocli/device/tools/ssh.py +++ b/riocli/device/tools/ssh.py @@ -15,23 +15,37 @@ import click -from riocli.device.tools.util import run_tunnel_on_device, run_tunnel_on_local, copy_to_device +from riocli.constants import Colors, Symbols +from riocli.device.tools.util import ( + run_tunnel_on_device, + run_tunnel_on_local, + copy_to_device, +) from riocli.device.util import name_to_guid from riocli.utils import random_string from riocli.utils.execute import run_on_device +from riocli.utils.spinner import with_spinner from riocli.utils.ssh_tunnel import get_free_tcp_port @click.command('ssh') @click.option('--user', '-u', default='root', help='Username for the SSH') -@click.option('--local-port', '-L', default=None, help='Port number on the local machine for forwarding SSH') -@click.option('--remote-port', '-R', default=22, help='Port number on the Device on which SSH Server is listening') +@click.option('--local-port', '-L', default=None, + help='Port number on the local machine for forwarding SSH') +@click.option('--remote-port', '-R', default=22, + help='Port number on the Device on which SSH Server is listening') @click.option('--x-forward/--no-x-forward', '-X', default=False, is_flag=True, help='Flag to enable X Forwarding over SSH') @click.argument('device-name', type=str) @name_to_guid -def device_ssh(device_name: str, device_guid: str, user: str, local_port: int, remote_port: int, - x_forward: bool) -> None: +def device_ssh( + device_name: str, + device_guid: str, + user: str, + local_port: int, + remote_port: int, + x_forward: bool, +) -> None: """ SSH to the Device """ @@ -42,32 +56,51 @@ def device_ssh(device_name: str, device_guid: str, user: str, local_port: int, r path = random_string(8, 5) if not local_port: local_port = get_free_tcp_port() - run_tunnel_on_device(device_guid=device_guid, remote_port=remote_port, path=path) + run_tunnel_on_device(device_guid=device_guid, remote_port=remote_port, + path=path) run_tunnel_on_local(local_port=local_port, path=path, background=True) - os.system('ssh -p {} {} -o StrictHostKeyChecking=no {}@localhost'.format(local_port, extra_args, user)) + os.system( + 'ssh -p {} {} -o StrictHostKeyChecking=no {}@localhost'.format( + local_port, extra_args, user)) except Exception as e: click.secho(str(e), fg='red') raise SystemExit(1) @click.command('ssh-authorize') -@click.option('--user', '-u', default='root', help='User for which SSH keys are added') +@click.option('--user', '-u', default='root', + help='User for which SSH keys are added') @click.argument('device-name', type=str) -@click.argument('public-key-file', default="~/.ssh/id_rsa.pub", type=click.Path(exists=True)) +@click.argument('public-key-file', default="~/.ssh/id_rsa.pub", + type=click.Path(exists=True)) +@with_spinner(text="Authorizing public SSH key...", timer=True) @name_to_guid -def ssh_authorize_key(device_name: str, device_guid: str, user: str, public_key_file: click.Path) -> None: +def ssh_authorize_key(device_name: str, device_guid: str, user: str, + public_key_file: click.Path, spinner=None) -> None: """ Authorize Public SSH Key """ try: temp_path = "/tmp/{}".format(random_string(8, 5)) - copy_to_device(device_guid, public_key_file, temp_path) - if user != "root": - command = ['cat', temp_path, '>>', '/home/' + user + '/.ssh/authorized_keys'] + + spinner.write("> Uploading public SSH key to device") + copy_to_device(device_guid, str(public_key_file), temp_path, + spinner=spinner) + + if user != 'root': + command = ['cat', temp_path, '>>', + '/home/' + user + '/.ssh/authorized_keys'] else: command = ['cat', temp_path, '>>', '/root/.ssh/authorized_keys'] + run_on_device(device_guid=device_guid, command=command, user=user) - click.secho('Keys added successfully!', fg='green') + + spinner.text = click.style( + 'Public key {} added successfully'.format(public_key_file), + fg=Colors.GREEN) + spinner.green.ok(Symbols.SUCCESS) except Exception as e: - click.secho(str(e), fg='red') + spinner.text = click.style("Failed to add keys".format(e), + fg=Colors.RED) + spinner.red.ok(Symbols.ERROR) raise SystemExit(1) diff --git a/riocli/device/tools/util.py b/riocli/device/tools/util.py index 04586144..87d19feb 100644 --- a/riocli/device/tools/util.py +++ b/riocli/device/tools/util.py @@ -60,8 +60,11 @@ def copy_from_device(device_guid: str, src: str, dest: str) -> None: run_bash('curl -o "{}" "{}"'.format(dest, url)) -def copy_to_device(device_guid: str, src: str, dest: str) -> None: +def copy_to_device(device_guid: str, src: str, dest: str, spinner=None) -> None: config = Configuration() path = random_string(8, 5) run_bash('curl -sT {} {}/{}'.format(src, config.piping_server, path), bg=True) - run_on_device(device_guid=device_guid, command=['curl', '-o', dest, '{}/{}'.format(config.piping_server, path)]) + with spinner.hidden(): + run_on_device( + device_guid=device_guid, + command=['curl', '-s', '-o', dest, '{}/{}'.format(config.piping_server, path)]) diff --git a/riocli/device/vpn.py b/riocli/device/vpn.py index 0a50f5f8..f964ef51 100644 --- a/riocli/device/vpn.py +++ b/riocli/device/vpn.py @@ -15,23 +15,27 @@ import click from click_help_colors import HelpColorsCommand +from yaspin import kbi_safe_yaspin from riocli.config import new_client +from riocli.constants import Colors from riocli.utils import tabulate_data @click.command( 'vpn', cls=HelpColorsCommand, - help_headers_color='yellow', - help_options_color='green' + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN ) @click.option('--devices', type=click.STRING, multiple=True, default=(), help='Device names to toggle VPN client') @click.argument('enable', type=click.BOOL) -@click.option('-f', '--force', '--silent', 'silent', is_flag=True, type=click.BOOL, default=False, +@click.option('-f', '--force', '--silent', 'silent', is_flag=True, + type=click.BOOL, default=False, help="Skip confirmation") -def toggle_vpn(devices: typing.List, enable: bool, silent: bool = False) -> None: +def toggle_vpn(devices: typing.List, enable: bool, + silent: bool = False) -> None: """ Enable or disable VPN client on the device @@ -71,10 +75,15 @@ def toggle_vpn(devices: typing.List, enable: bool, silent: bool = False) -> None "\nDo you want to proceed?", default=True, abort=True) + click.echo("") # Echo an empty line + result = [] - for device in final: - r = client.toggle_features(device.uuid, [('vpn', enable)]) - result.append([device.name, r.get('status')]) + with kbi_safe_yaspin() as spinner: + for device in final: + spinner.text = 'Updating VPN state on device {}'.format( + click.style(device.name, bold=True, fg=Colors.CYAN)) + r = client.toggle_features(device.uuid, [('vpn', enable)]) + result.append([device.name, r.get('status')]) click.echo("") # Echo an empty line diff --git a/riocli/disk/__init__.py b/riocli/disk/__init__.py index 366c5439..d6c4cedf 100644 --- a/riocli/disk/__init__.py +++ b/riocli/disk/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2022 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,6 +14,7 @@ import click from click_help_colors import HelpColorsGroup +from riocli.constants import Colors from riocli.disk.create import create_disk from riocli.disk.delete import delete_disk from riocli.disk.list import list_disks @@ -22,8 +23,8 @@ @click.group( invoke_without_command=False, cls=HelpColorsGroup, - help_headers_color='yellow', - help_options_color='green', + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, ) def disk() -> None: """ @@ -35,4 +36,3 @@ def disk() -> None: disk.add_command(list_disks) disk.add_command(create_disk) disk.add_command(delete_disk) -# disk.add_command(inspect_project) diff --git a/riocli/disk/create.py b/riocli/disk/create.py index f313d480..3af8cc1e 100644 --- a/riocli/disk/create.py +++ b/riocli/disk/create.py @@ -1,4 +1,4 @@ -# Copyright 2022 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,33 +12,51 @@ # See the License for the specific language governing permissions and # limitations under the License. import click -from click_spinner import spinner +from click_help_colors import HelpColorsCommand from rapyuta_io.clients.persistent_volumes import DiskCapacity -from rapyuta_io.utils.rest_client import HttpMethod -from riocli.disk.util import _api_call +from riocli.constants import Colors, Symbols +from riocli.disk.util import create_cloud_disk +from riocli.utils.spinner import with_spinner +SUPPORTED_CAPACITIES = [ + DiskCapacity.GiB_4.value, + DiskCapacity.GiB_8.value, + DiskCapacity.GiB_16.value, + DiskCapacity.GiB_32.value, + DiskCapacity.GiB_64.value, + DiskCapacity.GiB_128.value, + DiskCapacity.GiB_256.value, + DiskCapacity.GiB_512.value, +] -@click.command('create') + +@click.command( + 'create', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.argument('disk-name', type=str) -@click.option('--capacity', 'capacity', type=int) -def create_disk(disk_name: str, capacity: int) -> None: +@click.option('--capacity', 'capacity', type=click.Choice(SUPPORTED_CAPACITIES), + default=DiskCapacity.GiB_4.value, help='Disk size in GiB') +@with_spinner(text="Creating a new disk...") +def create_disk( + disk_name: str, + capacity: int = 4, + spinner=None, +) -> None: """ Creates a new disk """ try: - capacity = DiskCapacity(capacity) - with spinner(): - payload = { - "name": disk_name, - "diskType": "ssd", - "runtime": "cloud", - "capacity": DiskCapacity(capacity).value, - } - disk = _api_call(HttpMethod.POST, payload=payload) + disk = create_cloud_disk(disk_name, capacity) - click.secho('Disk {} ({}) created successfully!'. - format(disk['name'], disk['guid']), fg='green') + spinner.text = click.style( + 'Disk {} ({}) created successfully.'. + format(disk['name'], disk['guid']), fg=Colors.GREEN) + spinner.green.ok(Symbols.SUCCESS) except Exception as e: - click.secho(str(e), fg='red') + spinner.text = click.style('Failed to create disk: {}'.format(e), fg=Colors.RED) + spinner.red.fail(Symbols.ERROR) raise SystemExit(1) diff --git a/riocli/disk/delete.py b/riocli/disk/delete.py index c98e5359..e3b76eec 100644 --- a/riocli/disk/delete.py +++ b/riocli/disk/delete.py @@ -1,4 +1,4 @@ -# Copyright 2022 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,27 +12,45 @@ # See the License for the specific language governing permissions and # limitations under the License. import click -from click_spinner import spinner +from click_help_colors import HelpColorsCommand from rapyuta_io.utils.rest_client import HttpMethod +from riocli.constants import Symbols, Colors from riocli.disk.util import name_to_guid, _api_call +from riocli.utils.spinner import with_spinner -@click.command('delete') -@click.option('--force', '-f', 'force', is_flag=True, default=False, help='Skip confirmation') -@click.argument('disk-name', required=True) +@click.command( + 'delete', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) +@click.option('--force', '-f', 'force', is_flag=True, default=False, + help='Skip confirmation') +@click.argument('disk-name', required=True, type=str) @name_to_guid -def delete_disk(disk_name: str, disk_guid: str, force: bool): +@with_spinner(text="Deleting disk...") +def delete_disk( + disk_name: str, + disk_guid: str, + force: bool, + spinner=None +) -> None: """ - Delete the disk from the Platform + Delete a disk """ - if not force: - click.confirm('Deleting disk {} ({})'.format(disk_name, disk_guid), abort=True) + with spinner.hidden(): + if not force: + click.confirm( + 'Deleting disk {} ({})'.format(disk_name, disk_guid), abort=True) try: - with spinner(): - _api_call(HttpMethod.DELETE, guid=disk_guid, load_response=False) - click.echo(click.style('Disk deleted successfully!', fg='green')) + _api_call(HttpMethod.DELETE, guid=disk_guid, load_response=False) + + spinner.text = click.style('Disk deleted successfully.', fg=Colors.GREEN) + spinner.green.ok(Symbols.SUCCESS) except Exception as e: - click.secho(str(e), fg='red') + spinner.text = click.style('Failed to delete disk: {}'.format(str(e)), fg=Colors.RED) + spinner.red.fail(Symbols.ERROR) raise SystemExit(1) diff --git a/riocli/disk/list.py b/riocli/disk/list.py index 5c0f7da0..db37d23b 100644 --- a/riocli/disk/list.py +++ b/riocli/disk/list.py @@ -1,4 +1,4 @@ -# Copyright 2022 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ import click from rapyuta_io.utils.rest_client import HttpMethod +from riocli.constants import Colors from riocli.disk.util import _api_call from riocli.utils import tabulate_data @@ -30,15 +31,25 @@ def list_disks() -> None: disks = sorted(disks, key=lambda d: d['name'].lower()) _display_disk_list(disks, show_header=True) except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg=Colors.RED) raise SystemExit(1) def _display_disk_list(disks: typing.Any, show_header: bool = True): headers = [] if show_header: - headers = ('Disk ID', 'Name', 'Status', 'Capacity', 'Used By') - - data = [[d['guid'], d['name'], d['status'], d['capacity'], d['usedBy']] for d in disks] + headers = ( + 'Disk ID', 'Name', 'Status', 'Capacity', + 'Used', 'Available', 'Used By', + ) + + data = [[d['guid'], + d['name'], + d['status'], + d['capacity'], + d.get('used', 'NA'), + d.get('available', 'NA'), + d['usedBy']] + for d in disks] tabulate_data(data, headers) diff --git a/riocli/disk/model.py b/riocli/disk/model.py index 8643d4ea..2f489642 100644 --- a/riocli/disk/model.py +++ b/riocli/disk/model.py @@ -1,4 +1,4 @@ -# Copyright 2022 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,11 +15,11 @@ from time import sleep import click -import click_spinner from munch import munchify from rapyuta_io import Client from rapyuta_io.utils.rest_client import HttpMethod +from riocli.constants import Colors, Symbols from riocli.disk.util import _api_call from riocli.jsonschema.validate import load_schema from riocli.model import Model @@ -27,13 +27,17 @@ class Disk(Model): def find_object(self, client: Client) -> typing.Any: - _, disk = self.rc.find_depends({'kind': 'disk', 'nameOrGUID': self.metadata.name}) + _, disk = self.rc.find_depends({ + 'kind': 'disk', + 'nameOrGUID': self.metadata.name + }) + if not disk: return False return disk - def create_object(self, client: Client) -> typing.Any: + def create_object(self, client: Client, **kwargs) -> typing.Any: labels = self.metadata.get('labels', None) payload = { "labels": labels, @@ -42,18 +46,30 @@ def create_object(self, client: Client) -> typing.Any: "runtime": self.spec.runtime, "capacity": self.spec.capacity, } - with click_spinner.spinner(): - result = _api_call(HttpMethod.POST, payload=payload) - result = munchify(result) - disk_dep_guid, disk = self.rc.find_depends({'kind': self.kind.lower(), 'nameOrGUID': self.metadata.name}) - volume_instance = client.get_volume_instance(disk_dep_guid) - try: - volume_instance.poll_deployment_till_ready(sleep_interval=5) - return result - except Exception as e: - click.secho(">> Warning: Error Polling for disk ({}:{})".format(self.kind.lower(), self.metadata.name), - fg="yellow") - return result + + result = _api_call(HttpMethod.POST, payload=payload) + result = munchify(result) + disk_dep_guid, disk = self.rc.find_depends({ + 'kind': self.kind.lower(), + 'nameOrGUID': self.metadata.name + }) + + volume_instance = client.get_volume_instance(disk_dep_guid) + + retry_count = int(kwargs.get('retry_count')) + retry_interval = int(kwargs.get('retry_interval')) + try: + volume_instance.poll_deployment_till_ready( + retry_count=retry_count, + sleep_interval=retry_interval + ) + except Exception as e: + click.secho(">> {}: Error polling for disk ({}:{})".format( + Symbols.WARNING, + self.kind.lower(), + self.metadata.name), fg=Colors.YELLOW) + + return result def update_object(self, client: Client, obj: typing.Any) -> typing.Any: pass diff --git a/riocli/disk/util.py b/riocli/disk/util.py index f8000e09..faca2e78 100644 --- a/riocli/disk/util.py +++ b/riocli/disk/util.py @@ -1,4 +1,4 @@ -# Copyright 2022 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,13 +13,49 @@ # limitations under the License. import functools import json +import time import typing import click from rapyuta_io import Client +from rapyuta_io.clients.persistent_volumes import DiskCapacity, DiskType from rapyuta_io.utils.rest_client import RestClient, HttpMethod from riocli.config import Configuration, new_client +from riocli.constants import Colors, Symbols + + +class DiskNotFound(Exception): + def __init__(self): + super().__init__('Disk not found') + + +def _api_call( + method: str, + guid: typing.Union[str, None] = None, + payload: typing.Union[typing.Dict, None] = None, + load_response: bool = True, +) -> typing.Any: + config = Configuration() + catalog_host = config.data.get( + 'catalog_host', 'https://gacatalog.apps.rapyuta.io') + + url = '{}/disk'.format(catalog_host) + if guid: + url = '{}/{}'.format(url, guid) + + headers = config.get_auth_header() + response = RestClient(url).method(method).headers( + headers).execute(payload=payload) + + data = None + if load_response: + data = json.loads(response.text) + + if not response.ok: + err_msg = data.get('error') + raise Exception(err_msg) + return data def name_to_guid(f: typing.Callable) -> typing.Callable: @@ -40,8 +76,8 @@ def decorated(**kwargs: typing.Any): try: guid = find_disk_guid(client, name) except Exception as e: - click.secho(str(e), fg='red') - raise SystemExit(1) + click.secho('{} {}'.format(Symbols.ERROR, e), fg=Colors.RED) + raise SystemExit(1) from e kwargs['disk_name'] = name kwargs['disk_guid'] = guid @@ -66,30 +102,25 @@ def find_disk_guid(client: Client, name: str) -> str: raise DiskNotFound() -def _api_call(method: str, guid: typing.Union[str, None] = None, - payload: typing.Union[typing.Dict, None] = None, load_response: bool = True, - ) -> typing.Any: - config = Configuration() - catalog_host = config.data.get( - 'catalog_host', 'https://gacatalog.apps.rapyuta.io') - url = '{}/disk'.format(catalog_host) - if guid: - url = '{}/{}'.format(url, guid) - headers = config.get_auth_header() - response = RestClient(url).method(method).headers( - headers).execute(payload=payload) - data = None - err_msg = 'error in the api call' - if load_response: - data = json.loads(response.text) +def create_cloud_disk(disk_name: str, capacity: int) -> typing.Dict: + """ + Creates a new cloud disk and waits until it is provisioned + """ + payload = { + "name": disk_name, + "diskType": DiskType.SSD, + "runtime": "cloud", + "capacity": DiskCapacity(capacity).value, + } - if not response.ok: - err_msg = data.get('error') - raise Exception(err_msg) - return data + disk = _api_call(HttpMethod.POST, payload=payload) + while not is_disk_ready(disk.get('guid')): + time.sleep(5) -class DiskNotFound(Exception): - def __init__(self, message='disk not found!'): - self.message = message - super().__init__(self.message) + return disk + + +def is_disk_ready(disk_guid: str) -> bool: + disk = _api_call(HttpMethod.GET, disk_guid, load_response=True) + return disk.get('status') != 'Pending' diff --git a/riocli/jsonschema/schemas/build-schema.yaml b/riocli/jsonschema/schemas/build-schema.yaml index ce9d27f1..68bfadbc 100644 --- a/riocli/jsonschema/schemas/build-schema.yaml +++ b/riocli/jsonschema/schemas/build-schema.yaml @@ -39,7 +39,7 @@ definitions: - name projectGUID: type: string - pattern: "^project-[a-z]{24}$" + pattern: "^project-([a-z0-9]{20}|[a-z]{24})$" buildGUID: type: string pattern: "^build-[a-z]{24}$" diff --git a/riocli/jsonschema/schemas/deployment-schema.yaml b/riocli/jsonschema/schemas/deployment-schema.yaml index 1038f903..bbb577c3 100644 --- a/riocli/jsonschema/schemas/deployment-schema.yaml +++ b/riocli/jsonschema/schemas/deployment-schema.yaml @@ -369,7 +369,7 @@ definitions: pattern: "^org-[a-z]{24}$" projectGUID: type: string - pattern: "^project-[a-z]{24}$" + pattern: "^project-([a-z0-9]{20}|[a-z]{24})$" secretGUID: type: string pattern: "^secret-[a-z]{24}$" diff --git a/riocli/jsonschema/schemas/device-schema.yaml b/riocli/jsonschema/schemas/device-schema.yaml index 44799479..c0545a70 100644 --- a/riocli/jsonschema/schemas/device-schema.yaml +++ b/riocli/jsonschema/schemas/device-schema.yaml @@ -98,7 +98,7 @@ definitions: type: string projectGUID: type: string - pattern: "^project-[a-z]{24}$" + pattern: "^project-([a-z0-9]{20}|[a-z]{24})$" uuid: type: string pattern: "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" diff --git a/riocli/jsonschema/schemas/disk-schema.yaml b/riocli/jsonschema/schemas/disk-schema.yaml index 7134dcb4..87b0ffb3 100644 --- a/riocli/jsonschema/schemas/disk-schema.yaml +++ b/riocli/jsonschema/schemas/disk-schema.yaml @@ -38,7 +38,7 @@ definitions: - name projectGUID: type: string - pattern: "^project-[a-z]{24}$" + pattern: "^project-([a-z0-9]{20}|[a-z]{24})$" diskGUID: type: string pattern: "^disk-[a-z]{24}$" diff --git a/riocli/jsonschema/schemas/managedservice-schema.yaml b/riocli/jsonschema/schemas/managedservice-schema.yaml index 65ce5438..6fbb734f 100644 --- a/riocli/jsonschema/schemas/managedservice-schema.yaml +++ b/riocli/jsonschema/schemas/managedservice-schema.yaml @@ -38,7 +38,7 @@ definitions: - name projectGUID: type: string - pattern: "^project-[a-z]{24}$" + pattern: "^project-([a-z0-9]{20}|[a-z]{24})$" stringMap: type: object additionalProperties: diff --git a/riocli/jsonschema/schemas/network-schema.yaml b/riocli/jsonschema/schemas/network-schema.yaml index 9a9d9720..766b2ff3 100644 --- a/riocli/jsonschema/schemas/network-schema.yaml +++ b/riocli/jsonschema/schemas/network-schema.yaml @@ -99,7 +99,7 @@ definitions: pattern: "^network-[a-z]{24}$" projectGUID: type: string - pattern: "^project-[a-z]{24}$" + pattern: "^project-([a-z0-9]{20}|[a-z]{24})$" stringMap: type: object additionalProperties: diff --git a/riocli/jsonschema/schemas/package-schema.yaml b/riocli/jsonschema/schemas/package-schema.yaml index bba4875f..d53e46c6 100644 --- a/riocli/jsonschema/schemas/package-schema.yaml +++ b/riocli/jsonschema/schemas/package-schema.yaml @@ -638,7 +638,7 @@ definitions: pattern: "^org-[a-z]{24}$" projectGUID: type: string - pattern: "^project-[a-z]{24}$" + pattern: "^project-([a-z0-9]{20}|[a-z]{24})$" secretGUID: type: string pattern: "^secret-[a-z]{24}$" diff --git a/riocli/jsonschema/schemas/primitives.yaml b/riocli/jsonschema/schemas/primitives.yaml index 213cbecc..9efe18d9 100644 --- a/riocli/jsonschema/schemas/primitives.yaml +++ b/riocli/jsonschema/schemas/primitives.yaml @@ -9,7 +9,7 @@ definitions: pattern: "^org-[a-z]{24}$" projectGUID: type: string - pattern: "^project-[a-z]{24}$" + pattern: "^project-([a-z0-9]{20}|[a-z]{24})$" secretGUID: type: string pattern: "^secret-[a-z]{24}$" diff --git a/riocli/jsonschema/schemas/project-schema.yaml b/riocli/jsonschema/schemas/project-schema.yaml index 40a657a9..89f2c47f 100644 --- a/riocli/jsonschema/schemas/project-schema.yaml +++ b/riocli/jsonschema/schemas/project-schema.yaml @@ -43,7 +43,7 @@ definitions: - name projectGUID: type: string - pattern: "^project-[a-z]{24}$" + pattern: "^project-([a-z0-9]{20}|[a-z]{24})$" organizationGUID: type: string pattern: "^org-[a-z]{24}$" diff --git a/riocli/jsonschema/schemas/secret-schema.yaml b/riocli/jsonschema/schemas/secret-schema.yaml index 03242d63..09e79d7b 100644 --- a/riocli/jsonschema/schemas/secret-schema.yaml +++ b/riocli/jsonschema/schemas/secret-schema.yaml @@ -38,7 +38,7 @@ definitions: - name projectGUID: type: string - pattern: "^project-[a-z]{24}$" + pattern: "^project-([a-z0-9]{20}|[a-z]{24})$" secretGUID: type: string pattern: "^secret-[a-z]{24}$" diff --git a/riocli/jsonschema/schemas/static_route-schema.yaml b/riocli/jsonschema/schemas/static_route-schema.yaml index c43f050c..7cc26515 100644 --- a/riocli/jsonschema/schemas/static_route-schema.yaml +++ b/riocli/jsonschema/schemas/static_route-schema.yaml @@ -36,7 +36,7 @@ definitions: - name projectGUID: type: string - pattern: "^project-[a-z]{24}$" + pattern: "^project-([a-z0-9]{20}|[a-z]{24})$" staticRouteGUID: type: string pattern: "^staticroute-[a-z]{24}$" diff --git a/riocli/jsonschema/schemas/usergroup-schema.yaml b/riocli/jsonschema/schemas/usergroup-schema.yaml new file mode 100644 index 00000000..85586851 --- /dev/null +++ b/riocli/jsonschema/schemas/usergroup-schema.yaml @@ -0,0 +1,114 @@ +--- +$schema: http://json-schema.org/draft-07/schema# +title: UserGroup +description: A construct in rapyuta.io that allows one to grant access to projects to multiple users at once +$ref: "#/definitions/usergroup" +definitions: + usergroup: + type: object + properties: + apiVersion: + const: api.rapyuta.io/v2 + default: api.rapyuta.io/v2 + kind: + const: UserGroup + metadata: + "$ref": "#/definitions/metadata" + spec: + "$ref": "#/definitions/usergroupSpec" + + required: + - apiVersion + - kind + - metadata + - spec + + metadata: + type: object + properties: + name: + type: string + pattern: "^[a-zA-Z][a-z A-Z0-9-_]{2,63}$" + guid: + $ref: "#/definitions/uuid" + creator: + $ref: "#/definitions/uuid" + project: + $ref: "#/definitions/projectGUID" + organization: + $ref: "#/definitions/organizationGUID" + labels: + $ref: "#/definitions/stringMap" + uniqueItems: true + + required: + - name + - organization + + usergroupSpec: + type: object + properties: + description: + type: string + members: + type: array + items: + "$ref": "#/definitions/member" + admins: + type: array + items: + "$ref": "#/definitions/member" + projects: + type: array + items: + "$ref": "#/definitions/project" + additionalProperties: false + + member: + type: object + properties: + guid: + $ref: "#/definitions/uuid" + emailID: + $ref: "#/definitions/email" + + oneOf: + - required: + - guid + - required: + - emailID + + project: + type: object + properties: + guid: + type: string + pattern: "^project-([a-z0-9]{20}|[a-z]{24})$" + name: + type: string + oneOf: + - required: + - guid + - required: + - name + + stringMap: + type: object + additionalProperties: + type: string + + projectGUID: + type: string + pattern: "^project-([a-z0-9]{20}|[a-z]{24})$" + + organizationGUID: + type: string + pattern: "^org-([a-z0-9]{20}|[a-z]{24})$" + + uuid: + type: string + pattern: "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" + + email: + type: string + pattern: "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9]{2,}$" diff --git a/riocli/managedservice/model.py b/riocli/managedservice/model.py index 77ed3a85..54886874 100644 --- a/riocli/managedservice/model.py +++ b/riocli/managedservice/model.py @@ -32,7 +32,7 @@ def find_object(self, client: Client) -> typing.Any: except Exception: return False - def create_object(self, client: Client) -> typing.Any: + def create_object(self, client: Client, **kwargs) -> typing.Any: client = new_v2_client() ms = unmunchify(self) diff --git a/riocli/model/base.py b/riocli/model/base.py index d10c712c..b5d4f71e 100644 --- a/riocli/model/base.py +++ b/riocli/model/base.py @@ -19,79 +19,121 @@ import click from munch import Munch, munchify from rapyuta_io import Client +from yaspin.api import Yaspin +from riocli.constants import Colors, Symbols from riocli.project.util import find_project_guid -prompt = ">> {}{}{} [{}]" # >> msg spacer rigth_msg time +prompt = ">> {}{}{} [{}]" # >> left_msg spacer right_msg time DELETE_POLICY_LABEL = 'rapyuta.io/deletionPolicy' -def message_with_prompt(msg, right_msg="", fg='white', with_time=True): + +def message_with_prompt( + msg, + right_msg='', + fg=Colors.WHITE, + with_time=True, + spinner: Yaspin = None, +) -> None: columns, _ = get_terminal_size() - time = datetime.now().isoformat('T') - spacer = ' ' * (int(columns) - len(msg + right_msg + time) - 12) - msg = prompt.format(msg, spacer, right_msg, time) - click.secho(msg, fg=fg) + t = datetime.now().isoformat('T') + spacer = ' ' * (int(columns) - len(msg + right_msg + t) - 12) + msg = prompt.format(msg, spacer, right_msg, t) + msg = click.style(msg, fg=fg) + if spinner: + spinner.write(msg) + else: + click.echo(msg) class Model(ABC, Munch): def apply(self, client: Client, *args, **kwargs) -> typing.Any: + spinner = kwargs.get('spinner') try: self._set_project_in_client(client) obj = self.find_object(client) dryrun = kwargs.get("dryrun", False) if not obj: - message_with_prompt("⌛ Create {}:{}".format( - self.kind.lower(), self.metadata.name), fg='yellow') + message_with_prompt("{} Create {}:{}".format( + Symbols.WAITING, + self.kind.lower(), + self.metadata.name), fg=Colors.YELLOW, spinner=spinner) if not dryrun: - result = self.create_object(client) - message_with_prompt("✅ Created {}:{}".format( - self.kind.lower(), self.metadata.name), fg='green') + result = self.create_object(client, **kwargs) + message_with_prompt("{} Created {}:{}".format( + Symbols.SUCCESS, + self.kind.lower(), + self.metadata.name), fg=Colors.GREEN, spinner=spinner) return result else: - message_with_prompt('🔎 {}:{} exists. will be updated'.format( - self.kind.lower(), self.metadata.name)) - message_with_prompt("⌛ Update {}:{}".format( - self.kind.lower(), self.metadata.name), fg='yellow') + message_with_prompt('{} {}:{} exists. will be updated'.format( + Symbols.INFO, + self.kind.lower(), + self.metadata.name), spinner=spinner) + message_with_prompt("{} Update {}:{}".format( + Symbols.WAITING, + self.kind.lower(), + self.metadata.name), fg=Colors.YELLOW, spinner=spinner) if not dryrun: result = self.update_object(client, obj) - message_with_prompt("✅ Updated {}:{}".format( - self.kind.lower(), self.metadata.name), fg='green') + message_with_prompt("{} Updated {}:{}".format( + Symbols.SUCCESS, + self.kind.lower(), + self.metadata.name), fg=Colors.GREEN, spinner=spinner) return result except Exception as e: - message_with_prompt("‼ ERR {}:{}. {} ‼".format( - self.kind.lower(), self.metadata.name, str(e)), fg="red") + message_with_prompt("{} {}:{}. {} ‼".format( + Symbols.ERROR, + self.kind.lower(), + self.metadata.name, + str(e)), fg=Colors.RED, spinner=spinner) raise e def delete(self, client: Client, obj: typing.Any, *args, **kwargs): + spinner = kwargs.get('spinner') + dryrun = kwargs.get("dryrun", False) try: self._set_project_in_client(client) obj = self.find_object(client) - dryrun = kwargs.get("dryrun", False) if not obj: - message_with_prompt('⁉ {}:{} does not exist'.format( - self.kind.lower(), self.metadata.name)) + message_with_prompt( + '⁉ {}:{} does not exist'.format( + self.kind.lower(), self.metadata.name), + spinner=spinner) return - else: - message_with_prompt("⌛ Delete {}:{}".format( - self.kind.lower(), self.metadata.name), fg='yellow') - if not dryrun: - labels = self.metadata.get('labels', {}) - if DELETE_POLICY_LABEL in labels and \ - labels.get(DELETE_POLICY_LABEL) and \ - labels.get(DELETE_POLICY_LABEL).lower() == "retain": - click.secho(">> Warning: delete protection enabled on {}:{}. Resource will be retained ".format( - self.kind.lower(), self.metadata.name), fg="yellow") - return - - self.delete_object(client, obj) - message_with_prompt("❌ Deleted {}:{}".format( - self.kind.lower(), self.metadata.name), fg='red') + + message_with_prompt("{} Delete {}:{}".format( + Symbols.WAITING, + self.kind.lower(), + self.metadata.name), fg=Colors.YELLOW, spinner=spinner) + + if not dryrun: + labels = self.metadata.get('labels', {}) + if (DELETE_POLICY_LABEL in labels and + labels.get(DELETE_POLICY_LABEL) and + labels.get(DELETE_POLICY_LABEL).lower() == "retain"): + click.secho( + ">> {} Delete protection enabled on {}:{}. " + "Resource will be retained ".format( + Symbols.WARNING, + self.kind.lower(), + self.metadata.name), fg=Colors.YELLOW) + return + + self.delete_object(client, obj) + message_with_prompt("{} Deleted {}:{}".format( + Symbols.SUCCESS, + self.kind.lower(), + self.metadata.name), fg=Colors.GREEN, spinner=spinner) except Exception as e: - message_with_prompt("‼ ERR {}:{}. {} ‼".format( - self.kind.lower(), self.metadata.name, str(e)), fg="red") + message_with_prompt("{} {}:{}. {} ‼".format( + Symbols.ERROR, + self.kind.lower(), + self.metadata.name, + str(e)), fg=Colors.RED, spinner=spinner) raise e @abstractmethod @@ -99,7 +141,7 @@ def find_object(self, client: Client) -> typing.Any: pass @abstractmethod - def create_object(self, client: Client) -> typing.Any: + def create_object(self, client: Client, **kwargs) -> typing.Any: pass @abstractmethod diff --git a/riocli/network/__init__.py b/riocli/network/__init__.py index c3ef5b66..6099ea8d 100644 --- a/riocli/network/__init__.py +++ b/riocli/network/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,6 +14,7 @@ import click from click_help_colors import HelpColorsGroup +from riocli.constants import Colors from riocli.network.create import create_network from riocli.network.delete import delete_network from riocli.network.inspect import inspect_network @@ -24,8 +25,8 @@ @click.group( invoke_without_command=False, cls=HelpColorsGroup, - help_headers_color='yellow', - help_options_color='green', + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, ) def network() -> None: """ diff --git a/riocli/network/create.py b/riocli/network/create.py index 0e67774a..b47bb226 100644 --- a/riocli/network/create.py +++ b/riocli/network/create.py @@ -25,14 +25,19 @@ @click.option('--network', help='Type of Network', type=click.Choice(['routed', 'native']), default='routed') @click.option('--ros', help='Version of ROS', - type=click.Choice(['kinetic', 'melodic', 'noetic']), default='melodic') -@click.option('--device', 'device_name', help='Device ID of the Device where Network will run (device only)') + type=click.Choice(['kinetic', 'melodic', 'noetic']), + default='melodic') +@click.option('--device', 'device_name', + help='Device ID of the Device where Network will run (device only)') @click.option('--cpu', help='cpu limit for Network (cloud only) ', type=float) -@click.option('--memory', help='memory limit for Network (cloud only) ', type=int) +@click.option('--memory', help='memory limit for Network (cloud only) ', + type=int) @click.option('--network-interface', '-nic', type=str, help='Network Interface on which Network will listen (device only)') -@click.option('--restart-policy', help='Restart policy for the Network (device only)', - type=click.Choice(['always', 'no', 'on-failure']), default='always') +@click.option('--restart-policy', + help='Restart policy for the Network (device only)', + type=click.Choice(['always', 'no', 'on-failure']), + default='always') @device_name_to_guid def create_network(name: str, network: str, **kwargs: typing.Any) -> None: """ diff --git a/riocli/network/delete.py b/riocli/network/delete.py index 0ae802fe..7530c001 100644 --- a/riocli/network/delete.py +++ b/riocli/network/delete.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,37 +12,62 @@ # See the License for the specific language governing permissions and # limitations under the License. import click -from click_spinner import spinner +from click_help_colors import HelpColorsCommand +from yaspin.api import Yaspin from riocli.config import new_client +from riocli.constants import Colors, Symbols from riocli.network.util import name_to_guid +from riocli.utils.spinner import with_spinner -@click.command('delete') -@click.option('--force', '-f', is_flag=True, default=False, help='Skip confirmation', type=bool) -@click.option('--network', 'network_type', help='Type of Network', default=None, +@click.command( + 'delete', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) +@click.option('--force', '-f', is_flag=True, default=False, + help='Skip confirmation', type=bool) +@click.option('--network', 'network_type', help='Type of Network', + default=None, type=click.Choice(['routed', 'native'])) @click.argument('network-name', type=str) @name_to_guid -def delete_network(force: bool, network_name: str, network_guid: str, network_type: str) -> None: +@with_spinner(text='Deleting network...') +def delete_network( + force: bool, + network_name: str, + network_guid: str, + network_type: str, + spinner: Yaspin = None +) -> None: """ - Delete the network from the Platform + Deletes a network """ - if not force: - click.confirm('Deleting {} network {} ({})'. - format(network_type, network_name, network_guid), abort=True) + with spinner.hidden(): + click.confirm( + 'Deleting {} network {} ({})'. + format(network_type, network_name, network_guid), + abort=True) try: client = new_client() - with spinner(): - if network_type == 'routed': - # TODO: Implement and use the delete_routed_network of client directly. - rn = client.get_routed_network(network_guid) - rn.delete() - elif network_type == 'native': - client.delete_native_network(network_guid) - click.secho('{} Network deleted successfully!'.format(network_type.capitalize()), fg='green') + + if network_type == 'routed': + client.delete_routed_network(network_guid) + elif network_type == 'native': + client.delete_native_network(network_guid) + else: + raise Exception('invalid network type') + + spinner.text = click.style( + '{} deleted successfully!'.format(network_type.capitalize()), + fg=Colors.GREEN) + spinner.green.ok(Symbols.SUCCESS) except Exception as e: - click.secho(str(e), fg='red') - raise SystemExit(1) + spinner.text = click.style('Failed to delete network: {}'.format(e), + fg=Colors.RED) + spinner.red.fail(Symbols.ERROR) + raise SystemExit(1) from e diff --git a/riocli/network/inspect.py b/riocli/network/inspect.py index 9f8acf07..dfeb5b43 100644 --- a/riocli/network/inspect.py +++ b/riocli/network/inspect.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,19 +12,28 @@ # See the License for the specific language governing permissions and # limitations under the License. import click +from click_help_colors import HelpColorsCommand +from riocli.constants import Colors from riocli.network.native_network import inspect_native_network from riocli.network.routed_network import inspect_routed_network from riocli.network.util import name_to_guid from riocli.utils import inspect_with_format -@click.command('inspect') +@click.command( + 'inspect', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.option('--format', '-f', 'format_type', - type=click.Choice(['json', 'yaml'], case_sensitive=False), default='yaml') + type=click.Choice(['json', 'yaml'], case_sensitive=False), + default='yaml') @click.argument('network-name', type=str) @name_to_guid -def inspect_network(format_type: str, network_name: str, network_guid: str, network_type: str) -> None: +def inspect_network(format_type: str, network_name: str, network_guid: str, + network_type: str) -> None: """ Inspect the network resource """ @@ -36,5 +45,5 @@ def inspect_network(format_type: str, network_name: str, network_guid: str, netw inspect_with_format(data, format_type) except Exception as e: - click.secho(str(e), fg='red') - raise SystemExit(1) + click.secho(str(e), fg=Colors.RED) + raise SystemExit(1) from e diff --git a/riocli/network/list.py b/riocli/network/list.py index e0266c2d..5db25b51 100644 --- a/riocli/network/list.py +++ b/riocli/network/list.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,15 +14,22 @@ import typing import click +from click_help_colors import HelpColorsCommand from rapyuta_io import DeploymentPhaseConstants from rapyuta_io.clients.native_network import NativeNetwork from rapyuta_io.clients.routed_network import RoutedNetwork from riocli.config import new_client +from riocli.constants import Colors from riocli.utils import tabulate_data -@click.command('list') +@click.command( + 'list', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.option('--network', help='Type of Network', type=click.Choice(['routed', 'native', 'both']), default='both') def list_networks(network: str) -> None: @@ -31,6 +38,7 @@ def list_networks(network: str) -> None: """ try: client = new_client() + networks = [] if network in ['routed', 'both']: networks += client.get_all_routed_networks() @@ -39,6 +47,7 @@ def list_networks(network: str) -> None: networks += client.list_native_networks() networks = sorted(networks, key=lambda n: n.name.lower()) + _display_network_list(networks, show_header=True) except Exception as e: click.secho(str(e), fg='red') @@ -66,6 +75,7 @@ def _display_network_list( if phase and phase == DeploymentPhaseConstants.DEPLOYMENT_STOPPED.value: continue - data.append([network.guid, network.name, network.runtime, network_type, phase]) + data.append( + [network.guid, network.name, network.runtime, network_type, phase]) tabulate_data(data, headers) diff --git a/riocli/network/model.py b/riocli/network/model.py index 1607d3cf..3599113d 100644 --- a/riocli/network/model.py +++ b/riocli/network/model.py @@ -1,4 +1,4 @@ -# Copyright 2022 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -27,34 +27,32 @@ class Network(Model): - def __init__(self, *args, **kwargs): self.update(*args, **kwargs) def find_object(self, client: Client) -> bool: try: - network, _ = find_network_name(client, self.metadata.name, self.spec.type, is_resolve_conflict=False) + network, _ = find_network_name(client, self.metadata.name, + self.spec.type, + is_resolve_conflict=False) return network except NetworkNotFound: return False - def create_object(self, client: Client) -> Union[NativeNetwork, RoutedNetwork]: + def create_object(self, client: Client, **kwargs) -> Union[NativeNetwork, RoutedNetwork]: + retry_count = int(kwargs.get('retry_count')) + retry_interval = int(kwargs.get('retry_interval')) if self.spec.type == 'routed': network = self._create_routed_network(client) - network.poll_routed_network_till_ready() + network.poll_routed_network_till_ready(retry_count=retry_count, sleep_interval=retry_interval) return network network = client.create_native_network(self.to_v1(client)) - network.poll_native_network_till_ready() + network.poll_native_network_till_ready(retry_count=retry_count, sleep_interval=retry_interval) return network - def update_object(self, client: Client, obj: Union[RoutedNetwork, NativeNetwork]) -> Any: - # try: - # obj.delete() - # self.create_object(client) - # except Exception as e: - # click.secho(str(e), fg='red') - # raise SystemExit(1) + def update_object(self, client: Client, + obj: Union[RoutedNetwork, NativeNetwork]) -> Any: pass def delete_object(self, client: Client, obj: typing.Any) -> typing.Any: @@ -64,20 +62,26 @@ def delete_object(self, client: Client, obj: typing.Any) -> typing.Any: def pre_process(cls, client: Client, d: Dict) -> None: pass - def to_v1(self, client: Client) -> NativeNetwork: if self.spec.runtime == 'cloud': limits = self._get_limits() parameters = NativeNetworkParameters(limits=limits) else: device = client.get_device(self.spec.deviceGUID) - parameters = NativeNetworkParameters(device=device, - network_interface=self.spec.networkInterface) + parameters = NativeNetworkParameters( + device=device, + network_interface=self.spec.networkInterface) - return NativeNetwork(self.metadata.name, self.spec.runtime.lower(), self.spec.rosDistro, parameters=parameters) + return NativeNetwork( + self.metadata.name, + self.spec.runtime.lower(), + self.spec.rosDistro, + parameters=parameters + ) def _get_limits(self): - return Limits(self.spec.resourceLimits['cpu'], self.spec.resourceLimits['memory']) + return Limits(self.spec.resourceLimits['cpu'], + self.spec.resourceLimits['memory']) def _create_routed_network(self, client: Client) -> RoutedNetwork: if self.spec.runtime == 'cloud': @@ -90,13 +94,19 @@ def _create_routed_network(self, client: Client) -> RoutedNetwork: def _create_cloud_routed_network(self, client: Client) -> RoutedNetwork: limits = self._get_limits() parameters = RoutedNetworkParameters(limits) - return client.create_cloud_routed_network(self.metadata.name, self.spec.rosDistro, True, parameters=parameters) + return client.create_cloud_routed_network(self.metadata.name, + self.spec.rosDistro, True, + parameters=parameters) def _create_device_routed_network(self, client: Client) -> RoutedNetwork: device = client.get_device(self.spec.deviceGUID) - return client.create_device_routed_network(name=self.metadata.name, ros_distro=self.spec.rosDistro, shared=True, - device=device, - network_interface=self.spec.networkInterface) + return client.create_device_routed_network( + name=self.metadata.name, + ros_distro=self.spec.rosDistro, + shared=True, + device=device, + network_interface=self.spec.networkInterface, + ) @staticmethod def validate(data): diff --git a/riocli/network/native_network.py b/riocli/network/native_network.py index 4f1f0036..86aac499 100644 --- a/riocli/network/native_network.py +++ b/riocli/network/native_network.py @@ -14,15 +14,18 @@ import typing import click -from click_spinner import spinner +from rapyuta_io.clients.common_models import Limits from rapyuta_io.clients.native_network import NativeNetwork, Parameters from rapyuta_io.clients.package import Runtime, ROSDistro -from rapyuta_io.clients.common_models import Limits + from riocli.config import new_client -def create_native_network(name: str, ros: str, device_guid: str = None, network_interface: str = None, - cpu: float = 0, memory: int = 0, restart_policy: str = None, **kwargs: typing.Any) -> None: +def create_native_network(name: str, ros: str, device_guid: str = None, + network_interface: str = None, + cpu: float = 0, memory: int = 0, + restart_policy: str = None, + **kwargs: typing.Any) -> None: client = new_client() ros_distro = ROSDistro(ros) @@ -31,7 +34,8 @@ def create_native_network(name: str, ros: str, device_guid: str = None, network_ limit = None if cpu or memory: if device_guid: - raise Exception('Native network for device does not support cpu or memory') + raise Exception( + 'Native network for device does not support cpu or memory') limit = Limits(cpu, memory) device = None @@ -39,12 +43,13 @@ def create_native_network(name: str, ros: str, device_guid: str = None, network_ runtime = Runtime.DEVICE device = client.get_device(device_id=device_guid) - parameters = Parameters(limits=limit, device=device, network_interface=network_interface, + parameters = Parameters(limits=limit, device=device, + network_interface=network_interface, restart_policy=restart_policy) - with spinner(): - client.create_native_network(NativeNetwork(name, runtime=runtime, - ros_distro=ros_distro, - parameters=parameters)) + + client.create_native_network(NativeNetwork(name, runtime=runtime, + ros_distro=ros_distro, + parameters=parameters)) click.secho('Native Network created successfully!', fg='green') diff --git a/riocli/network/routed_network.py b/riocli/network/routed_network.py index 94b5710f..29d1664d 100644 --- a/riocli/network/routed_network.py +++ b/riocli/network/routed_network.py @@ -14,39 +14,44 @@ import typing import click -from click_spinner import spinner from rapyuta_io import ROSDistro +from rapyuta_io.clients.common_models import Limits from rapyuta_io.clients.package import RestartPolicy from rapyuta_io.clients.routed_network import Parameters -from rapyuta_io.clients.common_models import Limits + from riocli.config import new_client -def create_routed_network(name: str, ros: str, device_guid: str = None, network_interface: str = None, - cpu: float = 0, memory: int = 0, restart_policy: str = None, **kwargs: typing.Any) -> None: +def create_routed_network(name: str, ros: str, device_guid: str = None, + network_interface: str = None, + cpu: float = 0, memory: int = 0, + restart_policy: str = None, + **kwargs: typing.Any) -> None: client = new_client() ros_distro = ROSDistro(ros) limit = None if cpu or memory: if device_guid: - raise Exception('Routed network for device does not support cpu or memory') + raise Exception( + 'Routed network for device does not support cpu or memory') limit = Limits(cpu, memory) if restart_policy: restart_policy = RestartPolicy(restart_policy) - with spinner(): - if device_guid: - device = client.get_device(device_id=device_guid) - client.create_device_routed_network(name=name, ros_distro=ros_distro, shared=False, - device=device, - network_interface=network_interface, - restart_policy=restart_policy) - else: - parameters = None if not limit else Parameters(limit) - client.create_cloud_routed_network(name, ros_distro=ros_distro, shared=False, - parameters=parameters) + if device_guid: + device = client.get_device(device_id=device_guid) + client.create_device_routed_network(name=name, ros_distro=ros_distro, + shared=False, + device=device, + network_interface=network_interface, + restart_policy=restart_policy) + else: + parameters = None if not limit else Parameters(limit) + client.create_cloud_routed_network(name, ros_distro=ros_distro, + shared=False, + parameters=parameters) click.secho('Routed Network created successfully!', fg='green') diff --git a/riocli/network/util.py b/riocli/network/util.py index a5a1c70c..4b013dc1 100644 --- a/riocli/network/util.py +++ b/riocli/network/util.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ from rapyuta_io.clients.routed_network import RoutedNetwork from riocli.config import new_client +from riocli.constants import Colors from riocli.utils.selector import show_selection @@ -36,13 +37,19 @@ def decorated(**kwargs: Any): guid = name name = None - if guid: - network, network_type = find_network_guid(client, guid, network_type) - name = network.name + try: + if guid: + network, network_type = find_network_guid( + client, guid, network_type) + name = network.name - if name: - network, network_type = find_network_name(client, name, network_type) - guid = network.guid + if name: + network, network_type = find_network_name( + client, name, network_type) + guid = network.guid + except Exception as e: + click.secho(str(e), fg=Colors.RED) + raise SystemExit(1) from e kwargs['network_type'] = network_type kwargs['network_name'] = name @@ -93,7 +100,8 @@ def find_network_name( return resolve_conflict(routed, native, network_type, is_resolve_conflict) -def find_native_network_name(client: Client, name: str) -> Optional[NativeNetwork]: +def find_native_network_name(client: Client, name: str) -> Optional[ + NativeNetwork]: native_networks = client.list_native_networks() for network in native_networks: phase = network.internal_deployment_status.phase @@ -103,7 +111,8 @@ def find_native_network_name(client: Client, name: str) -> Optional[NativeNetwor return network -def find_routed_network_name(client: Client, name: str) -> Optional[RoutedNetwork]: +def find_routed_network_name(client: Client, name: str) -> Optional[ + RoutedNetwork]: routed_networks = client.get_all_routed_networks() for network in routed_networks: if network.phase == DeploymentPhaseConstants.DEPLOYMENT_STOPPED.value: @@ -140,15 +149,16 @@ def resolve_conflict( 'routed': '{} ({})'.format(routed.name, routed.guid), 'native': '{} ({})'.format(native.name, native.guid), } - choice = show_selection(options, header='Both Routed and Native networks were found with ' - 'the same name') + choice = show_selection(options, + header='Both Routed and Native networks were found with ' + 'the same name') if choice == 'routed': return routed, choice elif choice == 'native': return native, choice else: - click.secho('Invalid choice. Try again', fg='red') + click.secho('Invalid choice. Try again', fg=Colors.RED) raise SystemExit(1) @@ -180,6 +190,7 @@ def __init__(self, message='network not found!'): class NetworkConflict(Exception): - def __init__(self, message='both routed and native networks exist with the same name!'): + def __init__(self, + message='both routed and native networks exist with the same name!'): self.message = message super().__init__(self.message) diff --git a/riocli/organization/__init__.py b/riocli/organization/__init__.py index e43219f5..e3ccb7e9 100644 --- a/riocli/organization/__init__.py +++ b/riocli/organization/__init__.py @@ -14,15 +14,17 @@ import click from click_help_colors import HelpColorsGroup +from riocli.constants import Colors from riocli.organization.list import list_organizations from riocli.organization.select import select_organization +from riocli.organization.users import list_users @click.group( invoke_without_command=False, cls=HelpColorsGroup, - help_headers_color='yellow', - help_options_color='green', + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, ) def organization() -> None: """ @@ -31,5 +33,6 @@ def organization() -> None: pass +organization.add_command(list_users) organization.add_command(list_organizations) organization.add_command(select_organization) diff --git a/riocli/organization/list.py b/riocli/organization/list.py index 5a6c7e63..dae57f7a 100644 --- a/riocli/organization/list.py +++ b/riocli/organization/list.py @@ -13,12 +13,19 @@ # limitations under the License. import click +from click_help_colors import HelpColorsCommand from riocli.config import new_client +from riocli.constants import Colors from riocli.utils import tabulate_data -@click.command('list') +@click.command( + 'list', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.pass_context def list_organizations(ctx: click.Context) -> None: """ @@ -34,7 +41,7 @@ def list_organizations(ctx: click.Context) -> None: current = ctx.obj.data['organization_id'] print_organizations(organizations, current) except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg=Colors.RED) raise SystemExit(1) from e @@ -45,11 +52,12 @@ def print_organizations(organizations, current): data = [] for org in organizations: - fg = None + fg, bold = None, False if org.guid == current: - fg = 'green' + fg = Colors.GREEN + bold = True data.append([ - click.style(v, fg=fg) + click.style(v, fg=fg, bold=bold) for v in (org.name, org.guid, org.creator, org.short_guid) ]) diff --git a/riocli/organization/select.py b/riocli/organization/select.py index 200cb88f..163f5dd2 100644 --- a/riocli/organization/select.py +++ b/riocli/organization/select.py @@ -11,18 +11,35 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import sys + import click +from click_help_colors import HelpColorsCommand from riocli.auth.util import select_project +from riocli.constants import Colors from riocli.project.util import name_to_organization_guid from riocli.utils.context import get_root_context -@click.command('select') +@click.command( + 'select', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.argument('organization-name', type=str) +@click.option('--interactive/--no-interactive', '--interactive/--silent', + is_flag=True, type=bool, default=True, + help='Make the selection interactive') @click.pass_context @name_to_organization_guid -def select_organization(ctx: click.Context, organization_name: str, organization_guid: str) -> None: +def select_organization( + ctx: click.Context, + organization_name: str, + organization_guid: str, + interactive: bool, +) -> None: """ Sets the current organization to the one provided in the argument and prompts you to select a new project @@ -35,12 +52,18 @@ def select_organization(ctx: click.Context, organization_name: str, organization ctx = get_root_context(ctx) if ctx.obj.data['organization_id'] == organization_guid: - click.secho("You are already in the '{}' organization".format(organization_name), fg='green') + click.secho("You are already in the '{}' organization".format( + organization_name), fg=Colors.GREEN) return ctx.obj.data['organization_id'] = organization_guid ctx.obj.data['organization_name'] = organization_name - select_project(ctx.obj, organization=organization_guid) + if sys.stdout.isatty() and interactive: + select_project(ctx.obj, organization=organization_guid) + else: + click.secho( + "Your organization has been set to '{}'".format(organization_name), + fg=Colors.GREEN) ctx.obj.save() diff --git a/riocli/organization/users.py b/riocli/organization/users.py new file mode 100644 index 00000000..2c707fb0 --- /dev/null +++ b/riocli/organization/users.py @@ -0,0 +1,53 @@ +# Copyright 2023 Rapyuta Robotics +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click +from click_help_colors import HelpColorsCommand + +from riocli.constants import Colors, Symbols +from riocli.organization.utils import get_organization_details +from riocli.utils import tabulate_data +from riocli.utils.context import get_root_context + + +@click.command( + 'users', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) +@click.pass_context +def list_users(ctx: click.Context) -> None: + """ + Lists all users in the organization. + """ + ctx = get_root_context(ctx) + + try: + organization = get_organization_details(ctx.obj.data['organization_id']) + except Exception as e: + click.secho('{} Failed to get organization details'.format(Symbols.ERROR), fg=Colors.RED) + raise SystemExit(1) from e + + users = organization.get('users') + users.sort(key=lambda u: u['emailID']) + + data = [[ + u['guid'], + '{} {}'.format(u.get('firstName', '-'), u.get('lastName', '-')), + u['emailID'], + u['state'], + ] for u in users] + + tabulate_data(data, headers=['GUID', 'Name', 'EmailID', 'Status']) diff --git a/riocli/organization/utils.py b/riocli/organization/utils.py new file mode 100644 index 00000000..7efa95ba --- /dev/null +++ b/riocli/organization/utils.py @@ -0,0 +1,55 @@ +# Copyright 2023 Rapyuta Robotics +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import typing + +from rapyuta_io.utils import RestClient +from rapyuta_io.utils.rest_client import HttpMethod + +from riocli.config import Configuration + + +def _api_call( + method: str, + path: typing.Union[str, None] = None, + payload: typing.Union[typing.Dict, None] = None, + load_response: bool = True, +) -> typing.Dict: + config = Configuration() + coreapi_host = config.data.get( + 'core_api_host', + 'https://gaapiserver.apps.rapyuta.io' + ) + + url = '{}/api/organization'.format(coreapi_host) + if path: + url = '{}/{}'.format(url, path) + + headers = config.get_auth_header() + response = RestClient(url).method(method).headers(headers).execute( + payload=payload) + + data = None + + if load_response: + data = response.json() + + if not response.ok: + err_msg = data.get('error') + raise Exception(err_msg) + + return data + + +def get_organization_details(organization_guid: str) -> typing.Dict: + return _api_call(HttpMethod.GET, '{}/get'.format(organization_guid)) diff --git a/riocli/package/model.py b/riocli/package/model.py index de881fde..f670ff6c 100644 --- a/riocli/package/model.py +++ b/riocli/package/model.py @@ -40,7 +40,7 @@ def find_object(self, client: Client): return obj - def create_object(self, client: Client): + def create_object(self, client: Client, **kwargs): pkg_object = munchify({ 'name': 'default', 'packageVersion': 'v1.0.0', diff --git a/riocli/parameter/__init__.py b/riocli/parameter/__init__.py index ed99fbec..b6f3f780 100644 --- a/riocli/parameter/__init__.py +++ b/riocli/parameter/__init__.py @@ -15,6 +15,7 @@ import click from click_help_colors import HelpColorsGroup +from riocli.constants import Colors from riocli.parameter.apply import apply_configurations from riocli.parameter.delete import delete_configurations from riocli.parameter.diff import diff_configurations @@ -23,26 +24,22 @@ from riocli.parameter.upload import upload_configurations -# from riocli.parameter.diff import diff_configurations -# from riocli.parameter.download import download_configurations - - @click.group( invoke_without_command=False, cls=HelpColorsGroup, - help_headers_color='yellow', - help_options_color='green', + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, ) def parameter() -> None: """ - Define groups of executables to deploy together + Manage configuration parameters for your devices and deployments """ pass -parameter.add_command(upload_configurations) -parameter.add_command(download_configurations) parameter.add_command(diff_configurations) parameter.add_command(apply_configurations) -parameter.add_command(list_configuration_trees) parameter.add_command(delete_configurations) +parameter.add_command(upload_configurations) +parameter.add_command(download_configurations) +parameter.add_command(list_configuration_trees) diff --git a/riocli/parameter/apply.py b/riocli/parameter/apply.py index 22c368c3..0db0facb 100644 --- a/riocli/parameter/apply.py +++ b/riocli/parameter/apply.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,22 +14,35 @@ import typing import click +from click_help_colors import HelpColorsCommand +from yaspin import kbi_safe_yaspin from riocli.config import new_client +from riocli.constants import Colors, Symbols from riocli.utils import tabulate_data, print_separator -@click.command('apply') +@click.command( + 'apply', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.option('--devices', type=click.STRING, multiple=True, default=(), help='Device names to apply configurations') @click.option('--tree-names', type=click.STRING, multiple=True, default=None, help='Tree names to apply') @click.option('--retry-limit', type=click.INT, default=0, help='Retry limit') -@click.option('-f', '--force', '--silent', 'silent', is_flag=True, type=click.BOOL, default=False, +@click.option('-f', '--force', '--silent', 'silent', is_flag=True, + type=click.BOOL, default=False, help="Skip confirmation") -def apply_configurations(devices: typing.List, tree_names: str = None, retry_limit: int = 0, - silent: bool = False) -> None: +def apply_configurations( + devices: typing.List, + tree_names: str = None, + retry_limit: int = 0, + silent: bool = False, +) -> None: """ Apply a set of configurations to a list of devices """ @@ -40,27 +53,38 @@ def apply_configurations(devices: typing.List, tree_names: str = None, retry_lim device_map = {d.name: d for d in online_devices} if devices: - device_ids = {device_map[d].uuid: d for d in devices if d in device_map} + device_ids = {device_map[d].uuid: d for d in devices if + d in device_map} else: device_ids = {v.uuid: k for k, v in device_map.items()} if not device_ids: - click.secho("invalid devices or no device is currently online", fg='red') + click.secho( + "{} Invalid devices or no device is currently online".format( + Symbols.ERROR), + fg=Colors.RED) raise SystemExit(1) - click.secho('Online Devices: {}'.format(','.join(device_ids.values())), fg='green') + click.secho('Online Devices: {}'.format(','.join(device_ids.values())), + fg=Colors.GREEN) - printable_tree_names = ','.join(tree_names) if tree_names != "" else "*all*" - click.secho('Config Trees: {}'.format(printable_tree_names), fg='green') + printable_tree_names = ','.join( + tree_names) if tree_names != "" else "*all*" + + click.secho('Config Trees: {}'.format(printable_tree_names), + fg=Colors.GREEN) if not silent: click.confirm( "Do you want to apply the configurations?", default=True, abort=True) - response = client.apply_parameters(list(device_ids.keys()), - list(tree_names), - retry_limit) + with kbi_safe_yaspin(text='Applying parameters...') as spinner: + response = client.apply_parameters( + list(device_ids.keys()), + list(tree_names), + retry_limit + ) print_separator() @@ -72,6 +96,6 @@ def apply_configurations(devices: typing.List, tree_names: str = None, retry_lim tabulate_data(result, headers=["Device", "Success"]) except IOError as e: - click.secho(str(e.__traceback__), fg='red') - click.secho(str(e), fg='red') + click.secho(str(e.__traceback__), fg=Colors.RED) + click.secho(str(e), fg=Colors.RED) raise SystemExit(1) from e diff --git a/riocli/parameter/delete.py b/riocli/parameter/delete.py index eb5bf60f..0c1d84e2 100644 --- a/riocli/parameter/delete.py +++ b/riocli/parameter/delete.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,30 +13,47 @@ # limitations under the License. import click +from click_help_colors import HelpColorsCommand from rapyuta_io.utils.rest_client import HttpMethod +from yaspin import kbi_safe_yaspin +from riocli.constants import Colors, Symbols from riocli.parameter.utils import _api_call -@click.command('delete') -@click.option('-f', '--force', '--silent', 'silent', is_flag=True, default=False, +@click.command( + 'delete', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) +@click.option('-f', '--force', '--silent', 'silent', is_flag=True, + default=False, help="Skip confirmation") @click.argument('tree', type=click.STRING) -def delete_configurations(tree: str, silent: bool = False) -> None: +def delete_configurations( + tree: str, + silent: bool = False +) -> None: """ - Deletes the Configuration Parameter Tree. + Deletes the configuration parameter tree. """ click.secho('Configuration Parameter {} will be deleted'.format(tree)) if not silent: click.confirm('Do you want to proceed?', default=True, abort=True) - try: - data = _api_call(HttpMethod.DELETE, name=tree) - if data.get('data') != 'ok': - raise Exception('Something went wrong!') + with kbi_safe_yaspin(text='Deleting...', timer=True) as spinner: + try: + data = _api_call(HttpMethod.DELETE, name=tree) + if data.get('data') != 'ok': + raise Exception('Failed to delete configuration') - except IOError as e: - click.secho(str(e.__traceback__), fg='red') - click.secho(str(e), fg='red') - raise SystemExit(1) + spinner.text = click.style( + 'Configuration deleted successfully.', + fg=Colors.GREEN) + spinner.green.ok(Symbols.SUCCESS) + except IOError as e: + spinner.text = click.style(e, fg=Colors.RED) + spinner.red.fail(Symbols.ERROR) + raise SystemExit(1) diff --git a/riocli/parameter/diff.py b/riocli/parameter/diff.py index 52ca1449..a3b38a4f 100644 --- a/riocli/parameter/diff.py +++ b/riocli/parameter/diff.py @@ -24,48 +24,65 @@ from typing import Tuple import click +from click_help_colors import HelpColorsCommand from rapyuta_io.utils.error import APIError, InternalServerError from riocli.config import new_client +from riocli.constants import Colors from riocli.parameter.utils import filter_trees -@click.command('diff') +@click.command( + 'diff', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.option('--tree-names', type=click.STRING, multiple=True, default=None, help='Tree names to fetch') @click.argument('path', type=click.Path(exists=True), required=False) def diff_configurations(path: str, tree_names: Tuple = None) -> None: """ - Diff between the Local and Cloud Configuration Trees. + Diff between the local and cloud configuration trees. """ trees = filter_trees(path, tree_names) try: client = new_client() with TemporaryDirectory(prefix='riocli-') as tmp_path: - client.download_configurations(tmp_path, tree_names=list(tree_names)) + client.download_configurations(tmp_path, + tree_names=list(tree_names)) for tree in trees: - left_tree, right_tree = os.path.join(tmp_path, tree), os.path.join(path, tree) + left_tree = os.path.join(tmp_path, tree) + right_tree = os.path.join(path, tree) diff_tree(left_tree, right_tree) except (APIError, InternalServerError) as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg=Colors.RED) raise SystemExit(1) def diff_tree(left: str, right: str) -> None: comp = dircmp(left, right) + for f in comp.common_dirs: + remote_dir, local_dir = os.path.join(comp.left, f), os.path.join( + comp.right, f) + diff_tree(remote_dir, local_dir) + for f in comp.diff_files: - remote_file, local_file = os.path.join(comp.left, f), os.path.join(comp.right, f) + remote_file, local_file = os.path.join(comp.left, f), os.path.join( + comp.right, f) diff_file(remote_file, local_file) for f in comp.right_only: - remote_file, local_file = os.path.join(comp.left, f), os.path.join(comp.right, f) + remote_file, local_file = os.path.join(comp.left, f), os.path.join( + comp.right, f) changed_file(remote_file, local_file, right_only=True) for f in comp.left_only: - remote_file, local_file = os.path.join(comp.left, f), os.path.join(comp.right, f) + remote_file, local_file = os.path.join(comp.left, f), os.path.join( + comp.right, f) changed_file(remote_file, local_file, left_only=True) @@ -80,13 +97,15 @@ def diff_file(left: str, right: str): changed_file(left, right, binary=True) return - diff = unified_diff(left_lines, right_lines, fromfile=left, tofile=right, lineterm='') + diff = unified_diff(left_lines, right_lines, fromfile=left, tofile=right, + lineterm='\n') for line in diff: - click.secho(line) + click.secho(line, nl=False) -def changed_file(left: str, right: str, left_only: bool = False, right_only: bool = False, binary: bool = False): +def changed_file(left: str, right: str, left_only: bool = False, + right_only: bool = False, binary: bool = False): click.secho('--- {}'.format(left)) click.secho('+++ {}'.format(right)) diff --git a/riocli/parameter/download.py b/riocli/parameter/download.py index cf514bb2..cc653c29 100644 --- a/riocli/parameter/download.py +++ b/riocli/parameter/download.py @@ -12,14 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. import typing +from os.path import abspath from tempfile import mkdtemp -from xmlrpc.client import Boolean import click -from click_spinner import spinner +from click_help_colors import HelpColorsCommand from riocli.config import new_client -from riocli.parameter.utils import display_trees +from riocli.constants import Symbols, Colors +from riocli.utils.spinner import with_spinner # ----------------------------------------------------------------------------- @@ -30,29 +31,48 @@ # ----------------------------------------------------------------------------- -@click.command('download') -@click.option('--tree-names', type=click.STRING, multiple=True, default=None, help='Tree names to fetch') -@click.option('--overwrite', '--delete-existing', 'delete_existing', is_flag=True, +@click.command( + 'download', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) +@click.option('--tree-names', type=click.STRING, multiple=True, default=None, + help='Tree names to fetch') +@click.option('--overwrite', '--delete-existing', 'delete_existing', + is_flag=True, help='Overwrite existing parameter tree') @click.argument('path', type=click.Path(exists=True), required=False) -def download_configurations(path: str, tree_names: typing.Tuple[str] = None, delete_existing: Boolean = False) -> None: +@with_spinner(text='Download configurations...', timer=True) +def download_configurations( + path: str, + tree_names: typing.Tuple[str] = None, + delete_existing: bool = False, + spinner=None +) -> None: """ - Download the Configuration Parameter trees. + Download configuration parameter trees from rapyuta.io """ if path is None: - # Not using the Context Manager because we need to persist the Temporary directory. + # Not using the Context Manager because + # we need to persist the temporary directory. path = mkdtemp() - click.secho('Downloading at {}'.format(path)) + spinner.write('Downloading at {}'.format(abspath(path))) try: client = new_client() - with spinner(): - client.download_configurations(path, tree_names=list(tree_names), - delete_existing_trees=delete_existing) + client.download_configurations( + path, + tree_names=list(tree_names), + delete_existing_trees=delete_existing + ) - click.secho('✅ Configuration Parameters downloaded successfully', fg='green') + spinner.text = click.style("Configurations downloaded successfully.", + fg=Colors.GREEN) + spinner.green.ok(Symbols.SUCCESS) except Exception as e: - click.secho(e, fg='red') + spinner.text = click.style(e, fg=Colors.RED) + spinner.red.fail(Symbols.ERROR) raise SystemExit(1) diff --git a/riocli/parameter/list.py b/riocli/parameter/list.py index 5be5ebee..2995cdf5 100644 --- a/riocli/parameter/list.py +++ b/riocli/parameter/list.py @@ -12,13 +12,20 @@ # See the License for the specific language governing permissions and # limitations under the License. import click +from click_help_colors import HelpColorsCommand from rapyuta_io.utils.rest_client import HttpMethod +from riocli.constants import Colors from riocli.parameter.utils import _api_call from riocli.utils import tabulate_data -@click.command('list') +@click.command( + 'list', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) def list_configuration_trees() -> None: """ List the Configuration Parameter Trees. @@ -26,12 +33,12 @@ def list_configuration_trees() -> None: try: data = _api_call(HttpMethod.GET) if 'data' not in data: - raise Exception('Something went wrong!') + raise Exception('Failed to list configurations') trees = [[tree] for tree in data['data']] tabulate_data(trees, headers=['Tree Name']) except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg=Colors.RED) raise SystemExit(1) diff --git a/riocli/parameter/upload.py b/riocli/parameter/upload.py index d8f6fcaf..1cd59157 100644 --- a/riocli/parameter/upload.py +++ b/riocli/parameter/upload.py @@ -15,39 +15,63 @@ import typing import click -from click_spinner import spinner +from click_help_colors import HelpColorsCommand +from yaspin import kbi_safe_yaspin from riocli.config import new_client +from riocli.constants import Colors, Symbols from riocli.parameter.utils import filter_trees, display_trees -@click.command('upload') -@click.option('--tree-names', type=click.STRING, multiple=True, default=[], help='Directory names to upload') -@click.option('--recreate', '--delete-existing', 'delete_existing', is_flag=True, +@click.command( + 'upload', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) +@click.option('--tree-names', type=click.STRING, multiple=True, default=[], + help='Directory names to upload') +@click.option('--recreate', '--delete-existing', 'delete_existing', + is_flag=True, help='Overwrite existing parameter tree') -@click.option('-f', '--force', '--silent', 'silent', is_flag=True, default=False, help="Skip confirmation") +@click.option('-f', '--force', '--silent', 'silent', is_flag=True, + default=False, help="Skip confirmation") @click.argument('path', type=click.Path(exists=True)) -def upload_configurations(path: str, tree_names: typing.Tuple[str] = None, delete_existing: bool = False, - silent: bool = False) -> None: +def upload_configurations( + path: str, + tree_names: typing.Tuple[str] = None, + delete_existing: bool = False, + silent: bool = False +) -> None: """ - Upload a set of Configuration Parameter directory trees. + Upload a directories as configuration parameters. """ trees = filter_trees(path, tree_names) - click.secho('Following Trees will be uploaded') - click.secho('') + click.secho('Following configuration trees will be uploaded') + click.secho() display_trees(path, trees) if not silent: click.confirm('Do you want to proceed?', default=True, abort=True) - try: - client = new_client() - with spinner(): - client.upload_configurations(rootdir=path, tree_names=trees, delete_existing_trees=delete_existing, - as_folder=True) + client = new_client() - click.secho('✅ Configuration parameters uploaded successfully', fg='green') - except Exception as e: - click.secho(str(e), fg='red') - raise SystemExit(1) + with kbi_safe_yaspin(text="Uploading configurations...", + timer=True) as spinner: + try: + client.upload_configurations( + rootdir=path, + tree_names=trees, + delete_existing_trees=delete_existing, + as_folder=True + ) + + spinner.text = click.style( + 'Configuration parameters uploaded successfully', + fg=Colors.GREEN) + spinner.green.ok(Symbols.SUCCESS) + except Exception as e: + spinner.text = click.style(e, fg=Colors.RED) + spinner.red.fail(Symbols.ERROR) + raise SystemExit(1) diff --git a/riocli/parameter/utils.py b/riocli/parameter/utils.py index 9de43fca..ed527c92 100644 --- a/riocli/parameter/utils.py +++ b/riocli/parameter/utils.py @@ -22,9 +22,13 @@ from rapyuta_io.utils import RestClient from riocli.config import Configuration +from riocli.constants import Colors -def filter_trees(root_dir: str, tree_names: typing.Tuple[str]) -> typing.List[str]: +def filter_trees( + root_dir: str, + tree_names: typing.Tuple[str] +) -> typing.List[str]: trees = [] for each in os.listdir(root_dir): full_path = os.path.join(root_dir, each) @@ -43,15 +47,19 @@ def filter_trees(root_dir: str, tree_names: typing.Tuple[str]) -> typing.List[st return trees -def display_trees(root_dir: str, trees: typing.List[str] = []) -> None: +def display_trees(root_dir: str, trees: typing.List[str]) -> None: + trees = trees or [] for each in trees: tree_out = display_tree(os.path.join(root_dir, each), string_rep=True) - click.secho(tree_out, fg='yellow') + click.secho(tree_out, fg=Colors.YELLOW) -def _api_call(method: str, name: typing.Union[str, None] = None, - payload: typing.Union[typing.Dict, None] = None, load_response: bool = True, - ) -> typing.Any: +def _api_call( + method: str, + name: typing.Union[str, None] = None, + payload: typing.Union[typing.Dict, None] = None, + load_response: bool = True, +) -> typing.Any: config = Configuration() catalog_host = config.data.get( 'core_api_host', 'https://gaapiserver.apps.rapyuta.io') @@ -59,7 +67,8 @@ def _api_call(method: str, name: typing.Union[str, None] = None, if name: url = '{}/{}'.format(url, name) headers = config.get_auth_header() - response = RestClient(url).method(method).headers( headers).execute(payload=payload) + response = RestClient(url).method(method).headers(headers).execute( + payload=payload) data = None err_msg = 'error in the api call' if load_response: @@ -77,5 +86,8 @@ def phase3(self) -> None: # shallow=False enables the behaviour of matching the File content. The # original dircmp Class only compares os.Stat between the files, and # gives no way to modify the behaviour. - f_comp = filecmp.cmpfiles(self.left, self.right, self.common_files, shallow=False) + f_comp = filecmp.cmpfiles(self.left, + self.right, + self.common_files, + shallow=False) self.same_files, self.diff_files, self.funny_files = f_comp diff --git a/riocli/project/create.py b/riocli/project/create.py index f1bc61c4..491323ed 100644 --- a/riocli/project/create.py +++ b/riocli/project/create.py @@ -12,20 +12,33 @@ # See the License for the specific language governing permissions and # limitations under the License. import click -from click_spinner import spinner +from click_help_colors import HelpColorsCommand from riocli.config import new_v2_client +from riocli.constants import Symbols, Colors from riocli.project.util import name_to_organization_guid +from riocli.utils.spinner import with_spinner -@click.command('create') +@click.command( + 'create', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.argument('project-name', type=str) @click.option('--organization', 'organization_name', help='Pass organization name for which project needs to be created. Default will be current organization') @click.pass_context @name_to_organization_guid -def create_project(ctx: click.Context, project_name: str, - organization_guid: str, organization_name: str) -> None: +@with_spinner(text="Creating project...") +def create_project( + ctx: click.Context, + project_name: str, + organization_guid: str, + organization_name: str, + spinner=None, +) -> None: """ Creates a new project """ @@ -45,9 +58,12 @@ def create_project(ctx: click.Context, project_name: str, try: client = new_v2_client(with_project=False) - with spinner(): - client.create_project(payload) - click.secho('Project created successfully!', fg='green') + client.create_project(payload) + spinner.text = click.style( + 'Project created successfully.', fg=Colors.GREEN) + spinner.green.ok(Symbols.SUCCESS) except Exception as e: - click.secho('failed to create project: {}'.format(e), fg='red') + spinner.text = click.style( + 'Failed to create project: {}'.format(e), Colors.RED) + spinner.red.fail(Symbols.ERROR) raise SystemExit(1) diff --git a/riocli/project/delete.py b/riocli/project/delete.py index 16637def..73800679 100644 --- a/riocli/project/delete.py +++ b/riocli/project/delete.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,31 +12,47 @@ # See the License for the specific language governing permissions and # limitations under the License. import click -from click_spinner import spinner +from click_help_colors import HelpColorsCommand from riocli.config import new_v2_client +from riocli.constants import Symbols, Colors from riocli.project.util import name_to_guid +from riocli.utils.spinner import with_spinner -@click.command('delete') +@click.command( + 'delete', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.option('--force', '-f', '--silent', 'force', is_flag=True, help='Skip confirmation') @click.argument('project-name', type=str) @name_to_guid -def delete_project(force: bool, project_name: str, project_guid: str) -> None: +@with_spinner(text="Deleting project...") +def delete_project( + force: bool, + project_name: str, + project_guid: str, + spinner=None, +) -> None: """ - Deletes the project from the Platform + Deletes a project """ if not force: - click.confirm( - 'Deleting project {} ({})'.format(project_name, project_guid), - abort=True) + with spinner.hidden(): + click.confirm('Deleting project {} ({})'.format( + project_name, project_guid), abort=True) try: client = new_v2_client() - with spinner(): - client.delete_project(project_guid) - click.secho('Project deleted successfully!', fg='green') + client.delete_project(project_guid) + spinner.text = click.style( + 'Project deleted successfully.', fg=Colors.GREEN) + spinner.green.ok(Symbols.SUCCESS) except Exception as e: - click.secho('failed to delete project: {}'.format(e), fg='red') + spinner.text = click.style( + 'Failed to delete project: {}'.format(e), Colors.RED) + spinner.red.fail(Symbols.ERROR) raise SystemExit(1) diff --git a/riocli/project/features/__init__.py b/riocli/project/features/__init__.py index d6147e5e..95e011ba 100644 --- a/riocli/project/features/__init__.py +++ b/riocli/project/features/__init__.py @@ -14,14 +14,15 @@ import click from click_help_colors import HelpColorsGroup +from riocli.constants import Colors from riocli.project.features.vpn import vpn @click.group( invoke_without_command=False, cls=HelpColorsGroup, - help_headers_color='yellow', - help_options_color='green', + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, ) def features(): """ diff --git a/riocli/project/features/vpn.py b/riocli/project/features/vpn.py index 07cd7447..bf3fe261 100644 --- a/riocli/project/features/vpn.py +++ b/riocli/project/features/vpn.py @@ -12,16 +12,30 @@ # See the License for the specific language governing permissions and # limitations under the License. import click +from click_help_colors import HelpColorsCommand from riocli.config import new_v2_client +from riocli.constants import Colors, Symbols from riocli.project.util import name_to_guid +from riocli.utils.spinner import with_spinner -@click.command('vpn') +@click.command( + 'vpn', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.argument('project-name', type=str) @click.argument('enable', type=bool) @name_to_guid -def vpn(project_name: str, project_guid: str, enable: bool) -> None: +@with_spinner() +def vpn( + project_name: str, + project_guid: str, + enable: bool, + spinner=None, +) -> None: """ Enable or disable VPN on a project @@ -40,10 +54,14 @@ def vpn(project_name: str, project_guid: str, enable: bool) -> None: } } + state = 'Enabling' if enable else 'Disabling' + spinner.text = click.style('{} VPN...'.format(state), fg=Colors.YELLOW) + try: - r = client.update_project(project_guid, body) + client.update_project(project_guid, body) + spinner.text = click.style('Done', fg=Colors.GREEN) + spinner.green.ok(Symbols.SUCCESS) except Exception as e: - click.secho("❌ Failed: {}".format(e), fg='red') + spinner.text = click.style("Failed: {}".format(e), fg=Colors.RED) + spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e - - click.secho("✅ VPN has been {}".format("enabled" if enable else "disabled"), fg='green') diff --git a/riocli/project/inspect.py b/riocli/project/inspect.py index fda1f807..6c6426fd 100644 --- a/riocli/project/inspect.py +++ b/riocli/project/inspect.py @@ -12,14 +12,21 @@ # See the License for the specific language governing permissions and # limitations under the License. import click +from click_help_colors import HelpColorsCommand from munch import unmunchify from riocli.config import new_v2_client +from riocli.constants import Colors from riocli.project.util import name_to_guid from riocli.utils import inspect_with_format -@click.command('inspect') +@click.command( + 'inspect', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.option('--format', '-f', 'format_type', default='yaml', type=click.Choice(['json', 'yaml'], case_sensitive=False)) @click.argument('project-name', type=str) @@ -34,5 +41,5 @@ def inspect_project(format_type: str, project_name: str, project = client.get_project(project_guid) inspect_with_format(unmunchify(project), format_type) except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg=Colors.RED) raise SystemExit(1) diff --git a/riocli/project/list.py b/riocli/project/list.py index c63955ee..20faa534 100644 --- a/riocli/project/list.py +++ b/riocli/project/list.py @@ -14,15 +14,22 @@ import typing import click +from click_help_colors import HelpColorsCommand from munch import unmunchify from rapyuta_io import Project from riocli.config import new_v2_client +from riocli.constants import Colors from riocli.project.util import name_to_organization_guid from riocli.utils import tabulate_data -@click.command('list') +@click.command( + 'list', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.option('--organization', 'organization_name', help='Pass organization name for which project needs to be created. Default will be current organization') @click.option('--wide', '-w', is_flag=True, default=False, @@ -41,7 +48,7 @@ def list_project(ctx: click.Context = None, organization_guid: str = None, current = ctx.obj.data.get('project_id', None) _display_project_list(projects, current, show_header=True, wide=wide) except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg=Colors.RED) raise SystemExit(1) @@ -57,14 +64,14 @@ def _display_project_list(projects: typing.List[Project], current: str = None, data = [] for project in projects: metadata = project.metadata - fg = None + fg, bold = None, False if metadata.guid == current: - fg = 'green' - + fg = Colors.GREEN + bold = True row = [metadata.guid, metadata.name, project.status.status] if wide: row.extend([metadata.createdAt, metadata.creatorGUID, unmunchify(project.spec.features)]) - data.append([click.style(v, fg=fg) for v in row]) + data.append([click.style(v, fg=fg, bold=bold) for v in row]) tabulate_data(data, headers) diff --git a/riocli/project/model.py b/riocli/project/model.py index e7af5df5..d7bd804f 100644 --- a/riocli/project/model.py +++ b/riocli/project/model.py @@ -23,6 +23,7 @@ PROJECT_READY_TIMEOUT = 150 + class Project(Model): def __init__(self, *args, **kwargs): @@ -37,7 +38,7 @@ def find_object(self, client: Client) -> bool: return obj - def create_object(self, client: Client) -> typing.Any: + def create_object(self, client: Client, **kwargs) -> typing.Any: client = new_v2_client() # convert to a dict and remove the ResolverCache @@ -52,7 +53,8 @@ def create_object(self, client: Client) -> typing.Any: r = client.create_project(project) try: - wait(self.is_ready, timeout_seconds=PROJECT_READY_TIMEOUT, sleep_seconds=(1, 30, 2)) + wait(self.is_ready, timeout_seconds=PROJECT_READY_TIMEOUT, + sleep_seconds=(1, 30, 2)) except TimeoutExpired as e: raise e diff --git a/riocli/project/select.py b/riocli/project/select.py index 321cc4d6..4a565c5a 100644 --- a/riocli/project/select.py +++ b/riocli/project/select.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,24 +12,38 @@ # See the License for the specific language governing permissions and # limitations under the License. import click +from click_help_colors import HelpColorsCommand +from riocli.constants import Colors, Symbols from riocli.project.util import name_to_guid from riocli.utils.context import get_root_context -@click.command('select') +@click.command( + 'select', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.argument('project-name', type=str) @name_to_guid @click.pass_context -def select_project(ctx: click.Context, project_name: str, - project_guid: str) -> None: +def select_project( + ctx: click.Context, + project_name: str, + project_guid: str, +) -> None: """ Sets the given project in the CLI context. All other resources use this project to act upon. """ ctx = get_root_context(ctx) + ctx.obj.data['project_id'] = project_guid ctx.obj.data['project_name'] = project_name ctx.obj.save() - click.secho( - 'Project {} ({}) is selected!'.format(project_name, project_guid), - fg='green') + + click.secho('{} Project {} ({}) is selected!'.format( + Symbols.SUCCESS, + project_name, + project_guid), + fg=Colors.GREEN) diff --git a/riocli/project/util.py b/riocli/project/util.py index 75ba5fe9..5c910929 100644 --- a/riocli/project/util.py +++ b/riocli/project/util.py @@ -18,6 +18,7 @@ from rapyuta_io import Client from riocli.config import new_client, new_v2_client +from riocli.constants import Colors from riocli.utils.selector import show_selection from riocli.v2client import Client as v2Client @@ -40,7 +41,7 @@ def decorated(**kwargs: typing.Any): try: guid = find_project_guid(client, name) except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg=Colors.RED) raise SystemExit(1) kwargs['project_name'] = name @@ -110,7 +111,7 @@ def decorated(*args: typing.Any, **kwargs: typing.Any): else: guid = find_organization_guid(client, name) except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg=Colors.RED) raise SystemExit(1) kwargs['organization_name'] = name kwargs['organization_guid'] = guid diff --git a/riocli/secret/__init__.py b/riocli/secret/__init__.py index 5bde0c8c..94623ed9 100644 --- a/riocli/secret/__init__.py +++ b/riocli/secret/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,6 +14,7 @@ import click from click_help_colors import HelpColorsGroup +from riocli.constants import Colors from riocli.secret.create import create_secret from riocli.secret.delete import delete_secret from riocli.secret.import_secret import import_secret @@ -24,8 +25,8 @@ @click.group( invoke_without_command=False, cls=HelpColorsGroup, - help_headers_color='yellow', - help_options_color='green', + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, ) def secret() -> None: """ diff --git a/riocli/secret/delete.py b/riocli/secret/delete.py index 06222c88..d9d74e5b 100644 --- a/riocli/secret/delete.py +++ b/riocli/secret/delete.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,29 +12,40 @@ # See the License for the specific language governing permissions and # limitations under the License. import click -from click_spinner import spinner from riocli.config import new_client +from riocli.constants import Colors, Symbols from riocli.secret.util import name_to_guid +from riocli.utils.spinner import with_spinner +from click_help_colors import HelpColorsCommand - -@click.command('delete') +@click.command( + 'delete', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.option('--force', '-f', '--silent', 'force', is_flag=True, default=False, help='Skip confirmation') @click.argument('secret-name', type=str) @name_to_guid -def delete_secret(force: str, secret_name: str, secret_guid: str) -> None: +@with_spinner(text='Deleting secret...') +def delete_secret(force: str, secret_name: str, secret_guid: str, spinner=None) -> None: """ - Deletes the secret resource from the Platform + Deletes a secret """ - if not force: - click.confirm('Deleting secret {} ({})'.format(secret_name, secret_guid), abort=True) + with spinner.hidden(): + if not force: + click.confirm( + 'Deleting secret {} ({})'.format(secret_name, secret_guid), + abort=True) try: client = new_client() - with spinner(): - client.delete_secret(secret_guid) - click.secho('Secret deleted successfully!', fg='green') + client.delete_secret(secret_guid) + spinner.text = click.style('Secret deleted successfully.', fg=Colors.GREEN) + spinner.green.ok(Symbols.SUCCESS) except Exception as e: - click.secho(str(e), fg='red') - raise SystemExit(1) + spinner.text = click.style('Failed to delete secret: {}'.format(e), fg=Colors.RED) + spinner.red.fail(Symbols.ERROR) + raise SystemExit(1) from e diff --git a/riocli/secret/inspect.py b/riocli/secret/inspect.py index ee02ac4b..daf4ff0a 100644 --- a/riocli/secret/inspect.py +++ b/riocli/secret/inspect.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,21 +12,28 @@ # See the License for the specific language governing permissions and # limitations under the License. import click +from click_help_colors import HelpColorsCommand from rapyuta_io import Secret from riocli.config import new_client +from riocli.constants import Colors from riocli.secret.util import name_to_guid from riocli.utils import inspect_with_format -@click.command('inspect') +@click.command( + 'inspect', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.option('--format', '-f', 'format_type', default='yaml', type=click.Choice(['json', 'yaml'], case_sensitive=False)) @click.argument('secret-name', type=str) @name_to_guid def inspect_secret(format_type: str, secret_name: str, secret_guid: str) -> None: """ - Inspect the secret resource + Inspect a secret """ try: client = new_client() @@ -34,7 +41,7 @@ def inspect_secret(format_type: str, secret_name: str, secret_guid: str) -> None data = make_secret_inspectable(secret) inspect_with_format(data, format_type) except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg=Colors.RED) raise SystemExit(1) diff --git a/riocli/secret/list.py b/riocli/secret/list.py index abddd557..c2820fdf 100644 --- a/riocli/secret/list.py +++ b/riocli/secret/list.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,9 +18,15 @@ from riocli.config import new_client from riocli.utils import tabulate_data - - -@click.command('list') +from riocli.constants import Colors +from click_help_colors import HelpColorsCommand + +@click.command( + 'list', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.option('--secret-type', '-t', default=['docker', 'source'], multiple=True, help='Types to filter the list of Secret [default: docker,source]') def list_secrets(secret_type: typing.Union[str, typing.Tuple[str]]) -> None: @@ -33,7 +39,7 @@ def list_secrets(secret_type: typing.Union[str, typing.Tuple[str]]) -> None: secrets = sorted(secrets, key=lambda s: s.name.lower()) _display_secret_list(secrets, secret_type, show_header=True) except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg=Colors.RED) raise SystemExit(1) diff --git a/riocli/secret/model.py b/riocli/secret/model.py index 556defaa..59ad3ef2 100644 --- a/riocli/secret/model.py +++ b/riocli/secret/model.py @@ -1,4 +1,4 @@ -# Copyright 2022 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,28 +13,34 @@ # limitations under the License. import typing -from rapyuta_io import Secret as v1Secret, SecretConfigDocker, \ - SecretConfigSourceBasicAuth, \ - SecretConfigSourceSSHAuth, Client +from rapyuta_io import ( + Client, + Secret as v1Secret, + SecretConfigDocker, + SecretConfigSourceBasicAuth, + SecretConfigSourceSSHAuth, +) from riocli.jsonschema.validate import load_schema from riocli.model import Model class Secret(Model): - def __init__(self, *args, **kwargs): self.update(*args, **kwargs) def find_object(self, client: Client) -> bool: - _, secret = self.rc.find_depends( - {'kind': 'secret', 'nameOrGUID': self.metadata.name}) + _, secret = self.rc.find_depends({ + 'kind': 'secret', + 'nameOrGUID': self.metadata.name + }) + if not secret: return False return secret - def create_object(self, client: Client) -> v1Secret: + def create_object(self, client: Client, **kwargs) -> v1Secret: secret = client.create_secret(self.to_v1()) return secret @@ -51,8 +57,12 @@ def to_v1(self) -> v1Secret: return self._git_secret_to_v1() def _docker_secret_to_v1(self) -> v1Secret: - config = SecretConfigDocker(self.spec.docker.username, self.spec.docker.password, self.spec.docker.email, - self.spec.docker.registry) + config = SecretConfigDocker( + self.spec.docker.username, + self.spec.docker.password, + self.spec.docker.email, + self.spec.docker.registry, + ) return v1Secret(self.metadata.name, config) def _git_secret_to_v1(self) -> v1Secret: @@ -60,7 +70,11 @@ def _git_secret_to_v1(self) -> v1Secret: config = SecretConfigSourceSSHAuth(self.spec.git.privateKey) elif self.spec.git.authMethod == 'HTTP/S Basic Auth': ca_cert = self.spec.git.get('ca_cert', None) - config = SecretConfigSourceBasicAuth(self.spec.git.username, self.spec.git.password, ca_cert=ca_cert) + config = SecretConfigSourceBasicAuth( + self.spec.git.username, + self.spec.git.password, + ca_cert=ca_cert + ) elif self.spec.git.authMethod == 'HTTP/S Token Auth': # TODO(ankit): Implement it once SDK has support for it. raise Exception('token-based secret is not supported yet!') diff --git a/riocli/static_route/__init__.py b/riocli/static_route/__init__.py index 5d318f65..490b728d 100644 --- a/riocli/static_route/__init__.py +++ b/riocli/static_route/__init__.py @@ -14,6 +14,7 @@ import click from click_help_colors import HelpColorsGroup +from riocli.constants import Colors from riocli.static_route.create import create_static_route from riocli.static_route.delete import delete_static_route from riocli.static_route.inspect import inspect_static_route @@ -24,8 +25,8 @@ @click.group( invoke_without_command=False, cls=HelpColorsGroup, - help_headers_color='yellow', - help_options_color='green', + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, ) def static_route() -> None: """ diff --git a/riocli/static_route/create.py b/riocli/static_route/create.py index 2acfb9e9..a70c0af5 100644 --- a/riocli/static_route/create.py +++ b/riocli/static_route/create.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,22 +12,32 @@ # See the License for the specific language governing permissions and # limitations under the License. import click -from click_spinner import spinner +from click_help_colors import HelpColorsCommand from riocli.config import new_client +from riocli.constants import Colors, Symbols +from riocli.utils.spinner import with_spinner -@click.command('create') +@click.command( + 'create', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.argument('prefix', type=str) -def create_static_route(prefix: str) -> None: +@with_spinner(text="Creating static route...") +def create_static_route(prefix: str, spinner=None) -> None: """ - Creates a new instance of static route + Creates a new static route """ try: client = new_client() - with spinner(): - route = client.create_static_route(prefix) - click.secho("Static Route created successfully for URL {}".format(route.urlString), fg='green') + route = client.create_static_route(prefix) + spinner.text = click.style( + 'Static Route created successfully for URL {}'.format(route.urlString), fg=Colors.GREEN) + spinner.green.ok(Symbols.SUCCESS) except Exception as e: - click.secho(str(e), fg='red') + spinner.text = click.style('Failed to create static route: {}'.format(e), fg=Colors.RED) + spinner.red.fail(Symbols.ERROR) raise SystemExit(1) diff --git a/riocli/static_route/delete.py b/riocli/static_route/delete.py index a858ae92..45f39e95 100644 --- a/riocli/static_route/delete.py +++ b/riocli/static_route/delete.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,30 +12,46 @@ # See the License for the specific language governing permissions and # limitations under the License. import click -from click_spinner import spinner +from click_help_colors import HelpColorsCommand from riocli.config import new_client +from riocli.constants import Colors, Symbols from riocli.static_route.util import name_to_guid +from riocli.utils.spinner import with_spinner -@click.command('delete') +@click.command( + 'delete', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.option('--force', '-f', is_flag=True, default=False, help='Skip confirmation') @click.argument('static-route', type=str) @name_to_guid -def delete_static_route(static_route: str, static_route_guid: str, force: bool) -> None: +@with_spinner(text="Deleting static route...") +def delete_static_route( + static_route: str, + static_route_guid: str, + force: bool, + spinner=None, +) -> None: """ - Deletes the static route resource from the Platform + Deletes a static route """ - - if not force: - click.confirm('Deleting static route {} ({})'.format(static_route, static_route_guid), - abort=True) + with spinner.hidden(): + if not force: + click.confirm( + 'Deleting static route {} ({})'.format( + static_route, static_route_guid), abort=True) try: client = new_client() - with spinner(): - client.delete_static_route(static_route_guid) - click.secho('Static Route deleted successfully!', fg='green') + client.delete_static_route(static_route_guid) + spinner.text = click.style( + 'Static Route deleted successfully ', fg=Colors.GREEN) + spinner.green.ok(Symbols.SUCCESS) except Exception as e: - click.secho(str(e), fg='red') + spinner.text = click.style('Failed to delete static route: {}'.format(e), fg=Colors.RED) + spinner.red.fail(Symbols.ERROR) raise SystemExit(1) diff --git a/riocli/static_route/inspect.py b/riocli/static_route/inspect.py index 5cd65174..a6b10a20 100644 --- a/riocli/static_route/inspect.py +++ b/riocli/static_route/inspect.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,21 +12,33 @@ # See the License for the specific language governing permissions and # limitations under the License. import click +from click_help_colors import HelpColorsCommand from rapyuta_io.clients.static_route import StaticRoute from riocli.config import new_client +from riocli.constants import Colors from riocli.static_route.util import name_to_guid from riocli.utils import inspect_with_format -@click.command('inspect') +@click.command( + 'inspect', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.option('--format', '-f', 'format_type', - type=click.Choice(['json', 'yaml'], case_sensitive=True), default='yaml') + type=click.Choice(['json', 'yaml'], case_sensitive=True), + default='yaml') @click.argument('static-route', type=str) @name_to_guid -def inspect_static_route(format_type: str, static_route: str, static_route_guid: str) -> None: +def inspect_static_route( + format_type: str, + static_route: str, + static_route_guid: str +) -> None: """ - Inspect the static route resource + Inspect a static route """ try: client = new_client() @@ -34,7 +46,7 @@ def inspect_static_route(format_type: str, static_route: str, static_route_guid: data = make_static_route_inspectable(route) inspect_with_format(data, format_type) except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg=Colors.RED) raise SystemExit(1) diff --git a/riocli/static_route/list.py b/riocli/static_route/list.py index ab77cde2..b4317fa4 100644 --- a/riocli/static_route/list.py +++ b/riocli/static_route/list.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,13 +11,23 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from typing import List + import click +from click_help_colors import HelpColorsCommand +from rapyuta_io.clients.static_route import StaticRoute from riocli.config import new_client -from riocli.static_route.util import repr_static_routes +from riocli.constants import Colors +from riocli.utils import tabulate_data -@click.command('list') +@click.command( + 'list', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) def list_static_routes() -> None: """ List the static routes in the selected project @@ -25,7 +35,23 @@ def list_static_routes() -> None: try: client = new_client() routes = client.get_all_static_routes() - repr_static_routes(routes) + _display_routes_list(routes) except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg=Colors.RED) raise SystemExit(1) + + +def _display_routes_list(routes: List[StaticRoute]) -> None: + headers = ['Route ID', 'Name', 'URL', 'Creator', 'CreatedAt'] + + data = [] + for route in routes: + data.append([ + route.guid, + route.urlPrefix, + route.urlString, + route.creator, + route.CreatedAt, + ]) + + tabulate_data(data, headers) diff --git a/riocli/static_route/model.py b/riocli/static_route/model.py index 421672f2..698b2001 100644 --- a/riocli/static_route/model.py +++ b/riocli/static_route/model.py @@ -1,4 +1,4 @@ -# Copyright 2022 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -25,14 +25,17 @@ def __init__(self, *args, **kwargs): self.update(*args, **kwargs) def find_object(self, client: Client) -> bool: - _, static_route = self.rc.find_depends({'kind': 'staticroute', - 'nameOrGUID': self.metadata.name}) + _, static_route = self.rc.find_depends({ + 'kind': 'staticroute', + 'nameOrGUID': self.metadata.name + }) + if not static_route: return False return static_route - def create_object(self, client: Client) -> v1StaticRoute: + def create_object(self, client: Client, **kwargs) -> v1StaticRoute: static_route = client.create_static_route(self.metadata.name) return static_route diff --git a/riocli/static_route/open.py b/riocli/static_route/open.py index 348596b4..3162ae0b 100644 --- a/riocli/static_route/open.py +++ b/riocli/static_route/open.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,12 +12,19 @@ # See the License for the specific language governing permissions and # limitations under the License. import click +from click_help_colors import HelpColorsCommand from riocli.config import new_client +from riocli.constants import Colors from riocli.static_route.util import name_to_guid -@click.command('open') +@click.command( + 'open', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.argument('static-route', type=str) @name_to_guid def open_static_route(static_route, static_route_guid) -> None: @@ -29,5 +36,5 @@ def open_static_route(static_route, static_route_guid) -> None: route = client.get_static_route(static_route_guid) click.launch(url='https://{}'.format(route.urlString), wait=False) except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg=Colors.RED) raise SystemExit(1) diff --git a/riocli/static_route/util.py b/riocli/static_route/util.py index 5cefecb5..c0b702fb 100644 --- a/riocli/static_route/util.py +++ b/riocli/static_route/util.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,9 +14,7 @@ import functools import typing -import click from rapyuta_io import Client -from rapyuta_io.clients.static_route import StaticRoute from riocli.config import new_client @@ -59,23 +57,6 @@ def find_static_route_guid(client: Client, name: str) -> str: raise StaticRouteNotFound() -def repr_static_routes(routes: typing.List[StaticRoute]) -> None: - header = '{:<36} {:<25} {:36} {:36} {:32}'.format( - 'Static Route ID', - 'Name', - 'Full URL', - 'Creator', - 'Created At', - ) - click.echo(click.style(header, fg='yellow')) - for route in routes: - click.secho( - '{:<36} {:<25} {:36} {:36} {:32}'. - format(route.guid, route.urlPrefix, route.urlString, route.creator, - route.CreatedAt)) - - class StaticRouteNotFound(Exception): - def __init__(self, message='secret not found'): - self.message = message - super().__init__(self.message) + def __init__(self): + super().__init__('static route not found') diff --git a/riocli/usergroup/__init__.py b/riocli/usergroup/__init__.py index e69de29b..69839067 100644 --- a/riocli/usergroup/__init__.py +++ b/riocli/usergroup/__init__.py @@ -0,0 +1,39 @@ +# Copyright 2023 Rapyuta Robotics +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click +from click_help_colors import HelpColorsGroup + +from riocli.constants import Colors +from riocli.usergroup.delete import delete_usergroup +from riocli.usergroup.inspect import inspect_usergroup +from riocli.usergroup.list import list_usergroup + + +@click.group( + invoke_without_command=False, + cls=HelpColorsGroup, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) +def usergroup() -> None: + """ + Manage usergroups on rapyuta.io + """ + pass + + +usergroup.add_command(list_usergroup) +usergroup.add_command(inspect_usergroup) +usergroup.add_command(delete_usergroup) diff --git a/riocli/usergroup/delete.py b/riocli/usergroup/delete.py new file mode 100644 index 00000000..ad7eeccb --- /dev/null +++ b/riocli/usergroup/delete.py @@ -0,0 +1,60 @@ +# Copyright 2023 Rapyuta Robotics +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click +from click_help_colors import HelpColorsCommand +from yaspin.api import Yaspin + +from riocli.config import new_client +from riocli.constants import Colors, Symbols +from riocli.usergroup.util import name_to_guid +from riocli.utils.spinner import with_spinner + + +@click.command( + 'delete', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) +@click.option('--force', '-f', '--silent', 'force', is_flag=True, + default=False, help='Skip confirmation') +@click.argument('group-name') +@click.pass_context +@with_spinner(text="Deleting user group...") +@name_to_guid +def delete_usergroup( + ctx: click.Context, + group_name: str, + group_guid: str, + force: bool, + spinner: Yaspin = None, +) -> None: + """ + Delete usergroup from organization + """ + if not force: + with spinner.hidden(): + click.confirm('Deleting usergroup {} ({})'.format(group_name, group_guid), abort=True) + + try: + client = new_client() + org_guid = ctx.obj.data.get('organization_id') + client.delete_usergroup(org_guid, group_guid) + spinner.text = click.style('User group deleted successfully.', fg=Colors.GREEN) + spinner.green.ok(Symbols.SUCCESS) + except Exception as e: + spinner.text = click.style('Failed to delete usergroup: {}'.format(e), Colors.RED) + spinner.red.fail(Symbols.ERROR) + raise SystemExit(1) from e diff --git a/riocli/usergroup/inspect.py b/riocli/usergroup/inspect.py new file mode 100644 index 00000000..31d1b68f --- /dev/null +++ b/riocli/usergroup/inspect.py @@ -0,0 +1,84 @@ +# Copyright 2023 Rapyuta Robotics +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import typing + +import click +from click_help_colors import HelpColorsCommand +from rapyuta_io.clients.project import User, Project + +from riocli.config import new_client +from riocli.constants import Colors +from riocli.usergroup.util import name_to_guid +from riocli.utils import inspect_with_format + + +@click.command( + 'inspect', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) +@click.option('--format', '-f', 'format_type', default='yaml', + type=click.Choice(['json', 'yaml'], case_sensitive=False)) +@click.argument('group-name') +@click.pass_context +@name_to_guid +def inspect_usergroup(ctx: click.Context, format_type: str, group_name: str, group_guid: str, spinner=None) -> None: + """ + Inspect the usergroup resource + """ + try: + client = new_client() + org_guid = ctx.obj.data.get('organization_id') + usergroup = client.get_usergroup(org_guid, group_guid) + data = make_usergroup_inspectable(usergroup) + inspect_with_format(data, format_type) + except Exception as e: + click.secho(str(e), fg=Colors.RED) + raise SystemExit(1) + + +def make_usergroup_inspectable(usergroup: typing.Any): + return { + 'name': usergroup.name, + 'description': usergroup.description, + 'guid': usergroup.guid, + 'creator': usergroup.creator, + 'members': [make_user_inspectable(member) for member in usergroup.members], + 'admins': [make_user_inspectable(admin) for admin in usergroup.admins], + 'projects': [make_project_inspectable(project) for project in getattr(usergroup, 'projects') or []] + } + + +def make_user_inspectable(u: User): + return { + 'guid': u.guid, + 'firstName': u.first_name, + 'lastName': u.last_name, + 'emailID': u.email_id, + 'state': u.state, + 'organizations': u.organizations + } + + +def make_project_inspectable(p: Project): + return { + 'ID': p.id, + 'CreatedAt': p.created_at, + 'UpdatedAt': p.updated_at, + 'DeletedAt': p.deleted_at, + 'name': p.name, + 'guid': p.guid, + 'creator': p.creator + } diff --git a/riocli/usergroup/list.py b/riocli/usergroup/list.py index e69de29b..551c237b 100644 --- a/riocli/usergroup/list.py +++ b/riocli/usergroup/list.py @@ -0,0 +1,65 @@ +# Copyright 2023 Rapyuta Robotics +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import typing + +import click +from click_help_colors import HelpColorsCommand +from riocli.config import new_client +from riocli.constants import Colors +from riocli.utils import tabulate_data + + +@click.command( + 'list', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) +@click.pass_context +def list_usergroup(ctx: click.Context) -> None: + """ + List all user groups in selected organization + """ + + try: + client = new_client() + org_guid = ctx.obj.data.get('organization_id') + user_groups = client.list_usergroups(org_guid) + _display_usergroup_list(user_groups) + except Exception as e: + click.secho(str(e), fg=Colors.RED) + raise SystemExit(1) + + +def _display_usergroup_list(usergroups: typing.Any, show_header: bool = True): + headers = [] + if show_header: + headers = ( + 'ID', 'Name', 'Creator', 'Members', 'Projects', 'Description' + ) + + data = [ + [ + group.guid, + group.name, + group.creator, + len(group.members) if group.members else 0, + len(group.projects) if group.projects else 0, + group.description + ] + for group in usergroups + ] + + tabulate_data(data, headers) diff --git a/riocli/usergroup/model.py b/riocli/usergroup/model.py new file mode 100644 index 00000000..da135ad9 --- /dev/null +++ b/riocli/usergroup/model.py @@ -0,0 +1,161 @@ +# Copyright 2023 Rapyuta Robotics +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import typing + +from munch import unmunchify +from rapyuta_io import Client + +from riocli.config import new_v2_client +from riocli.jsonschema.validate import load_schema +from riocli.model import Model +from riocli.organization.utils import get_organization_details + +USER_GUID = 'guid' +USER_EMAIL = 'emailID' + + +class UserGroup(Model): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + v2client = new_v2_client() + organization_details = get_organization_details(self.metadata.organization) + user_projects = v2client.list_projects(self.metadata.organization) + + self.project_name_to_guid_map = {p['metadata']['name']: p['metadata']['guid'] for p in user_projects} + self.user_email_to_guid_map = {user[USER_EMAIL]: user[USER_GUID] for user in organization_details['users']} + + def unmunchify(self) -> typing.Dict: + """Unmuchify self""" + usergroup = unmunchify(self) + usergroup.pop('rc', None) + usergroup.pop('project_name_to_guid_map', None) + usergroup.pop('user_email_to_guid_map', None) + + return usergroup + + def find_object(self, client: Client) -> typing.Any: + group_guid, usergroup = self.rc.find_depends({ + 'kind': self.kind.lower(), + 'nameOrGUID': self.metadata.name, + 'organization': self.metadata.organization + }) + + if not usergroup: + return False + + return usergroup + + def create_object(self, client: Client, **kwargs) -> typing.Any: + usergroup = self.unmunchify() + payload = self._modify_payload(usergroup) + # Inject the user group name in the payload + payload['spec']['name'] = usergroup['metadata']['name'] + return client.create_usergroup(self.metadata.organization, payload['spec']) + + def update_object(self, client: Client, obj: typing.Any) -> typing.Any: + payload = self._modify_payload(self.unmunchify()) + payload = self._create_update_payload(obj, payload) + return client.update_usergroup(self.metadata.organization, obj.guid, payload) + + def delete_object(self, client: Client, obj: typing.Any) -> typing.Any: + return client.delete_usergroup(self.metadata.organization, obj.guid) + + def _modify_payload(self, group: typing.Dict) -> typing.Dict: + for entity in ('members', 'admins'): + for u in group['spec'].get(entity, []): + if USER_GUID in u: + continue + u[USER_GUID] = self.user_email_to_guid_map.get(u[USER_EMAIL]) + u.pop(USER_EMAIL) + + for p in group['spec'].get('projects', []): + if 'guid' in p: + continue + p['guid'] = self.project_name_to_guid_map.get(p['name']) + p.pop('name') + + return group + + @classmethod + def pre_process(cls, client: Client, d: typing.Dict) -> None: + pass + + @staticmethod + def validate(data): + schema = load_schema('usergroup') + schema.validate(data) + + @staticmethod + def _create_update_payload(old: typing.Any, new: typing.Dict) -> typing.Dict: + payload = { + 'name': old.name, + 'guid': old.guid, + 'description': new['spec']['description'], + 'update': { + 'members': {'add': [], 'remove': []}, + 'projects': {'add': [], 'remove': []}, + 'admins': {'add': [], 'remove': []} + } + } + + entity_sets = { + "members": { + "old": set(), + "new": set(), + }, + "admins": { + "old": set(), + "new": set(), + }, + "projects": { + "old": set(), + "new": set(), + } + } + + for entity in ('members', 'projects', 'admins'): + # Assure that the group creator is not removed + old_set = {i.guid for i in (getattr(old, entity) or []) if i.guid != old.creator} + new_set = {i['guid'] for i in new['spec'].get(entity, [])} + + entity_sets[entity]["old"] = old_set + entity_sets[entity]["new"] = new_set + + for entity in ('projects', 'admins'): + new = entity_sets[entity]['new'] + old = entity_sets[entity]['old'] + added = new - old + removed = old - new + + payload['update'][entity]['add'] = [{'guid': guid} for guid in added] + payload['update'][entity]['remove'] = [{'guid': guid} for guid in removed] + + # Handle special cases in the members section separately + new_members = entity_sets['members']['new'] + old_members = entity_sets['members']['old'] + added_members = new_members - old_members + removed_members = old_members - new_members + + # Additional handling to avoid active admins becoming a part of + # removed members set which leads to their removal altogether. + removed_members = removed_members - entity_sets['admins']['new'] + + payload['update']['members']['add'] = [{'guid': guid} for guid in added_members] + payload['update']['members']['remove'] = [{'guid': guid} for guid in removed_members] + + # This is a special case where admins are not added to the membership list + # And as a consequence they don't show up in the group. This will fix that. + payload['update']['members']['add'].extend(payload['update']['admins']['add']) + + return payload diff --git a/riocli/usergroup/util.py b/riocli/usergroup/util.py new file mode 100644 index 00000000..861eb66f --- /dev/null +++ b/riocli/usergroup/util.py @@ -0,0 +1,84 @@ +# Copyright 2023 Rapyuta Robotics +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import functools +import typing + +import click +from rapyuta_io import Client + +from riocli.config import new_client +from riocli.constants import Colors + + +def name_to_guid(f: typing.Callable) -> typing.Callable: + @functools.wraps(f) + def decorated(*args: typing.Any, **kwargs: typing.Any) -> None: + try: + client = new_client() + except Exception as e: + click.secho(str(e), fg=Colors.RED) + raise SystemExit(1) + + group_name = kwargs.pop('group_name') + group_guid = None + + ctx = args[0] + org_guid = ctx.obj.data.get('organization_id') + + if group_name.startswith('group-'): + group_guid = group_name + group_name = None + + if group_name is None: + group_name = get_usergroup_name(client, org_guid, group_guid) + + if group_guid is None: + try: + group_guid = find_usergroup_guid(client, org_guid, group_name) + except Exception as e: + click.secho(str(e), fg=Colors.RED) + raise SystemExit(1) + + kwargs['group_name'] = group_name + kwargs['group_guid'] = group_guid + f(*args, **kwargs) + + return decorated + + +def get_usergroup_name(client: Client, org_guid: str, group_guid: str) -> str: + try: + usergroup = client.get_usergroup(org_guid, group_guid) + except Exception as e: + click.secho(str(e), fg=Colors.RED) + raise SystemExit(1) + + return usergroup.name + + +def find_usergroup_guid(client: Client, org_guid, group_name: str) -> str: + user_groups = client.list_usergroups(org_guid=org_guid) + + for g in user_groups: + if g.name == group_name: + return g.guid + + raise UserGroupNotFound() + + +class UserGroupNotFound(Exception): + def __init__(self, message='usergroup not found!'): + self.message = message + super().__init__(self.message) diff --git a/riocli/utils/__init__.py b/riocli/utils/__init__.py index 6d8f5577..792b5b91 100644 --- a/riocli/utils/__init__.py +++ b/riocli/utils/__init__.py @@ -12,19 +12,28 @@ # See the License for the specific language governing permissions and # limitations under the License. import json +import os import random import shlex import string import subprocess +import sys import typing +from pathlib import Path from shutil import get_terminal_size +from tempfile import TemporaryDirectory from uuid import UUID import click +import requests +import semver import yaml from click_help_colors import HelpColorsGroup +from munch import munchify from tabulate import tabulate +from riocli.constants import Colors, Symbols + def inspect_with_format(obj: typing.Any, format_type: str): if format_type == 'json': @@ -63,13 +72,18 @@ def run_bash_with_return_code(cmd, bg=False) -> (str, int): return stdout.strip(), ret_code -def run_bash(cmd, bg=False) -> str: +def run_bash(cmd, bg=False): """ Runs a bash command and returns the output only """ - output, _ = run_bash_with_return_code(cmd, bg) - - return output + cmd_parts = shlex.split(cmd) + if bg is True: + bg_output = subprocess.Popen(cmd_parts) + output = str(bg_output.stdout).strip() + else: + output = subprocess.run(cmd_parts, stdout=subprocess.PIPE, + check=True).stdout.decode('utf-8') + return output.strip() riocli_group_opts = { @@ -81,11 +95,13 @@ def run_bash(cmd, bg=False) -> str: def random_string(letter_count, digit_count): - str1 = ''.join((random.choice(string.ascii_letters) for x in range(letter_count))) + str1 = ''.join( + (random.choice(string.ascii_letters) for x in range(letter_count))) str1 += ''.join((random.choice(string.digits) for x in range(digit_count))) sam_list = list(str1) # it converts the string to list. - random.shuffle(sam_list) # It uses a random.shuffle() function to shuffle the string. + random.shuffle( + sam_list) # It uses a random.shuffle() function to shuffle the string. final_string = ''.join(sam_list) return final_string @@ -118,7 +134,8 @@ def is_valid_uuid(uuid_to_test, version=4): return str(uuid_obj) == uuid_to_test -def tabulate_data(data: typing.List[typing.List], headers: typing.List[str] = None): +def tabulate_data(data: typing.List[typing.List], + headers: typing.List[str] = None): """ Prints data in tabular format """ @@ -138,3 +155,93 @@ def print_separator(color: str = 'blue'): """ col, _ = get_terminal_size() click.secho(" " * col, bg=color) + + +def is_pip_installation() -> bool: + return 'python' in sys.executable + + +def check_for_updates(current_version: str) -> tuple[bool, str]: + try: + package_info = requests.get( + 'https://pypi.org/pypi/rapyuta-io-cli/json').json() + except Exception as e: + click.secho('Failed to fetch upstream package info: {}'.format(e), + fg=Colors.RED) + raise SystemExit(1) from e + + upstream_version = package_info.get('info', {}).get('version') + + current_version = semver.Version.parse(current_version) + available = semver.Version.parse(upstream_version).compare(current_version) + + return available > 0, upstream_version + + +def pip_install_cli( + version: str, + force_reinstall: bool = False, +) -> subprocess.CompletedProcess: + """ + Installs the given rapyuta-io-cli version using pip + """ + if not version: + raise ValueError('version cannot by empty.') + + try: + semver.Version.parse(version) + except ValueError as err: + raise err + + package_name = 'rapyuta-io-cli=={}'.format(version) + + # https://pip.pypa.io/en/latest/user_guide/#using-pip-from-your-program + command = [sys.executable, '-m', 'pip', 'install', package_name] + if force_reinstall: + command.append('--force-reinstall') + + return subprocess.run(command, check=True) + + +def update_appimage(version: str): + """ + Updates the AppImage locally + """ + if not version: + raise ValueError('version cannot be empty') + + if os.getuid() != 0: + click.secho( + '{} Please run this as the root user.'.format(Symbols.WARNING), + fg=Colors.YELLOW) + raise SystemExit(1) + + # URL to get the latest release metadata + url = 'https://api.github.com/repos/rapyuta-robotics/rapyuta-io-cli/releases/latest' + + try: + response = requests.get(url) + data = munchify(response.json()) + except Exception as e: + click.secho('Failed to fetch release info: {}'.format(e), + fg=Colors.RED) + raise SystemExit(1) from e + + asset = None + for a in data.get('assets', []): + if 'AppImage' in a.name and version in a.name: + asset = a + break + + if asset is None: + raise Exception( + 'Failed to retrieve the download URL for the latest AppImage') + + with TemporaryDirectory() as tmp: + # Download and save the binary in a temp dir + response = requests.get(asset.browser_download_url) + save_to = Path(tmp) / 'rio' + save_to.write_bytes(response.content) + os.chmod(save_to, 0o755) + # Now replace the current executable with the new file + os.rename(save_to, sys.executable) diff --git a/riocli/vpn/connect.py b/riocli/vpn/connect.py index d569d880..94cb6a10 100644 --- a/riocli/vpn/connect.py +++ b/riocli/vpn/connect.py @@ -17,13 +17,16 @@ from datetime import datetime, timedelta import click +from click_help_colors import HelpColorsCommand from munch import Munch +from yaspin.api import Yaspin from riocli.config import new_v2_client +from riocli.constants import Colors, Symbols from riocli.utils import run_bash_with_return_code +from riocli.utils.spinner import with_spinner from riocli.v2client import Client as v2Client from riocli.vpn.util import ( - is_tailscale_installed, is_tailscale_up, stop_tailscale, install_vpn_tools, @@ -33,72 +36,99 @@ ) -@click.command('connect') +@click.command( + 'connect', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.pass_context -def connect(ctx: click.Context): +@with_spinner(text="Connecting...") +def connect(ctx: click.Context, spinner: Yaspin = None): """ Connect to the current project's VPN network """ try: - if not is_tailscale_installed(): - click.confirm( - click.style('VPN tools are not installed. Do you want ' - 'to install them now?', fg='yellow'), - default=True, abort=True) + with spinner.hidden(): install_vpn_tools() client = new_v2_client() if not is_vpn_enabled_in_project( client, ctx.obj.data.get('project_id')): - click.secho('⚠ VPN is not enabled in the project. ' - 'Please ask the organization or project ' - 'creator to enable VPN', fg='yellow') + spinner.write( + click.style('{} VPN is not enabled in the project. ' + 'Please ask the organization or project ' + 'creator to enable VPN'.format(Symbols.WAITING), + fg=Colors.YELLOW)) raise SystemExit(1) - if is_tailscale_up(): - click.confirm('The VPN client is already running. ' - 'Do you want to stop it and connect to the VPN of ' - 'the current project?', default=False, abort=True) - success = stop_tailscale() - if not success: - msg = ('❌ Failed to stop tailscale. Please run the following ' - 'commands manually\n sudo tailscale down\n sudo ' - 'tailscale logout') - click.secho(msg, fg='yellow') - raise SystemExit(1) - - click.secho('🛈 VPN is enabled in the project ({})'.format( - ctx.obj.data.get('project_name')), fg='cyan') - - if not start_tailscale(ctx, client): - click.secho('❌ Failed to connect to the project VPN', fg='red') + with spinner.hidden(): + if is_tailscale_up(): + click.confirm( + '{} The VPN client is already running. ' + 'Do you want to stop it and connect to the VPN of ' + 'the current project?'.format(Symbols.WARNING), + default=False, abort=True) + success = stop_tailscale() + if not success: + msg = ( + '{} Failed to stop tailscale. Please run the ' + 'following commands manually\n sudo tailscale down\n ' + 'sudo tailscale logout'.format(Symbols.ERROR)) + click.secho(msg, fg=Colors.YELLOW) + raise SystemExit(1) + + spinner.write( + click.style( + '{} VPN is enabled in the project ({})'.format( + Symbols.INFO, ctx.obj.data.get('project_name')), + fg=Colors.CYAN)) + + if not start_tailscale(ctx, client, spinner): + click.secho('{} Failed to connect to the project VPN'.format( + Symbols.ERROR), fg=Colors.RED) raise SystemExit(1) - click.secho('✅ You are now connected to the project\'s VPN', - fg='green') + spinner.green.text = 'You are now connected to the project\'s VPN' + spinner.green.ok(Symbols.SUCCESS) + except click.exceptions.Abort as e: + spinner.red.text = 'Aborted!' + spinner.red.fail(Symbols.ERROR) + raise SystemExit(1) from e except Exception as e: - click.secho(str(e), fg='red') + spinner.red.text = str(e) + spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e -def start_tailscale(ctx: click.Context, client: v2Client) -> bool: +def start_tailscale( + ctx: click.Context, + client: v2Client, + spinner: Yaspin, +) -> bool: cmd = ('sudo tailscale up --auth-key={} --login-server={}' ' --reset --force-reauth --accept-routes --accept-dns' ' --advertise-tags={} --timeout=30s') - args = generate_tailscale_args(ctx, client) + args = generate_tailscale_args(ctx, client, spinner) command = cmd.format(args.HEADSCALE_PRE_AUTH_KEY, args.HEADSCALE_URL, args.HEADSCALE_ACL_TAG) output, code = run_bash_with_return_code(command) if code != 0: - click.secho('❌ Failed to start vpn client', fg='red') + spinner.write( + click.style('{} Failed to start vpn client'.format(Symbols.ERROR), + fg=Colors.RED)) return False return True -def generate_tailscale_args(ctx: click.Context, client: v2Client) -> Munch: +def generate_tailscale_args( + ctx: click.Context, + client: v2Client, + spinner: Yaspin, +) -> Munch: vpn_instance = 'rio-internal-headscale' binding_name = '{}-{}'.format(ctx.obj.machine_id, int(time.time())) @@ -124,14 +154,14 @@ def generate_tailscale_args(ctx: click.Context, client: v2Client) -> Munch: } } - click.secho('⌛ Generating a token to join the network...') + spinner.text = 'Generating a token to join the network...' + try: # We may end up creating multiple throwaway tokens in the database. # But that's okay and something that we can live with binding = client.create_instance_binding(vpn_instance, binding=body) return binding.spec.environment except Exception as e: - click.secho(str(e), fg='red') raise SystemExit(1) from e diff --git a/riocli/vpn/disconnect.py b/riocli/vpn/disconnect.py index a38e6b8f..0903b178 100644 --- a/riocli/vpn/disconnect.py +++ b/riocli/vpn/disconnect.py @@ -13,32 +13,41 @@ # limitations under the License. import click +from click_help_colors import HelpColorsCommand -from riocli.vpn.util import is_tailscale_installed, install_vpn_tools, \ - is_tailscale_up, stop_tailscale +from riocli.constants import Colors, Symbols +from riocli.vpn.util import ( + install_vpn_tools, + is_tailscale_up, + stop_tailscale +) -@click.command('disconnect') +@click.command( + 'disconnect', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.pass_context def disconnect(ctx: click.Context): """ Disconnect from the project's VPN network """ try: - if not is_tailscale_installed(): - click.confirm( - click.style('VPN tools are not installed. Do you want ' - 'to install them now?', fg='yellow'), - default=True, abort=True) - install_vpn_tools() + install_vpn_tools() if is_tailscale_up() and not stop_tailscale(): - click.secho('❌ Failed to disconnect from VPN. ' - 'Although, trying again may work.', - fg='red') + click.secho( + '{} Failed to disconnect from VPN. ' + 'Although, trying again may work.'.format(Symbols.ERROR), + fg=Colors.RED) raise SystemExit(1) - click.secho("✅ You have been disconnected from the project's VPN", fg='green') + click.secho( + '{} You have been disconnected from the project\'s VPN'.format( + Symbols.SUCCESS), + fg=Colors.GREEN) except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg=Colors.RED) raise SystemExit(1) from e diff --git a/riocli/vpn/ping.py b/riocli/vpn/ping.py index d4e1f816..59e393c4 100644 --- a/riocli/vpn/ping.py +++ b/riocli/vpn/ping.py @@ -13,9 +13,12 @@ # limitations under the License. import click +from click_help_colors import HelpColorsCommand +from yaspin.api import Yaspin +from riocli.constants import Colors, Symbols +from riocli.utils.spinner import with_spinner from riocli.vpn.util import ( - is_tailscale_installed, install_vpn_tools, is_tailscale_up, get_tailscale_status, @@ -23,39 +26,52 @@ ) -@click.command('ping-all') +@click.command( + 'ping-all', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.pass_context -def ping_all(ctx: click.Context): +@with_spinner(text="Pinging all peers...") +def ping_all(ctx: click.Context, spinner: Yaspin = None): """ Ping all the peers in the network """ try: - if not is_tailscale_installed(): - click.confirm( - click.style('VPN tools are not installed. Do you want ' - 'to install them now?', fg='yellow'), - default=True, abort=True) + with spinner.hidden(): install_vpn_tools() if not is_tailscale_up(): - click.secho('You are not connected to the VPN', fg='green') + spinner.text = click.style( + 'You are not connected to the VPN', fg=Colors.YELLOW) + spinner.yellow.ok(Symbols.WARNING) return - ping_all_peers() + ping_all_peers(spinner) - click.secho('✅ Ping complete.', fg='green') + spinner.text = click.style('Ping complete', fg=Colors.GREEN) + spinner.green.ok(Symbols.SUCCESS) except Exception as e: - click.secho(str(e), fg='red') + spinner.text = click.style(str(e), fg=Colors.RED) + spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e -def ping_all_peers(): +def ping_all_peers(spinner: Yaspin): s = get_tailscale_status() peers = s.get('Peer', {}) for _, v in peers.items(): - click.secho("⌛ Pinging: {}...".format(v.get('HostName')), fg='blue') + # Do not waste time pinging + # offline nodes + if not v.get('Online'): + continue + + spinner.text = 'Pinging: {}...'.format( + click.style(v.get('HostName'), italic=True) + ) ips = v.get('TailscaleIPs') for ip in ips: tailscale_ping(ip) diff --git a/riocli/vpn/status.py b/riocli/vpn/status.py index 7055b2cc..1e252001 100644 --- a/riocli/vpn/status.py +++ b/riocli/vpn/status.py @@ -13,19 +13,25 @@ # limitations under the License. import click +from click_help_colors import HelpColorsCommand from riocli.config import new_v2_client +from riocli.constants import Colors, Symbols from riocli.utils import tabulate_data from riocli.vpn.util import ( install_vpn_tools, - is_tailscale_installed, is_tailscale_up, get_tailscale_status, is_vpn_enabled_in_project, ) -@click.command('status') +@click.command( + 'status', + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) @click.option('--wide', '-w', is_flag=True, default=False, help='Print more details', type=bool) @click.pass_context @@ -34,35 +40,36 @@ def status(ctx: click.Context, wide: bool = False): Check VPN status """ try: - if not is_tailscale_installed(): - click.confirm(click.style( - 'VPN tools are not installed. ' - 'Do you want to install them now?', - fg='yellow'), default=True, abort=True) - install_vpn_tools() + install_vpn_tools() client = new_v2_client() if not is_vpn_enabled_in_project( client, ctx.obj.data.get('project_id')): - click.secho('⚠ VPN is not enabled in the project. ' + click.secho('{} VPN is not enabled in the project. ' 'Please ask the organization or project ' - 'creator to enable VPN', fg='yellow') + 'creator to enable VPN'.format(Symbols.WARNING), + fg=Colors.YELLOW) raise SystemExit(1) - click.secho('🛈 VPN is enabled in the project ({})'.format( - ctx.obj.data.get('project_name')), fg='cyan') + click.secho( + '{} VPN is enabled in the project ({})'.format( + Symbols.INFO, ctx.obj.data.get('project_name')), + fg=Colors.CYAN) click.echo() if not is_tailscale_up(): - click.secho('You are not connected to the VPN', fg='green') + click.secho( + '{} You are not connected to the VPN'.format(Symbols.WARNING), + fg=Colors.YELLOW) return display_vpn_status(wide) - click.secho('🛈 You are connected to the VPN.', fg='green') + click.secho('{} You are connected to the VPN.'.format(Symbols.INFO), + fg=Colors.GREEN) except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg=Colors.RED) raise SystemExit(1) from e @@ -95,7 +102,7 @@ def display_vpn_status(wide: bool = False): ]) if k == 'me': - row = [click.style(i, fg='bright_blue') for i in row] + row = [click.style(i, fg=Colors.BRIGHT_BLUE) for i in row] data.append(row) diff --git a/riocli/vpn/util.py b/riocli/vpn/util.py index 64cdac9d..dc4b3ae8 100644 --- a/riocli/vpn/util.py +++ b/riocli/vpn/util.py @@ -20,6 +20,7 @@ import click +from riocli.constants import Colors, Symbols from riocli.utils import run_bash, run_bash_with_return_code from riocli.v2client import Client as v2Client @@ -73,16 +74,26 @@ def get_tailscale_status() -> dict: def install_vpn_tools() -> None: + if is_tailscale_installed(): + return + + click.confirm( + click.style( + '{} VPN tools are not installed. Do you want ' + 'to install them now?'.format(Symbols.INFO), + fg=Colors.YELLOW), + default=True, abort=True) + if not is_linux(): - click.secho('Only linux is supported', fg='yellow') + click.secho('Only linux is supported', fg=Colors.YELLOW) raise SystemExit(1) if is_tailscale_installed(): - click.secho('VPN tools already installed', fg='green') + click.secho('VPN tools already installed', fg=Colors.GREEN) return if not is_curl_installed(): - click.secho('Please install `curl`', fg='red') + click.secho('Please install `curl`', fg=Colors.RED) raise SystemExit(1) # download the tailscale install script @@ -100,9 +111,10 @@ def install_vpn_tools() -> None: run_bash('sh {}'.format(script_path)) if not is_tailscale_installed(): - raise Exception('Failed to install VPN tools') + raise Exception('{} Failed to install VPN tools'.format(Symbols.ERROR)) - click.secho('VPN tools installed', fg='green') + click.secho('{} VPN tools installed'.format(Symbols.SUCCESS), + fg=Colors.GREEN) def tailscale_ping(tailscale_peer_ip): diff --git a/setup.py b/setup.py index 280ecc14..b248d552 100644 --- a/setup.py +++ b/setup.py @@ -47,7 +47,7 @@ "python-dateutil>=2.8.2", "pytz", "pyyaml>=5.4.1", - "rapyuta-io>=1.10.0", + "rapyuta-io>=1.11.1", "requests>=2.20.0", "setuptools", "six>=1.13.0", @@ -57,7 +57,8 @@ "directory-tree>=0.0.3.1", "yaspin>=2.3.0", "jsonschema>=4.0.0", - "waiting>=1.4.1" + "waiting>=1.4.1", + "semver>=3.0.0", ], setup_requires=["flake8"], )