Repository: codemagic-ci-cd/cli-tools (language: python)

Issue:
google-play CLI client does not detect or allow custom tracks
I've created a custom track, which are now supported in the publishing stage of the pipeline. However, the `google-play` cli tool does not yet support the ability to detect and/or query custom tracks. 

This is problematic as publishing to a track increments the version code, but since the google-play tool doesn't detect the custom track, it cannot increment the build number, and thus the build fails due to it attempting to overwrite an existing version code. 


Discussion / hints:
same issue.

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1295679a..57d366ed 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,56 @@
+Version 0.24.0
+-------------
+
+Changes in this release improve usability of tool `google-play`. Updates are from [PR #215](https://github.com/codemagic-ci-cd/cli-tools/pull/215) and [PR #216](https://github.com/codemagic-ci-cd/cli-tools/pull/216).
+
+**Breaking**
+
+**None of the breaking changes have an effect on command line usage**, only the Python API of is affected.
+
+- Method signature changes:
+  - Signature of `GooglePlay` (tool `google-play`) initialization was changed:
+    - positional argument `package_name` was removed,
+    - keyword argument `log_requests` was removed,
+    - keyword argument `json_output` was removed,
+    - positional argument `credentials` accepts now Google Play service account credentials both as JSON `str` and parsed `dict`.
+  - Signature of `GooglePlayDeveloperAPIClient` initialization was changed and simplified:
+    - positional argument `resource_printer` was removed,
+    - positional argument `package_name` was removed.
+  - `GooglePlay` method `get_latest_build_number` requires `package_name` argument,
+  - `GooglePlayDeveloperAPIClient` methods `create_edit` and `delete_edit` require `package_name` argument,
+  - property `max_version_code` of `Track` was converted into a method `get_max_version_code()`.
+- CLI argument definitions for `google-play` were updated and moved from `codemagic.tools.google_play` to `codemagic.tools.google_play.arguments`.
+- Removed definitions:
+  - enumeration `codemagic.google_play.resources.TrackName` was removed.
+  - class `codemagic.google_play.ResourcePrinter` was removed.
+  - exception `codemagic.google_play.VersionCodeFromTrackError` was removed.
+  - removals in `GooglePlayDeveloperAPIClient`:
+    - method `get_track_information` was removed,
+    - property `service` was removed.
+
+**Features**
+
+- Update tool `google-play`:
+  - Allow using custom release tracks with action `google-play get-latest-build-number`.
+  - Add new action `google-play tracks get` to get information about specific release track for given package name.
+  - Add new action `google-play tracks list` to get information about all available release tracks for given package name.
+
+**Development**
+
+- Module `codemagic.tools.google_play` was refactored by splitting single source file into a subpackage.
+  - Define actions group `TracksActionGroup` for working with tracks.
+  - Move `get_latest_build_number` action / method implementation into separate subclass `GetLatestBuildNumberAction`.
+- Rework the internals of `GooglePlayDeveloperAPIClient`:
+  - add context manager to handle `edit` lifecycle so that callers don't have to take care of deletion afterwards,
+  - add new methods `get_track` and `list_tracks`,
+  - service resource instance was removed from class instance as it wasn't thread safe.
+- Update `mypy` version.
+- Add more tests for `GooglePlay` tool.
+
+**Docs**
+
+- TBD
+
 Version 0.23.1
 -------------
 
diff --git a/Pipfile.lock b/Pipfile.lock
index 8a46b511..4f91eb8e 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -18,11 +18,11 @@
     "default": {
         "cachetools": {
             "hashes": [
-                "sha256:89ea6f1b638d5a73a4f9226be57ac5e4f399d22770b92355f92dcb0f7f001693",
-                "sha256:92971d3cb7d2a97efff7c7bb1657f21a8f5fb309a37530537c71b1774189f2d1"
+                "sha256:486471dfa8799eb7ec503a8059e263db000cdda20075ce5e48903087f79d5fd6",
+                "sha256:8fecd4203a38af17928be7b90689d8083603073622229ca7077b72d8e5a976e4"
             ],
-            "markers": "python_version ~= '3.5'",
-            "version": "==4.2.4"
+            "markers": "python_version ~= '3.7'",
+            "version": "==5.0.0"
         },
         "certifi": {
             "hashes": [
@@ -88,11 +88,11 @@
         },
         "charset-normalizer": {
             "hashes": [
-                "sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd",
-                "sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455"
+                "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597",
+                "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"
             ],
             "markers": "python_version >= '3'",
-            "version": "==2.0.10"
+            "version": "==2.0.12"
         },
         "cryptography": {
             "hashes": [
@@ -129,11 +129,11 @@
         },
         "google-auth": {
             "hashes": [
-                "sha256:a348a50b027679cb7dae98043ac8dbcc1d7951f06d8387496071a1e05a2465c0",
-                "sha256:d83570a664c10b97a1dc6f8df87e5fdfff012f48f62be131e449c20dfc32630e"
+                "sha256:5e079eb4d21df1853d55cf2b6766b77ef36f7f7bdaf7d4a70434aa97c7578d60",
+                "sha256:d65bb0e3701eaaa64fd2aa85e1325580524b0bddc6dc5db3ab89c481b6a20141"
             ],
             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
-            "version": "==2.3.3"
+            "version": "==2.6.3"
         },
         "google-auth-httplib2": {
             "hashes": [
@@ -288,14 +288,6 @@
             "markers": "python_version >= '3.6' and python_version < '4'",
             "version": "==4.8"
         },
-        "setuptools": {
-            "hashes": [
-                "sha256:2404879cda71495fc4d5cbc445ed52fdaddf352b36e40be8dcc63147cb4edabe",
-                "sha256:68eb94073fc486091447fcb0501efd6560a0e5a1839ba249e5ff3c4c93f05f90"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==60.5.0"
-        },
         "six": {
             "hashes": [
                 "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
@@ -314,21 +306,29 @@
         },
         "urllib3": {
             "hashes": [
-                "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed",
-                "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"
+                "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14",
+                "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"
             ],
             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
-            "version": "==1.26.8"
+            "version": "==1.26.9"
         }
     },
     "develop": {
+        "appnope": {
+            "hashes": [
+                "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24",
+                "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"
+            ],
+            "markers": "sys_platform == 'darwin'",
+            "version": "==0.1.3"
+        },
         "astroid": {
             "hashes": [
-                "sha256:1efdf4e867d4d8ba4a9f6cf9ce07cd182c4c41de77f23814feb27ca93ca9d877",
-                "sha256:506daabe5edffb7e696ad82483ad0228245a9742ed7d2d8c9cdb31537decf9f6"
+                "sha256:8d0a30fe6481ce919f56690076eafbb2fb649142a89dc874f1ec0e7a011492d0",
+                "sha256:cc8cc0d2d916c42d0a7c476c57550a4557a083081976bf42a73414322a6411d9"
             ],
             "markers": "python_full_version >= '3.6.2'",
-            "version": "==2.9.3"
+            "version": "==2.11.2"
         },
         "attrs": {
             "hashes": [
@@ -355,11 +355,11 @@
         },
         "bleach": {
             "hashes": [
-                "sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da",
-                "sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994"
+                "sha256:08a1fe86d253b5c88c92cc3d810fd8048a16d15762e1e5b74d502256e5926aa1",
+                "sha256:c6d6cc054bdc9c83b48b8083e236e5f00f238428666d2ce2e083eaa5fd568565"
             ],
-            "markers": "python_version >= '3.6'",
-            "version": "==4.1.0"
+            "markers": "python_version >= '3.7'",
+            "version": "==5.0.0"
         },
         "certifi": {
             "hashes": [
@@ -368,166 +368,87 @@
             ],
             "version": "==2021.10.8"
         },
-        "cffi": {
-            "hashes": [
-                "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3",
-                "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2",
-                "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636",
-                "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20",
-                "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728",
-                "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27",
-                "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66",
-                "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443",
-                "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0",
-                "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7",
-                "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39",
-                "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605",
-                "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a",
-                "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37",
-                "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029",
-                "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139",
-                "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc",
-                "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df",
-                "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14",
-                "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880",
-                "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2",
-                "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a",
-                "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e",
-                "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474",
-                "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024",
-                "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8",
-                "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0",
-                "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e",
-                "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a",
-                "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e",
-                "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032",
-                "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6",
-                "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e",
-                "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b",
-                "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e",
-                "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954",
-                "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962",
-                "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c",
-                "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4",
-                "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55",
-                "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962",
-                "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023",
-                "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c",
-                "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6",
-                "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8",
-                "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382",
-                "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7",
-                "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc",
-                "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997",
-                "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796"
-            ],
-            "version": "==1.15.0"
-        },
         "charset-normalizer": {
             "hashes": [
-                "sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd",
-                "sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455"
+                "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597",
+                "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"
             ],
             "markers": "python_version >= '3'",
-            "version": "==2.0.10"
+            "version": "==2.0.12"
         },
-        "colorama": {
+        "commonmark": {
             "hashes": [
-                "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b",
-                "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"
+                "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60",
+                "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"
             ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
-            "version": "==0.4.4"
+            "version": "==0.9.1"
         },
         "coverage": {
             "extras": [
                 "toml"
             ],
             "hashes": [
-                "sha256:01774a2c2c729619760320270e42cd9e797427ecfddd32c2a7b639cdc481f3c0",
-                "sha256:03b20e52b7d31be571c9c06b74746746d4eb82fc260e594dc662ed48145e9efd",
-                "sha256:0a7726f74ff63f41e95ed3a89fef002916c828bb5fcae83b505b49d81a066884",
-                "sha256:1219d760ccfafc03c0822ae2e06e3b1248a8e6d1a70928966bafc6838d3c9e48",
-                "sha256:13362889b2d46e8d9f97c421539c97c963e34031ab0cb89e8ca83a10cc71ac76",
-                "sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0",
-                "sha256:17e6c11038d4ed6e8af1407d9e89a2904d573be29d51515f14262d7f10ef0a64",
-                "sha256:215f8afcc02a24c2d9a10d3790b21054b58d71f4b3c6f055d4bb1b15cecce685",
-                "sha256:22e60a3ca5acba37d1d4a2ee66e051f5b0e1b9ac950b5b0cf4aa5366eda41d47",
-                "sha256:2641f803ee9f95b1f387f3e8f3bf28d83d9b69a39e9911e5bfee832bea75240d",
-                "sha256:276651978c94a8c5672ea60a2656e95a3cce2a3f31e9fb2d5ebd4c215d095840",
-                "sha256:3f7c17209eef285c86f819ff04a6d4cbee9b33ef05cbcaae4c0b4e8e06b3ec8f",
-                "sha256:3feac4084291642165c3a0d9eaebedf19ffa505016c4d3db15bfe235718d4971",
-                "sha256:49dbff64961bc9bdd2289a2bda6a3a5a331964ba5497f694e2cbd540d656dc1c",
-                "sha256:4e547122ca2d244f7c090fe3f4b5a5861255ff66b7ab6d98f44a0222aaf8671a",
-                "sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de",
-                "sha256:5d6b09c972ce9200264c35a1d53d43ca55ef61836d9ec60f0d44273a31aa9f17",
-                "sha256:600617008aa82032ddeace2535626d1bc212dfff32b43989539deda63b3f36e4",
-                "sha256:619346d57c7126ae49ac95b11b0dc8e36c1dd49d148477461bb66c8cf13bb521",
-                "sha256:63c424e6f5b4ab1cf1e23a43b12f542b0ec2e54f99ec9f11b75382152981df57",
-                "sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b",
-                "sha256:6e1394d24d5938e561fbeaa0cd3d356207579c28bd1792f25a068743f2d5b282",
-                "sha256:86f2e78b1eff847609b1ca8050c9e1fa3bd44ce755b2ec30e70f2d3ba3844644",
-                "sha256:8bdfe9ff3a4ea37d17f172ac0dff1e1c383aec17a636b9b35906babc9f0f5475",
-                "sha256:8e2c35a4c1f269704e90888e56f794e2d9c0262fb0c1b1c8c4ee44d9b9e77b5d",
-                "sha256:92b8c845527eae547a2a6617d336adc56394050c3ed8a6918683646328fbb6da",
-                "sha256:9365ed5cce5d0cf2c10afc6add145c5037d3148585b8ae0e77cc1efdd6aa2953",
-                "sha256:9a29311bd6429be317c1f3fe4bc06c4c5ee45e2fa61b2a19d4d1d6111cb94af2",
-                "sha256:9a2b5b52be0a8626fcbffd7e689781bf8c2ac01613e77feda93d96184949a98e",
-                "sha256:a4bdeb0a52d1d04123b41d90a4390b096f3ef38eee35e11f0b22c2d031222c6c",
-                "sha256:a9c8c4283e17690ff1a7427123ffb428ad6a52ed720d550e299e8291e33184dc",
-                "sha256:b637c57fdb8be84e91fac60d9325a66a5981f8086c954ea2772efe28425eaf64",
-                "sha256:bf154ba7ee2fd613eb541c2bc03d3d9ac667080a737449d1a3fb342740eb1a74",
-                "sha256:c254b03032d5a06de049ce8bca8338a5185f07fb76600afff3c161e053d88617",
-                "sha256:c332d8f8d448ded473b97fefe4a0983265af21917d8b0cdcb8bb06b2afe632c3",
-                "sha256:c7912d1526299cb04c88288e148c6c87c0df600eca76efd99d84396cfe00ef1d",
-                "sha256:cfd9386c1d6f13b37e05a91a8583e802f8059bebfccde61a418c5808dea6bbfa",
-                "sha256:d5d2033d5db1d58ae2d62f095e1aefb6988af65b4b12cb8987af409587cc0739",
-                "sha256:dca38a21e4423f3edb821292e97cec7ad38086f84313462098568baedf4331f8",
-                "sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8",
-                "sha256:e3db840a4dee542e37e09f30859f1612da90e1c5239a6a2498c473183a50e781",
-                "sha256:edcada2e24ed68f019175c2b2af2a8b481d3d084798b8c20d15d34f5c733fa58",
-                "sha256:f467bbb837691ab5a8ca359199d3429a11a01e6dfb3d9dcc676dc035ca93c0a9",
-                "sha256:f506af4f27def639ba45789fa6fde45f9a217da0be05f8910458e4557eed020c",
-                "sha256:f614fc9956d76d8a88a88bb41ddc12709caa755666f580af3a688899721efecd",
-                "sha256:f9afb5b746781fc2abce26193d1c817b7eb0e11459510fba65d2bd77fe161d9e",
-                "sha256:fb8b8ee99b3fffe4fd86f4c81b35a6bf7e4462cba019997af2fe679365db0c49"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==6.2"
-        },
-        "cryptography": {
-            "hashes": [
-                "sha256:0a7dcbcd3f1913f664aca35d47c1331fce738d44ec34b7be8b9d332151b0b01e",
-                "sha256:1eb7bb0df6f6f583dd8e054689def236255161ebbcf62b226454ab9ec663746b",
-                "sha256:21ca464b3a4b8d8e86ba0ee5045e103a1fcfac3b39319727bc0fc58c09c6aff7",
-                "sha256:34dae04a0dce5730d8eb7894eab617d8a70d0c97da76b905de9efb7128ad7085",
-                "sha256:3520667fda779eb788ea00080124875be18f2d8f0848ec00733c0ec3bb8219fc",
-                "sha256:3c4129fc3fdc0fa8e40861b5ac0c673315b3c902bbdc05fc176764815b43dd1d",
-                "sha256:3fa3a7ccf96e826affdf1a0a9432be74dc73423125c8f96a909e3835a5ef194a",
-                "sha256:5b0fbfae7ff7febdb74b574055c7466da334a5371f253732d7e2e7525d570498",
-                "sha256:695104a9223a7239d155d7627ad912953b540929ef97ae0c34c7b8bf30857e89",
-                "sha256:8695456444f277af73a4877db9fc979849cd3ee74c198d04fc0776ebc3db52b9",
-                "sha256:94cc5ed4ceaefcbe5bf38c8fba6a21fc1d365bb8fb826ea1688e3370b2e24a1c",
-                "sha256:94fff993ee9bc1b2440d3b7243d488c6a3d9724cc2b09cdb297f6a886d040ef7",
-                "sha256:9965c46c674ba8cc572bc09a03f4c649292ee73e1b683adb1ce81e82e9a6a0fb",
-                "sha256:a00cf305f07b26c351d8d4e1af84ad7501eca8a342dedf24a7acb0e7b7406e14",
-                "sha256:a305600e7a6b7b855cd798e00278161b681ad6e9b7eca94c721d5f588ab212af",
-                "sha256:cd65b60cfe004790c795cc35f272e41a3df4631e2fb6b35aa7ac6ef2859d554e",
-                "sha256:d2a6e5ef66503da51d2110edf6c403dc6b494cc0082f85db12f54e9c5d4c3ec5",
-                "sha256:d9ec0e67a14f9d1d48dd87a2531009a9b251c02ea42851c060b25c782516ff06",
-                "sha256:f44d141b8c4ea5eb4dbc9b3ad992d45580c1d22bf5e24363f2fbf50c2d7ae8a7"
+                "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9",
+                "sha256:07e6db90cd9686c767dcc593dff16c8c09f9814f5e9c51034066cad3373b914d",
+                "sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf",
+                "sha256:1ebf730d2381158ecf3dfd4453fbca0613e16eaa547b4170e2450c9707665ce7",
+                "sha256:21b7745788866028adeb1e0eca3bf1101109e2dc58456cb49d2d9b99a8c516e6",
+                "sha256:26e2deacd414fc2f97dd9f7676ee3eaecd299ca751412d89f40bc01557a6b1b4",
+                "sha256:2c6dbb42f3ad25760010c45191e9757e7dce981cbfb90e42feef301d71540059",
+                "sha256:2fea046bfb455510e05be95e879f0e768d45c10c11509e20e06d8fcaa31d9e39",
+                "sha256:34626a7eee2a3da12af0507780bb51eb52dca0e1751fd1471d0810539cefb536",
+                "sha256:37d1141ad6b2466a7b53a22e08fe76994c2d35a5b6b469590424a9953155afac",
+                "sha256:46191097ebc381fbf89bdce207a6c107ac4ec0890d8d20f3360345ff5976155c",
+                "sha256:4dd8bafa458b5c7d061540f1ee9f18025a68e2d8471b3e858a9dad47c8d41903",
+                "sha256:4e21876082ed887baed0146fe222f861b5815455ada3b33b890f4105d806128d",
+                "sha256:58303469e9a272b4abdb9e302a780072c0633cdcc0165db7eec0f9e32f901e05",
+                "sha256:5ca5aeb4344b30d0bec47481536b8ba1181d50dbe783b0e4ad03c95dc1296684",
+                "sha256:68353fe7cdf91f109fc7d474461b46e7f1f14e533e911a2a2cbb8b0fc8613cf1",
+                "sha256:6f89d05e028d274ce4fa1a86887b071ae1755082ef94a6740238cd7a8178804f",
+                "sha256:7a15dc0a14008f1da3d1ebd44bdda3e357dbabdf5a0b5034d38fcde0b5c234b7",
+                "sha256:8bdde1177f2311ee552f47ae6e5aa7750c0e3291ca6b75f71f7ffe1f1dab3dca",
+                "sha256:8ce257cac556cb03be4a248d92ed36904a59a4a5ff55a994e92214cde15c5bad",
+                "sha256:8cf5cfcb1521dc3255d845d9dca3ff204b3229401994ef8d1984b32746bb45ca",
+                "sha256:8fbbdc8d55990eac1b0919ca69eb5a988a802b854488c34b8f37f3e2025fa90d",
+                "sha256:9548f10d8be799551eb3a9c74bbf2b4934ddb330e08a73320123c07f95cc2d92",
+                "sha256:96f8a1cb43ca1422f36492bebe63312d396491a9165ed3b9231e778d43a7fca4",
+                "sha256:9b27d894748475fa858f9597c0ee1d4829f44683f3813633aaf94b19cb5453cf",
+                "sha256:9baff2a45ae1f17c8078452e9e5962e518eab705e50a0aa8083733ea7d45f3a6",
+                "sha256:a2a8b8bcc399edb4347a5ca8b9b87e7524c0967b335fbb08a83c8421489ddee1",
+                "sha256:acf53bc2cf7282ab9b8ba346746afe703474004d9e566ad164c91a7a59f188a4",
+                "sha256:b0be84e5a6209858a1d3e8d1806c46214e867ce1b0fd32e4ea03f4bd8b2e3359",
+                "sha256:b31651d018b23ec463e95cf10070d0b2c548aa950a03d0b559eaa11c7e5a6fa3",
+                "sha256:b78e5afb39941572209f71866aa0b206c12f0109835aa0d601e41552f9b3e620",
+                "sha256:c76aeef1b95aff3905fb2ae2d96e319caca5b76fa41d3470b19d4e4a3a313512",
+                "sha256:dd035edafefee4d573140a76fdc785dc38829fe5a455c4bb12bac8c20cfc3d69",
+                "sha256:dd6fe30bd519694b356cbfcaca9bd5c1737cddd20778c6a581ae20dc8c04def2",
+                "sha256:e5f4e1edcf57ce94e5475fe09e5afa3e3145081318e5fd1a43a6b4539a97e518",
+                "sha256:ec6bc7fe73a938933d4178c9b23c4e0568e43e220aef9472c4f6044bfc6dd0f0",
+                "sha256:f1555ea6d6da108e1999b2463ea1003fe03f29213e459145e70edbaf3e004aaa",
+                "sha256:f5fa5803f47e095d7ad8443d28b01d48c0359484fec1b9d8606d0e3282084bc4",
+                "sha256:f7331dbf301b7289013175087636bbaf5b2405e57259dd2c42fdcc9fcc47325e",
+                "sha256:f9987b0354b06d4df0f4d3e0ec1ae76d7ce7cbca9a2f98c25041eb79eec766f1",
+                "sha256:fd9e830e9d8d89b20ab1e5af09b32d33e1a08ef4c4e14411e559556fd788e6b2"
             ],
-            "index": "pypi",
-            "version": "==3.4.8"
+            "markers": "python_version >= '3.7'",
+            "version": "==6.3.2"
         },
         "decorator": {
             "hashes": [
                 "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330",
                 "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"
             ],
-            "markers": "python_version >= '3.5'",
+            "markers": "python_version >= '3.7'",
             "version": "==5.1.1"
         },
+        "dill": {
+            "hashes": [
+                "sha256:7e40e4a70304fd9ceab3535d36e58791d9c4a776b38ec7f7ec9afc8d3dca4d4f",
+                "sha256:9f9734205146b2b353ab3fec9af0070237b6ddae78452af83d2fca84d739e675"
+            ],
+            "markers": "python_version >= '2.7' and python_version != '3.0'",
+            "version": "==0.3.4"
+        },
         "distlib": {
             "hashes": [
                 "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b",
@@ -545,11 +466,11 @@
         },
         "filelock": {
             "hashes": [
-                "sha256:38b4f4c989f9d06d44524df1b24bd19e167d851f19b50bf3e3559952dddc5b80",
-                "sha256:cf0fc6a2f8d26bd900f19bf33915ca70ba4dd8c56903eeb14e1e7a2fd7590146"
+                "sha256:9cd540a9352e432c7246a48fe4e8712b10acb1df2ad1f30e8c070b82ae1fed85",
+                "sha256:f8314284bfffbdcfa0ff3d7992b023d4c628ced6feb957351d4c48d059f56bc0"
             ],
             "markers": "python_version >= '3.7'",
-            "version": "==3.4.2"
+            "version": "==3.6.0"
         },
         "flake8": {
             "hashes": [
@@ -613,11 +534,11 @@
         },
         "ipython": {
             "hashes": [
-                "sha256:55df3e0bd0f94e715abd968bedd89d4e8a7bce4bf498fb123fed4f5398fea874",
-                "sha256:b5548ec5329a4bcf054a5deed5099b0f9622eb9ea51aaa7104d215fece201d8c"
+                "sha256:468abefc45c15419e3c8e8c0a6a5c115b2127bafa34d7c641b1d443658793909",
+                "sha256:86df2cf291c6c70b5be6a7b608650420e89180c8ec74f376a34e2dc15c3400e7"
             ],
-            "index": "pypi",
-            "version": "==7.31.1"
+            "markers": "python_version >= '3.7'",
+            "version": "==7.32.0"
         },
         "isort": {
             "hashes": [
@@ -635,14 +556,6 @@
             "markers": "python_version >= '3.6'",
             "version": "==0.18.1"
         },
-        "jeepney": {
-            "hashes": [
-                "sha256:1b5a0ea5c0e7b166b2f5895b91a08c14de8915afda4407fb5022a195224958ac",
-                "sha256:fa9e232dfa0c498bd0b8a3a73b8d8a31978304dcef0515adc859d4e096f96f4f"
-            ],
-            "markers": "sys_platform == 'linux'",
-            "version": "==0.7.1"
-        },
         "keyring": {
             "hashes": [
                 "sha256:9012508e141a80bd1c0b6778d5c610dd9f8c464d75ac6774248500503f972fb9",
@@ -718,29 +631,32 @@
         },
         "mypy": {
             "hashes": [
-                "sha256:0038b21890867793581e4cb0d810829f5fd4441aa75796b53033af3aa30430ce",
-                "sha256:1171f2e0859cfff2d366da2c7092b06130f232c636a3f7301e3feb8b41f6377d",
-                "sha256:1b06268df7eb53a8feea99cbfff77a6e2b205e70bf31743e786678ef87ee8069",
-                "sha256:1b65714dc296a7991000b6ee59a35b3f550e0073411ac9d3202f6516621ba66c",
-                "sha256:1bf752559797c897cdd2c65f7b60c2b6969ffe458417b8d947b8340cc9cec08d",
-                "sha256:300717a07ad09525401a508ef5d105e6b56646f7942eb92715a1c8d610149714",
-                "sha256:3c5b42d0815e15518b1f0990cff7a705805961613e701db60387e6fb663fe78a",
-                "sha256:4365c60266b95a3f216a3047f1d8e3f895da6c7402e9e1ddfab96393122cc58d",
-                "sha256:50c7346a46dc76a4ed88f3277d4959de8a2bd0a0fa47fa87a4cde36fe247ac05",
-                "sha256:5b56154f8c09427bae082b32275a21f500b24d93c88d69a5e82f3978018a0266",
-                "sha256:74f7eccbfd436abe9c352ad9fb65872cc0f1f0a868e9d9c44db0893440f0c697",
-                "sha256:7b3f6f557ba4afc7f2ce6d3215d5db279bcf120b3cfd0add20a5d4f4abdae5bc",
-                "sha256:8c11003aaeaf7cc2d0f1bc101c1cc9454ec4cc9cb825aef3cafff8a5fdf4c799",
-                "sha256:8ca7f8c4b1584d63c9a0f827c37ba7a47226c19a23a753d52e5b5eddb201afcd",
-                "sha256:c89702cac5b302f0c5d33b172d2b55b5df2bede3344a2fbed99ff96bddb2cf00",
-                "sha256:d8f1ff62f7a879c9fe5917b3f9eb93a79b78aad47b533911b853a757223f72e7",
-                "sha256:d9d2b84b2007cea426e327d2483238f040c49405a6bf4074f605f0156c91a47a",
-                "sha256:e839191b8da5b4e5d805f940537efcaa13ea5dd98418f06dc585d2891d228cf0",
-                "sha256:f9fe20d0872b26c4bba1c1be02c5340de1019530302cf2dcc85c7f9fc3252ae0",
-                "sha256:ff3bf387c14c805ab1388185dd22d6b210824e164d4bb324b195ff34e322d166"
+                "sha256:0e2dd88410937423fba18e57147dd07cd8381291b93d5b1984626f173a26543e",
+                "sha256:10daab80bc40f84e3f087d896cdb53dc811a9f04eae4b3f95779c26edee89d16",
+                "sha256:17e44649fec92e9f82102b48a3bf7b4a5510ad0cd22fa21a104826b5db4903e2",
+                "sha256:1a0459c333f00e6a11cbf6b468b870c2b99a906cb72d6eadf3d1d95d38c9352c",
+                "sha256:246e1aa127d5b78488a4a0594bd95f6d6fb9d63cf08a66dafbff8595d8891f67",
+                "sha256:2b184db8c618c43c3a31b32ff00cd28195d39e9c24e7c3b401f3db7f6e5767f5",
+                "sha256:2bc249409a7168d37c658e062e1ab5173300984a2dada2589638568ddc1db02b",
+                "sha256:3841b5433ff936bff2f4dc8d54cf2cdbfea5d8e88cedfac45c161368e5770ba6",
+                "sha256:4c3e497588afccfa4334a9986b56f703e75793133c4be3a02d06a3df16b67a58",
+                "sha256:5bf44840fb43ac4074636fd47ee476d73f0039f4f54e86d7265077dc199be24d",
+                "sha256:64235137edc16bee6f095aba73be5334677d6f6bdb7fa03cfab90164fa294a17",
+                "sha256:6776e5fa22381cc761df53e7496a805801c1a751b27b99a9ff2f0ca848c7eca0",
+                "sha256:6ce34a118d1a898f47def970a2042b8af6bdcc01546454726c7dd2171aa6dfca",
+                "sha256:6f6ad963172152e112b87cc7ec103ba0f2db2f1cd8997237827c052a3903eaa6",
+                "sha256:6f7106cbf9cc2f403693bf50ed7c9fa5bb3dfa9007b240db3c910929abe2a322",
+                "sha256:7742d2c4e46bb5017b51c810283a6a389296cda03df805a4f7869a6f41246534",
+                "sha256:9521c1265ccaaa1791d2c13582f06facf815f426cd8b07c3a485f486a8ffc1f3",
+                "sha256:a1b383fe99678d7402754fe90448d4037f9512ce70c21f8aee3b8bf48ffc51db",
+                "sha256:b840cfe89c4ab6386c40300689cd8645fc8d2d5f20101c7f8bd23d15fca14904",
+                "sha256:d8d3ba77e56b84cd47a8ee45b62c84b6d80d32383928fe2548c9a124ea0a725c",
+                "sha256:dcd955f36e0180258a96f880348fbca54ce092b40fbb4b37372ae3b25a0b0a46",
+                "sha256:e865fec858d75b78b4d63266c9aff770ecb6a39dfb6d6b56c47f7f8aba6baba8",
+                "sha256:edf7237137a1a9330046dbb14796963d734dd740a98d5e144a3eb1d267f5f9ee"
             ],
             "index": "pypi",
-            "version": "==0.931"
+            "version": "==0.942"
         },
         "mypy-extensions": {
             "hashes": [
@@ -790,19 +706,19 @@
         },
         "pip": {
             "hashes": [
-                "sha256:deaf32dcd9ab821e359cd8330786bcd077604b5c5730c0b096eda46f95c24a2d",
-                "sha256:fd11ba3d0fdb4c07fbc5ecbba0b1b719809420f25038f8ee3cd913d3faa3033a"
+                "sha256:b3a9de2c6ef801e9247d1527a4b16f92f2cc141cd1489f3fffaf6a9e96729764",
+                "sha256:c6aca0f2f081363f689f041d90dab2a07a9a07fb840284db2218117a52da800b"
             ],
-            "markers": "python_version >= '3.6'",
-            "version": "==21.3.1"
+            "markers": "python_version >= '3.7'",
+            "version": "==22.0.4"
         },
         "pipenv": {
             "hashes": [
-                "sha256:3b80b4512934b9d8e8ce12c988394642ff96bb697680e5b092e59af503178327",
-                "sha256:f84d7119239b22ab2ac2b8fbc7d619d83cf41135206d72a17c4f151cda529fd0"
+                "sha256:3ac62c1121477b0758b0eef9e47643d5a2ba1b57e47a0789620ce5ef9bbc007e",
+                "sha256:53562bf69d9e5238f99a1e2101c356746b1c0aefa5dceb9b8a84a5a3e201de0d"
             ],
             "index": "pypi",
-            "version": "==2022.1.8"
+            "version": "==2022.4.8"
         },
         "pkginfo": {
             "hashes": [
@@ -813,11 +729,11 @@
         },
         "platformdirs": {
             "hashes": [
-                "sha256:1d7385c7db91728b83efd0ca99a5afb296cab9d0ed8313a45ed8ba17967ecfca",
-                "sha256:440633ddfebcc36264232365d7840a970e75e1018d15b4327d11f91909045fda"
+                "sha256:7535e70dfa32e84d4b34996ea99c5e432fa29a708d0f4e394bbcb2a8faa4f16d",
+                "sha256:bcae7cab893c2d310a711b70b24efb93334febe65f8de776ee320b517471e227"
             ],
             "markers": "python_version >= '3.7'",
-            "version": "==2.4.1"
+            "version": "==2.5.1"
         },
         "pluggy": {
             "hashes": [
@@ -829,11 +745,11 @@
         },
         "prompt-toolkit": {
             "hashes": [
-                "sha256:1bb05628c7d87b645974a1bad3f17612be0c29fa39af9f7688030163f680bad6",
-                "sha256:e56f2ff799bacecd3e88165b1e2f5ebf9bcd59e80e06d395fa0cc4b8bd7bb506"
+                "sha256:62291dad495e665fca0bda814e342c69952086afb0f4094d0893d357e5c78752",
+                "sha256:bd640f60e8cecd74f0dc249713d433ace2ddc62b65ee07f96d358e0b152b6ea7"
             ],
             "markers": "python_full_version >= '3.6.2'",
-            "version": "==3.0.24"
+            "version": "==3.0.29"
         },
         "ptyprocess": {
             "hashes": [
@@ -858,14 +774,6 @@
             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
             "version": "==2.8.0"
         },
-        "pycparser": {
-            "hashes": [
-                "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9",
-                "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
-            "version": "==2.21"
-        },
         "pyflakes": {
             "hashes": [
                 "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c",
@@ -884,11 +792,11 @@
         },
         "pylint": {
             "hashes": [
-                "sha256:9d945a73640e1fec07ee34b42f5669b770c759acd536ec7b16d7e4b87a9c9ff9",
-                "sha256:daabda3f7ed9d1c60f52d563b1b854632fd90035bcf01443e234d3dc794e3b74"
+                "sha256:c149694cfdeaee1aa2465e6eaab84c87a881a7d55e6e93e09466be7164764d1e",
+                "sha256:dab221658368c7a05242e673c275c488670144123f4bd262b2777249c1c0de9b"
             ],
             "index": "pypi",
-            "version": "==2.12.2"
+            "version": "==2.13.5"
         },
         "pyparsing": {
             "hashes": [
@@ -900,11 +808,11 @@
         },
         "pytest": {
             "hashes": [
-                "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89",
-                "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"
+                "sha256:841132caef6b1ad17a9afde46dc4f6cfa59a05f9555aae5151f73bdf2820ca63",
+                "sha256:92f723789a8fdd7180b6b06483874feca4c48a5c76968e03bb3e7f806a1869ea"
             ],
             "index": "pypi",
-            "version": "==6.2.5"
+            "version": "==7.1.1"
         },
         "pytest-cov": {
             "hashes": [
@@ -916,11 +824,11 @@
         },
         "readme-renderer": {
             "hashes": [
-                "sha256:a50a0f2123a4c1145ac6f420e1a348aafefcc9211c846e3d51df05fe3d865b7d",
-                "sha256:b512beafa6798260c7d5af3e1b1f097e58bfcd9a575da7c4ddd5e037490a5b85"
+                "sha256:262510fe6aae81ed4e94d8b169077f325614c0b1a45916a80442c6576264a9c2",
+                "sha256:dfb4d17f21706d145f7473e0b61ca245ba58e810cf9b2209a48239677f82e5b0"
             ],
             "markers": "python_version >= '3.6'",
-            "version": "==32.0"
+            "version": "==34.0"
         },
         "requests": {
             "hashes": [
@@ -945,21 +853,21 @@
             "markers": "python_version >= '3.7'",
             "version": "==2.0.0"
         },
-        "secretstorage": {
+        "rich": {
             "hashes": [
-                "sha256:422d82c36172d88d6a0ed5afdec956514b189ddbfb72fefab0c8a1cee4eaf71f",
-                "sha256:fd666c51a6bf200643495a04abb261f83229dcb6fd8472ec393df7ffc8b6f195"
+                "sha256:c50f3d253bc6a9bb9c79d61a26d510d74abdf1b16881260fab5edfc3edfb082f",
+                "sha256:ea74bc9dad9589d8eea3e3fd0b136d8bf6e428888955f215824c2894f0da8b47"
             ],
-            "markers": "sys_platform == 'linux'",
-            "version": "==3.3.1"
+            "markers": "python_version < '4' and python_full_version >= '3.6.3'",
+            "version": "==12.2.0"
         },
         "setuptools": {
             "hashes": [
-                "sha256:2404879cda71495fc4d5cbc445ed52fdaddf352b36e40be8dcc63147cb4edabe",
-                "sha256:68eb94073fc486091447fcb0501efd6560a0e5a1839ba249e5ff3c4c93f05f90"
+                "sha256:09980778aa734c3037a47997f28d6db5ab18bdf2af0e49f719bfc53967fd2e82",
+                "sha256:608a7885b664342ae9fafc43840b29d219c5a578876f6f7e00c4e2612160587f"
             ],
-            "markers": "python_version >= '3.7'",
-            "version": "==60.5.0"
+            "index": "pypi",
+            "version": "==59.8.0"
         },
         "six": {
             "hashes": [
@@ -979,19 +887,11 @@
         },
         "tomli": {
             "hashes": [
-                "sha256:b5bde28da1fed24b9bd1d4d2b8cba62300bfb4ec9a6187a957e8ddb9434c5224",
-                "sha256:c292c34f58502a1eb2bbb9f5bbc9a5ebc37bee10ffb8c2d6bbdfa8eb13cc14e1"
+                "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
+                "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
             ],
             "markers": "python_version >= '3.7'",
-            "version": "==2.0.0"
-        },
-        "tqdm": {
-            "hashes": [
-                "sha256:8dd278a422499cd6b727e6ae4061c40b48fce8b76d1ccbf5d34fca9b7f925b0c",
-                "sha256:d359de7217506c9851b7869f3708d8ee53ed70a1b8edbba4dbcb47442592920d"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
-            "version": "==4.62.3"
+            "version": "==2.0.1"
         },
         "traitlets": {
             "hashes": [
@@ -1003,75 +903,80 @@
         },
         "twine": {
             "hashes": [
-                "sha256:28460a3db6b4532bde6a5db6755cf2dce6c5020bada8a641bb2c5c7a9b1f35b8",
-                "sha256:8c120845fc05270f9ee3e9d7ebbed29ea840e41f48cd059e04733f7e1d401345"
+                "sha256:6f7496cf14a3a8903474552d5271c79c71916519edb42554f23f42a8563498a9",
+                "sha256:817aa0c0bdc02a5ebe32051e168e23c71a0608334e624c793011f120dbbc05b7"
             ],
             "index": "pypi",
-            "version": "==3.7.1"
+            "version": "==4.0.0"
         },
         "typed-ast": {
             "hashes": [
-                "sha256:24058827d8f5d633f97223f5148a7d22628099a3d2efe06654ce872f46f07cdb",
-                "sha256:256115a5bc7ea9e665c6314ed6671ee2c08ca380f9d5f130bd4d2c1f5848d695",
-                "sha256:38cf5c642fa808300bae1281460d4f9b7617cf864d4e383054a5ef336e344d32",
-                "sha256:484137cab8ecf47e137260daa20bafbba5f4e3ec7fda1c1e69ab299b75fa81c5",
-                "sha256:4f30a2bcd8e68adbb791ce1567fdb897357506f7ea6716f6bbdd3053ac4d9471",
-                "sha256:591bc04e507595887160ed7aa8d6785867fb86c5793911be79ccede61ae96f4d",
-                "sha256:5b6ab14c56bc9c7e3c30228a0a0b54b915b1579613f6e463ba6f4eb1382e7fd4",
-                "sha256:5d8314c92414ce7481eee7ad42b353943679cf6f30237b5ecbf7d835519e1212",
-                "sha256:71dcda943a471d826ea930dd449ac7e76db7be778fcd722deb63642bab32ea3f",
-                "sha256:7c42707ab981b6cf4b73490c16e9d17fcd5227039720ca14abe415d39a173a30",
-                "sha256:9caaf2b440efb39ecbc45e2fabde809cbe56272719131a6318fd9bf08b58e2cb",
-                "sha256:a2b8d7007f6280e36fa42652df47087ac7b0a7d7f09f9468f07792ba646aac2d",
-                "sha256:a6d495c1ef572519a7bac9534dbf6d94c40e5b6a608ef41136133377bba4aa08",
-                "sha256:a80d84f535642420dd17e16ae25bb46c7f4c16ee231105e7f3eb43976a89670a",
-                "sha256:b53ae5de5500529c76225d18eeb060efbcec90ad5e030713fe8dab0fb4531631",
-                "sha256:b6d17f37f6edd879141e64a5db17b67488cfeffeedad8c5cec0392305e9bc775",
-                "sha256:c9bcad65d66d594bffab8575f39420fe0ee96f66e23c4d927ebb4e24354ec1af",
-                "sha256:ca9e8300d8ba0b66d140820cf463438c8e7b4cdc6fd710c059bfcfb1531d03fb",
-                "sha256:de4ecae89c7d8b56169473e08f6bfd2df7f95015591f43126e4ea7865928677e"
+                "sha256:0eb77764ea470f14fcbb89d51bc6bbf5e7623446ac4ed06cbd9ca9495b62e36e",
+                "sha256:1098df9a0592dd4c8c0ccfc2e98931278a6c6c53cb3a3e2cf7e9ee3b06153344",
+                "sha256:183b183b7771a508395d2cbffd6db67d6ad52958a5fdc99f450d954003900266",
+                "sha256:18fe320f354d6f9ad3147859b6e16649a0781425268c4dde596093177660e71a",
+                "sha256:26a432dc219c6b6f38be20a958cbe1abffcc5492821d7e27f08606ef99e0dffd",
+                "sha256:294a6903a4d087db805a7656989f613371915fc45c8cc0ddc5c5a0a8ad9bea4d",
+                "sha256:31d8c6b2df19a777bc8826770b872a45a1f30cfefcfd729491baa5237faae837",
+                "sha256:33b4a19ddc9fc551ebabca9765d54d04600c4a50eda13893dadf67ed81d9a098",
+                "sha256:42c47c3b43fe3a39ddf8de1d40dbbfca60ac8530a36c9b198ea5b9efac75c09e",
+                "sha256:525a2d4088e70a9f75b08b3f87a51acc9cde640e19cc523c7e41aa355564ae27",
+                "sha256:58ae097a325e9bb7a684572d20eb3e1809802c5c9ec7108e85da1eb6c1a3331b",
+                "sha256:676d051b1da67a852c0447621fdd11c4e104827417bf216092ec3e286f7da596",
+                "sha256:74cac86cc586db8dfda0ce65d8bcd2bf17b58668dfcc3652762f3ef0e6677e76",
+                "sha256:8c08d6625bb258179b6e512f55ad20f9dfef019bbfbe3095247401e053a3ea30",
+                "sha256:90904d889ab8e81a956f2c0935a523cc4e077c7847a836abee832f868d5c26a4",
+                "sha256:963a0ccc9a4188524e6e6d39b12c9ca24cc2d45a71cfdd04a26d883c922b4b78",
+                "sha256:bbebc31bf11762b63bf61aaae232becb41c5bf6b3461b80a4df7e791fabb3aca",
+                "sha256:bc2542e83ac8399752bc16e0b35e038bdb659ba237f4222616b4e83fb9654985",
+                "sha256:c29dd9a3a9d259c9fa19d19738d021632d673f6ed9b35a739f48e5f807f264fb",
+                "sha256:c7407cfcad702f0b6c0e0f3e7ab876cd1d2c13b14ce770e412c0c4b9728a0f88",
+                "sha256:da0a98d458010bf4fe535f2d1e367a2e2060e105978873c04c04212fb20543f7",
+                "sha256:df05aa5b241e2e8045f5f4367a9f6187b09c4cdf8578bb219861c4e27c443db5",
+                "sha256:f290617f74a610849bd8f5514e34ae3d09eafd521dceaa6cf68b3f4414266d4e",
+                "sha256:f30ddd110634c2d7534b2d4e0e22967e88366b0d356b24de87419cc4410c41b7"
             ],
             "markers": "python_version < '3.8'",
-            "version": "==1.5.1"
+            "version": "==1.5.2"
         },
         "types-requests": {
             "hashes": [
-                "sha256:5e0dc681e9bfadb5c1d17f63d29f6d13d29ef2897d30cb9c79285b34a1655fd7",
-                "sha256:8f8a3cf66c525247750b0a0e6ffef4a210d7c8637065a1e7a3cb5769b1eb82a4"
+                "sha256:2437a5f4d16c0c8bd7539a8126d492b7aeb41e6cda670d76b286c7f83a658d42",
+                "sha256:c8010c18b291a7efb60b1452dbe12530bc25693dd657e70c62803fcdc4bffe9b"
             ],
             "index": "pypi",
-            "version": "==2.27.6"
+            "version": "==2.27.16"
         },
         "types-urllib3": {
             "hashes": [
-                "sha256:3adcf2cb5981809091dbff456e6999fe55f201652d8c360f99997de5ac2f556e",
-                "sha256:cfd1fbbe4ba9a605ed148294008aac8a7b8b7472651d1cc357d507ae5962e3d2"
+                "sha256:24d64e441168851eb05f1d022de18ae31558f5649c8f1117e384c2e85e31315b",
+                "sha256:bd0abc01e9fb963e4fddd561a56d21cc371b988d1245662195c90379077139cd"
             ],
-            "version": "==1.26.7"
+            "version": "==1.26.11"
         },
         "typing-extensions": {
             "hashes": [
-                "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e",
-                "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"
+                "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42",
+                "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"
             ],
-            "markers": "python_version < '3.10'",
-            "version": "==4.0.1"
+            "markers": "python_version >= '3.6'",
+            "version": "==4.1.1"
         },
         "urllib3": {
             "hashes": [
-                "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed",
-                "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"
+                "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14",
+                "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"
             ],
             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
-            "version": "==1.26.8"
+            "version": "==1.26.9"
         },
         "virtualenv": {
             "hashes": [
-                "sha256:339f16c4a86b44240ba7223d0f93a7887c3ca04b5f9c8129da7958447d079b09",
-                "sha256:d8458cf8d59d0ea495ad9b34c2599487f8a7772d796f9910858376d1600dd2dd"
+                "sha256:e617f16e25b42eb4f6e74096b9c9e37713cf10bf30168fb4a739f3fa8f898a3a",
+                "sha256:ef589a79795589aada0c1c5b319486797c03b67ac3984c48c669c0e4f50df3a5"
             ],
             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
-            "version": "==20.13.0"
+            "version": "==20.14.1"
         },
         "virtualenv-clone": {
             "hashes": [
@@ -1097,68 +1002,81 @@
         },
         "wrapt": {
             "hashes": [
-                "sha256:086218a72ec7d986a3eddb7707c8c4526d677c7b35e355875a0fe2918b059179",
-                "sha256:0877fe981fd76b183711d767500e6b3111378ed2043c145e21816ee589d91096",
-                "sha256:0a017a667d1f7411816e4bf214646d0ad5b1da2c1ea13dec6c162736ff25a374",
-                "sha256:0cb23d36ed03bf46b894cfec777eec754146d68429c30431c99ef28482b5c1df",
-                "sha256:1fea9cd438686e6682271d36f3481a9f3636195578bab9ca3382e2f5f01fc185",
-                "sha256:220a869982ea9023e163ba915077816ca439489de6d2c09089b219f4e11b6785",
-                "sha256:25b1b1d5df495d82be1c9d2fad408f7ce5ca8a38085e2da41bb63c914baadff7",
-                "sha256:2dded5496e8f1592ec27079b28b6ad2a1ef0b9296d270f77b8e4a3a796cf6909",
-                "sha256:2ebdde19cd3c8cdf8df3fc165bc7827334bc4e353465048b36f7deeae8ee0918",
-                "sha256:43e69ffe47e3609a6aec0fe723001c60c65305784d964f5007d5b4fb1bc6bf33",
-                "sha256:46f7f3af321a573fc0c3586612db4decb7eb37172af1bc6173d81f5b66c2e068",
-                "sha256:47f0a183743e7f71f29e4e21574ad3fa95676136f45b91afcf83f6a050914829",
-                "sha256:498e6217523111d07cd67e87a791f5e9ee769f9241fcf8a379696e25806965af",
-                "sha256:4b9c458732450ec42578b5642ac53e312092acf8c0bfce140ada5ca1ac556f79",
-                "sha256:51799ca950cfee9396a87f4a1240622ac38973b6df5ef7a41e7f0b98797099ce",
-                "sha256:5601f44a0f38fed36cc07db004f0eedeaadbdcec90e4e90509480e7e6060a5bc",
-                "sha256:5f223101f21cfd41deec8ce3889dc59f88a59b409db028c469c9b20cfeefbe36",
-                "sha256:610f5f83dd1e0ad40254c306f4764fcdc846641f120c3cf424ff57a19d5f7ade",
-                "sha256:6a03d9917aee887690aa3f1747ce634e610f6db6f6b332b35c2dd89412912bca",
-                "sha256:705e2af1f7be4707e49ced9153f8d72131090e52be9278b5dbb1498c749a1e32",
-                "sha256:766b32c762e07e26f50d8a3468e3b4228b3736c805018e4b0ec8cc01ecd88125",
-                "sha256:77416e6b17926d953b5c666a3cb718d5945df63ecf922af0ee576206d7033b5e",
-                "sha256:778fd096ee96890c10ce96187c76b3e99b2da44e08c9e24d5652f356873f6709",
-                "sha256:78dea98c81915bbf510eb6a3c9c24915e4660302937b9ae05a0947164248020f",
-                "sha256:7dd215e4e8514004c8d810a73e342c536547038fb130205ec4bba9f5de35d45b",
-                "sha256:7dde79d007cd6dfa65afe404766057c2409316135cb892be4b1c768e3f3a11cb",
-                "sha256:81bd7c90d28a4b2e1df135bfbd7c23aee3050078ca6441bead44c42483f9ebfb",
-                "sha256:85148f4225287b6a0665eef08a178c15097366d46b210574a658c1ff5b377489",
-                "sha256:865c0b50003616f05858b22174c40ffc27a38e67359fa1495605f96125f76640",
-                "sha256:87883690cae293541e08ba2da22cacaae0a092e0ed56bbba8d018cc486fbafbb",
-                "sha256:8aab36778fa9bba1a8f06a4919556f9f8c7b33102bd71b3ab307bb3fecb21851",
-                "sha256:8c73c1a2ec7c98d7eaded149f6d225a692caa1bd7b2401a14125446e9e90410d",
-                "sha256:936503cb0a6ed28dbfa87e8fcd0a56458822144e9d11a49ccee6d9a8adb2ac44",
-                "sha256:944b180f61f5e36c0634d3202ba8509b986b5fbaf57db3e94df11abee244ba13",
-                "sha256:96b81ae75591a795d8c90edc0bfaab44d3d41ffc1aae4d994c5aa21d9b8e19a2",
-                "sha256:981da26722bebb9247a0601e2922cedf8bb7a600e89c852d063313102de6f2cb",
-                "sha256:ae9de71eb60940e58207f8e71fe113c639da42adb02fb2bcbcaccc1ccecd092b",
-                "sha256:b73d4b78807bd299b38e4598b8e7bd34ed55d480160d2e7fdaabd9931afa65f9",
-                "sha256:d4a5f6146cfa5c7ba0134249665acd322a70d1ea61732723c7d3e8cc0fa80755",
-                "sha256:dd91006848eb55af2159375134d724032a2d1d13bcc6f81cd8d3ed9f2b8e846c",
-                "sha256:e05e60ff3b2b0342153be4d1b597bbcfd8330890056b9619f4ad6b8d5c96a81a",
-                "sha256:e6906d6f48437dfd80464f7d7af1740eadc572b9f7a4301e7dd3d65db285cacf",
-                "sha256:e92d0d4fa68ea0c02d39f1e2f9cb5bc4b4a71e8c442207433d8db47ee79d7aa3",
-                "sha256:e94b7d9deaa4cc7bac9198a58a7240aaf87fe56c6277ee25fa5b3aa1edebd229",
-                "sha256:ea3e746e29d4000cd98d572f3ee2a6050a4f784bb536f4ac1f035987fc1ed83e",
-                "sha256:ec7e20258ecc5174029a0f391e1b948bf2906cd64c198a9b8b281b811cbc04de",
-                "sha256:ec9465dd69d5657b5d2fa6133b3e1e989ae27d29471a672416fd729b429eb554",
-                "sha256:f122ccd12fdc69628786d0c947bdd9cb2733be8f800d88b5a37c57f1f1d73c10",
-                "sha256:f99c0489258086308aad4ae57da9e8ecf9e1f3f30fa35d5e170b4d4896554d80",
-                "sha256:f9c51d9af9abb899bd34ace878fbec8bf357b3194a10c4e8e0a25512826ef056",
-                "sha256:fd76c47f20984b43d93de9a82011bb6e5f8325df6c9ed4d8310029a55fa361ea"
+                "sha256:00108411e0f34c52ce16f81f1d308a571df7784932cc7491d1e94be2ee93374b",
+                "sha256:01f799def9b96a8ec1ef6b9c1bbaf2bbc859b87545efbecc4a78faea13d0e3a0",
+                "sha256:09d16ae7a13cff43660155383a2372b4aa09109c7127aa3f24c3cf99b891c330",
+                "sha256:14e7e2c5f5fca67e9a6d5f753d21f138398cad2b1159913ec9e9a67745f09ba3",
+                "sha256:167e4793dc987f77fd476862d32fa404d42b71f6a85d3b38cbce711dba5e6b68",
+                "sha256:1807054aa7b61ad8d8103b3b30c9764de2e9d0c0978e9d3fc337e4e74bf25faa",
+                "sha256:1f83e9c21cd5275991076b2ba1cd35418af3504667affb4745b48937e214bafe",
+                "sha256:21b1106bff6ece8cb203ef45b4f5778d7226c941c83aaaa1e1f0f4f32cc148cd",
+                "sha256:22626dca56fd7f55a0733e604f1027277eb0f4f3d95ff28f15d27ac25a45f71b",
+                "sha256:23f96134a3aa24cc50614920cc087e22f87439053d886e474638c68c8d15dc80",
+                "sha256:2498762814dd7dd2a1d0248eda2afbc3dd9c11537bc8200a4b21789b6df6cd38",
+                "sha256:28c659878f684365d53cf59dc9a1929ea2eecd7ac65da762be8b1ba193f7e84f",
+                "sha256:2eca15d6b947cfff51ed76b2d60fd172c6ecd418ddab1c5126032d27f74bc350",
+                "sha256:354d9fc6b1e44750e2a67b4b108841f5f5ea08853453ecbf44c81fdc2e0d50bd",
+                "sha256:36a76a7527df8583112b24adc01748cd51a2d14e905b337a6fefa8b96fc708fb",
+                "sha256:3a0a4ca02752ced5f37498827e49c414d694ad7cf451ee850e3ff160f2bee9d3",
+                "sha256:3a71dbd792cc7a3d772ef8cd08d3048593f13d6f40a11f3427c000cf0a5b36a0",
+                "sha256:3a88254881e8a8c4784ecc9cb2249ff757fd94b911d5df9a5984961b96113fff",
+                "sha256:47045ed35481e857918ae78b54891fac0c1d197f22c95778e66302668309336c",
+                "sha256:4775a574e9d84e0212f5b18886cace049a42e13e12009bb0491562a48bb2b758",
+                "sha256:493da1f8b1bb8a623c16552fb4a1e164c0200447eb83d3f68b44315ead3f9036",
+                "sha256:4b847029e2d5e11fd536c9ac3136ddc3f54bc9488a75ef7d040a3900406a91eb",
+                "sha256:59d7d92cee84a547d91267f0fea381c363121d70fe90b12cd88241bd9b0e1763",
+                "sha256:5a0898a640559dec00f3614ffb11d97a2666ee9a2a6bad1259c9facd01a1d4d9",
+                "sha256:5a9a1889cc01ed2ed5f34574c90745fab1dd06ec2eee663e8ebeefe363e8efd7",
+                "sha256:5b835b86bd5a1bdbe257d610eecab07bf685b1af2a7563093e0e69180c1d4af1",
+                "sha256:5f24ca7953f2643d59a9c87d6e272d8adddd4a53bb62b9208f36db408d7aafc7",
+                "sha256:61e1a064906ccba038aa3c4a5a82f6199749efbbb3cef0804ae5c37f550eded0",
+                "sha256:65bf3eb34721bf18b5a021a1ad7aa05947a1767d1aa272b725728014475ea7d5",
+                "sha256:6807bcee549a8cb2f38f73f469703a1d8d5d990815c3004f21ddb68a567385ce",
+                "sha256:68aeefac31c1f73949662ba8affaf9950b9938b712fb9d428fa2a07e40ee57f8",
+                "sha256:6915682f9a9bc4cf2908e83caf5895a685da1fbd20b6d485dafb8e218a338279",
+                "sha256:6d9810d4f697d58fd66039ab959e6d37e63ab377008ef1d63904df25956c7db0",
+                "sha256:729d5e96566f44fccac6c4447ec2332636b4fe273f03da128fff8d5559782b06",
+                "sha256:748df39ed634851350efa87690c2237a678ed794fe9ede3f0d79f071ee042561",
+                "sha256:763a73ab377390e2af26042f685a26787c402390f682443727b847e9496e4a2a",
+                "sha256:8323a43bd9c91f62bb7d4be74cc9ff10090e7ef820e27bfe8815c57e68261311",
+                "sha256:8529b07b49b2d89d6917cfa157d3ea1dfb4d319d51e23030664a827fe5fd2131",
+                "sha256:87fa943e8bbe40c8c1ba4086971a6fefbf75e9991217c55ed1bcb2f1985bd3d4",
+                "sha256:88236b90dda77f0394f878324cfbae05ae6fde8a84d548cfe73a75278d760291",
+                "sha256:891c353e95bb11abb548ca95c8b98050f3620a7378332eb90d6acdef35b401d4",
+                "sha256:89ba3d548ee1e6291a20f3c7380c92f71e358ce8b9e48161401e087e0bc740f8",
+                "sha256:8c6be72eac3c14baa473620e04f74186c5d8f45d80f8f2b4eda6e1d18af808e8",
+                "sha256:9a242871b3d8eecc56d350e5e03ea1854de47b17f040446da0e47dc3e0b9ad4d",
+                "sha256:9a3ff5fb015f6feb78340143584d9f8a0b91b6293d6b5cf4295b3e95d179b88c",
+                "sha256:9a5a544861b21e0e7575b6023adebe7a8c6321127bb1d238eb40d99803a0e8bd",
+                "sha256:9d57677238a0c5411c76097b8b93bdebb02eb845814c90f0b01727527a179e4d",
+                "sha256:9d8c68c4145041b4eeae96239802cfdfd9ef927754a5be3f50505f09f309d8c6",
+                "sha256:9d9fcd06c952efa4b6b95f3d788a819b7f33d11bea377be6b8980c95e7d10775",
+                "sha256:a0057b5435a65b933cbf5d859cd4956624df37b8bf0917c71756e4b3d9958b9e",
+                "sha256:a65bffd24409454b889af33b6c49d0d9bcd1a219b972fba975ac935f17bdf627",
+                "sha256:b0ed6ad6c9640671689c2dbe6244680fe8b897c08fd1fab2228429b66c518e5e",
+                "sha256:b21650fa6907e523869e0396c5bd591cc326e5c1dd594dcdccac089561cacfb8",
+                "sha256:b3f7e671fb19734c872566e57ce7fc235fa953d7c181bb4ef138e17d607dc8a1",
+                "sha256:b77159d9862374da213f741af0c361720200ab7ad21b9f12556e0eb95912cd48",
+                "sha256:bb36fbb48b22985d13a6b496ea5fb9bb2a076fea943831643836c9f6febbcfdc",
+                "sha256:d066ffc5ed0be00cd0352c95800a519cf9e4b5dd34a028d301bdc7177c72daf3",
+                "sha256:d332eecf307fca852d02b63f35a7872de32d5ba8b4ec32da82f45df986b39ff6",
+                "sha256:d808a5a5411982a09fef6b49aac62986274ab050e9d3e9817ad65b2791ed1425",
+                "sha256:d9bdfa74d369256e4218000a629978590fd7cb6cf6893251dad13d051090436d",
+                "sha256:db6a0ddc1282ceb9032e41853e659c9b638789be38e5b8ad7498caac00231c23",
+                "sha256:debaf04f813ada978d7d16c7dfa16f3c9c2ec9adf4656efdc4defdf841fc2f0c",
+                "sha256:f0408e2dbad9e82b4c960274214af533f856a199c9274bd4aff55d4634dedc33",
+                "sha256:f2f3bc7cd9c9fcd39143f11342eb5963317bd54ecc98e3650ca22704b69d9653"
             ],
             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
-            "version": "==1.13.3"
+            "version": "==1.14.0"
         },
         "zipp": {
             "hashes": [
-                "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d",
-                "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"
+                "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad",
+                "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"
             ],
             "markers": "python_version >= '3.7'",
-            "version": "==3.7.0"
+            "version": "==3.8.0"
         }
     }
 }
diff --git a/src/codemagic/__version__.py b/src/codemagic/__version__.py
index de703209..0a517d15 100644
--- a/src/codemagic/__version__.py
+++ b/src/codemagic/__version__.py
@@ -1,5 +1,5 @@
 __title__ = 'codemagic-cli-tools'
 __description__ = 'CLI tools used in Codemagic builds'
-__version__ = '0.23.1'
+__version__ = '0.24.0'
 __url__ = 'https://github.com/codemagic-ci-cd/cli-tools'
 __licence__ = 'GNU General Public License v3.0'
diff --git a/src/codemagic/cli/cli_process_stream.py b/src/codemagic/cli/cli_process_stream.py
index fb2ac088..977d6e23 100644
--- a/src/codemagic/cli/cli_process_stream.py
+++ b/src/codemagic/cli/cli_process_stream.py
@@ -91,7 +91,7 @@ def unblock(self):
     def read(self, buffer_size=1024) -> bytes:
         # https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-readfile
         try:
-            chunk, _result = winapi.ReadFile(self._pipe_handle, buffer_size, 0)
+            chunk, _result = winapi.ReadFile(self._pipe_handle, buffer_size, 0)  # type: ignore
         except BrokenPipeError:
             chunk = b''
         return self._bytes(chunk or b'')
@@ -100,7 +100,7 @@ def read_all(self) -> bytes:
         # https://docs.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-peeknamedpipe
         # Check how much is there still remaining in the pipe to be read and use that as the final buffer size
         try:
-            _data, buffer_size, *_rest = winapi.PeekNamedPipe(self._pipe_handle, 1)
+            _data, buffer_size, *_rest = winapi.PeekNamedPipe(self._pipe_handle, 1)  # type: ignore
         except BrokenPipeError:
             # The pipe has already been ended
             return b''
diff --git a/src/codemagic/google_play/__init__.py b/src/codemagic/google_play/__init__.py
index b21e2b4d..4861204d 100644
--- a/src/codemagic/google_play/__init__.py
+++ b/src/codemagic/google_play/__init__.py
@@ -1,4 +1,1 @@
 from .api_error import GooglePlayDeveloperAPIClientError
-from .api_error import VersionCodeFromTrackError
-from .resource_printer import ResourcePrinter
-from .resources import TrackName
diff --git a/src/codemagic/google_play/api_client.py b/src/codemagic/google_play/api_client.py
index 473cff53..80a72ece 100644
--- a/src/codemagic/google_play/api_client.py
+++ b/src/codemagic/google_play/api_client.py
@@ -1,19 +1,27 @@
+import contextlib
 import json
+from functools import lru_cache
+from typing import AnyStr
+from typing import List
 from typing import Optional
+from typing import Union
 
 import httplib2
 from googleapiclient import discovery
 from googleapiclient import errors
 from oauth2client.service_account import ServiceAccountCredentials
 
+from codemagic.utilities import log
+
 from .api_error import AuthorizationError
-from .api_error import CredentialsError
 from .api_error import EditError
-from .api_error import VersionCodeFromTrackError
-from .resource_printer import ResourcePrinter
+from .api_error import GetResourceError
+from .api_error import GooglePlayDeveloperAPIClientError
+from .api_error import ListResourcesError
 from .resources import Edit
 from .resources import Track
-from .resources import TrackName
+
+CredentialsJson = Union[AnyStr, dict]
 
 
 class GooglePlayDeveloperAPIClient:
@@ -21,78 +29,135 @@ class GooglePlayDeveloperAPIClient:
     SCOPE_URI = f'https://www.googleapis.com/auth/{SCOPE}'
     API_VERSION = 'v3'
 
-    _service_instance: Optional[discovery.Resource] = None
-
-    def __init__(self,
-                 credentials: str,
-                 package_name: str,
-                 resource_printer: ResourcePrinter):
+    def __init__(self, service_account_json_keyfile: CredentialsJson):
         """
-        :param credentials: Your Gloud Service account credentials with JSON key type
-        :param package_name: package name of the app in Google Play Console (Ex: com.google.example)
-        :param resource_printer: printer initialized in google-play tool
+        :param service_account_json_keyfile: Your Gcloud Service account credentials with JSON key type
         """
-        self._credentials = credentials
-        self.package_name = package_name
-        self.resource_printer = resource_printer
+        self._service_account_json_keyfile = service_account_json_keyfile
+        self._logger = log.get_logger(self.__class__)
 
-    def get_edit_resource(self):
-        return self.service.edits()  # type: ignore
+    @classmethod
+    def _get_edit_id(cls, edit: Union[str, Edit]) -> str:
+        if isinstance(edit, Edit):
+            return edit.id
+        return edit
+
+    def _get_json_keyfile_dict(self) -> dict:
+        if isinstance(self._service_account_json_keyfile, dict):
+            return self._service_account_json_keyfile
+        try:
+            return json.loads(self._service_account_json_keyfile)
+        except ValueError as ve:
+            message = 'Unable to parse service account credentials, must be a valid json'
+            raise GooglePlayDeveloperAPIClientError(message) from ve
+
+    @lru_cache(1)
+    def _get_android_publisher_service(self):
+        json_keyfile = self._get_json_keyfile_dict()
+        http = httplib2.Http()
+        try:
+            service_account_credentials = ServiceAccountCredentials.from_json_keyfile_dict(
+                json_keyfile,
+                scopes=self.SCOPE_URI,
+            )
+            http = service_account_credentials.authorize(http)
+            return discovery.build(
+                self.SCOPE,
+                self.API_VERSION,
+                http=http,
+                cache_discovery=False,
+            )
+        except (errors.Error, errors.HttpError) as e:
+            raise AuthorizationError(e) from e
+
+    @property
+    def android_publishing_service(self):
+        return self._get_android_publisher_service()
 
     @property
-    def service(self) -> discovery.Resource:
-        if self._service_instance is None:
-            try:
-                json_credentials = json.loads(str(self._credentials))
-            except json.decoder.JSONDecodeError:
-                raise CredentialsError()
-
-            http = httplib2.Http()
-            try:
-                service_account_credentials = ServiceAccountCredentials.from_json_keyfile_dict(
-                    json_credentials, scopes=self.SCOPE_URI)
-                http = service_account_credentials.authorize(http)
-                self._service_instance = discovery.build(self.SCOPE, self.API_VERSION, http=http, cache_discovery=False)
-            except errors.HttpError as e:
-                raise AuthorizationError(e._get_reason() or 'Http Error')
-            except errors.Error as e:
-                raise AuthorizationError(str(e))
-        return self._service_instance
-
-    def create_edit(self) -> Edit:
-        self.resource_printer.log_request(f'Create an edit for the package "{self.package_name}"')
+    def edits_service(self):
+        return self.android_publishing_service.edits()
+
+    def create_edit(self, package_name: str) -> Edit:
+        self._logger.debug(f'Create an edit for the package {package_name!r}')
         try:
-            edit_response = self.get_edit_resource().insert(body={}, packageName=self.package_name).execute()
-            resource = Edit(**edit_response)
-            self.resource_printer.print_resource(resource)
-            return resource
-        except errors.HttpError as e:
-            raise EditError('create', self.package_name, e._get_reason() or 'Http Error')
-        except errors.Error as e:
-            raise EditError('create', self.package_name, str(e))
-
-    def delete_edit(self, edit_id: str) -> None:
-        self.resource_printer.log_request(f'Delete the edit "{edit_id}" for the package "{self.package_name}"')
+            edit_request = self.edits_service.insert(body={}, packageName=package_name)
+            edit_response = edit_request.execute()
+            self._logger.debug(f'Created edit {edit_response} for package {package_name!r}')
+        except (errors.Error, errors.HttpError) as e:
+            raise EditError('create', package_name, e) from e
+        else:
+            return Edit(**edit_response)
+
+    def delete_edit(self, edit: Union[str, Edit], package_name: str) -> None:
+        edit_id = self._get_edit_id(edit)
+        self._logger.debug(f'Delete the edit {edit_id!r} for the package {package_name!r}')
         try:
-            self.get_edit_resource().delete(packageName=self.package_name, editId=edit_id).execute()
-            self._service_instance = None
-        except errors.HttpError as e:
-            raise EditError('delete', self.package_name, e._get_reason() or 'Http Error')
-        except errors.Error as e:
-            raise EditError('delete', self.package_name, str(e))
-
-    def get_track_information(self, edit_id: str, track_name: TrackName) -> Track:
-        self.resource_printer.log_request(
-            f'Get information about the track "{track_name.value}" '
-            f'for the package "{self.package_name}"',
-        )
+            delete_request = self.edits_service.delete(
+                packageName=package_name,
+                editId=edit_id,
+            )
+            delete_request.execute()
+            self._logger.debug(f'Deleted edit {edit_id} for package {package_name!r}')
+        except (errors.Error, errors.HttpError) as e:
+            raise EditError('delete', package_name, e) from e
+
+    @contextlib.contextmanager
+    def use_app_edit(self, package_name: str):
+        edit = self.create_edit(package_name)
+        try:
+            yield edit
+        finally:
+            self.delete_edit(edit, package_name)
+
+    def get_track(
+        self,
+        package_name: str,
+        track_name: str,
+        edit: Optional[Union[str, Edit]] = None,
+    ) -> Track:
+        if edit is not None:
+            edit_id = self._get_edit_id(edit)
+            return self._get_track(package_name, track_name, edit_id)
+        with self.use_app_edit(package_name) as _edit:
+            return self._get_track(package_name, track_name, _edit.id)
+
+    def _get_track(self, package_name: str, track_name: str, edit_id: str) -> Track:
+        self._logger.debug(f'Get track {track_name!r} for package {package_name!r} using edit {edit_id}')
+        try:
+            track_request = self.edits_service.tracks().get(
+                packageName=package_name,
+                editId=edit_id,
+                track=track_name,
+            )
+            track_response = track_request.execute()
+            self._logger.debug(f'Got track {track_name!r} for package {package_name!r}: {track_response}')
+        except (errors.Error, errors.HttpError) as e:
+            raise GetResourceError('track', package_name, e) from e
+        else:
+            return Track(**track_response)
+
+    def list_tracks(
+        self,
+        package_name: str,
+        edit: Optional[Union[str, Edit]] = None,
+    ) -> List[Track]:
+        if edit is not None:
+            edit_id = self._get_edit_id(edit)
+            return self._list_tracks(package_name, edit_id)
+        with self.use_app_edit(package_name) as _edit:
+            return self._list_tracks(package_name, _edit.id)
+
+    def _list_tracks(self, package_name: str, edit_id: str) -> List[Track]:
+        self._logger.debug(f'List tracks for package {package_name!r} using edit {edit_id}')
         try:
-            track_response = self.get_edit_resource().tracks().get(
-                packageName=self.package_name, editId=edit_id, track=track_name.value).execute()
-            resource = Track(**track_response)
-            self.resource_printer.print_resource(resource)
-            return resource
-        except errors.HttpError as e:
-            raise VersionCodeFromTrackError(track_name.value, e._get_reason() or 'Http Error')
-        except errors.Error as e:
-            raise VersionCodeFromTrackError(track_name.value, str(e))
+            tracks_request = self.edits_service.tracks().list(
+                packageName=package_name,
+                editId=edit_id,
+            )
+            tracks_response = tracks_request.execute()
+            self._logger.debug(f'Got tracks for package {package_name!r}: {tracks_response}')
+        except (errors.Error, errors.HttpError) as e:
+            raise ListResourcesError('tracks', package_name, e) from e
+        else:
+            return [Track(**track) for track in tracks_response['tracks']]
diff --git a/src/codemagic/google_play/api_error.py b/src/codemagic/google_play/api_error.py
index 844d6679..630a0fb1 100644
--- a/src/codemagic/google_play/api_error.py
+++ b/src/codemagic/google_play/api_error.py
@@ -1,23 +1,62 @@
-class GooglePlayDeveloperAPIClientError(Exception):
-    pass
+from abc import ABC
+from abc import abstractmethod
+from typing import Union
+
+from googleapiclient import errors
 
 
-class CredentialsError(GooglePlayDeveloperAPIClientError):
-    def __init__(self):
-        super().__init__('Unable to parse service account credentials, must be a valid json')
+class GooglePlayDeveloperAPIClientError(Exception):
+    @classmethod
+    def _get_error_reason(cls, error: Union[errors.Error, errors.HttpError]) -> str:
+        if isinstance(error, errors.Error):
+            return str(error)
+        else:
+            reason = error._get_reason()
+            return reason or 'Http Error'
 
 
 class AuthorizationError(GooglePlayDeveloperAPIClientError):
-    def __init__(self, reason: str):
+    def __init__(self, error: Union[errors.Error, errors.HttpError]):
+        reason = self._get_error_reason(error)
         super().__init__(f'Unable to authorize with provided credentials. {reason}')
 
 
 class EditError(GooglePlayDeveloperAPIClientError):
-    def __init__(self, action: str, package_name: str, reason: str):
+    def __init__(self, action: str, package_name: str, error: Union[errors.Error, errors.HttpError]):
+        reason = self._get_error_reason(error)
         super().__init__(f'Unable to {action} an edit for package "{package_name}". {reason}')
 
 
-class VersionCodeFromTrackError(GooglePlayDeveloperAPIClientError):
-    def __init__(self, track: str, reason: str):
-        super().__init__(
-            f'Failed to get version code from Google Play from {track} track. {reason}')
+class _RequestError(GooglePlayDeveloperAPIClientError, ABC):
+    def __init__(
+            self,
+            resource_description: str,
+            package_name: str,
+            request_error: Union[errors.Error, errors.HttpError],
+    ):
+        self.package_name = package_name
+        self.request_error = request_error
+        super().__init__(self._get_message(resource_description))
+
+    def _get_reason(self) -> str:
+        return self._get_error_reason(self.request_error)
+
+    @abstractmethod
+    def _get_message(self, resource_description: str) -> str:
+        raise NotImplementedError()
+
+
+class GetResourceError(_RequestError):
+    def _get_message(self, resource_description: str) -> str:
+        return (
+            f'Failed to get {resource_description} from Google Play for package "{self.package_name}". '
+            f'{self._get_reason()}'
+        )
+
+
+class ListResourcesError(_RequestError):
+    def _get_message(self, resource_description: str) -> str:
+        return (
+            f'Failed to list {resource_description} from Google Play for package "{self.package_name}". '
+            f'{self._get_reason()}'
+        )
diff --git a/src/codemagic/google_play/resource_printer.py b/src/codemagic/google_play/resource_printer.py
deleted file mode 100644
index b077aa31..00000000
--- a/src/codemagic/google_play/resource_printer.py
+++ /dev/null
@@ -1,33 +0,0 @@
-from typing import Callable
-from typing import TypeVar
-
-from codemagic.cli import Colors
-
-from .resources.resource import Resource
-
-ResourceToPrint = TypeVar('ResourceToPrint', bound=Resource)
-
-
-class ResourcePrinter:
-
-    def __init__(self,
-                 should_print: bool = False,
-                 print_json: bool = False,
-                 print_function: Callable[[str], None] = None):
-        self.print = print_function
-        self.should_print = should_print
-        self.print_json = print_json
-
-    def log_request(self, header: str) -> None:
-        if not self.print:
-            return
-        self.print(Colors.BLUE(header))
-
-    def print_resource(self, resource: ResourceToPrint) -> None:
-        if not self.print:
-            return
-        if self.should_print:
-            if self.print_json:
-                self.print(resource.json())
-            else:
-                self.print(str(resource))
diff --git a/src/codemagic/google_play/resources/__init__.py b/src/codemagic/google_play/resources/__init__.py
index 32b10132..b1c85f51 100644
--- a/src/codemagic/google_play/resources/__init__.py
+++ b/src/codemagic/google_play/resources/__init__.py
@@ -1,3 +1,3 @@
 from .edit import Edit
-from .enums import TrackName
+from .resource import Resource
 from .track import Track
diff --git a/src/codemagic/google_play/resources/enums.py b/src/codemagic/google_play/resources/enums.py
index c3961863..3f954bc4 100644
--- a/src/codemagic/google_play/resources/enums.py
+++ b/src/codemagic/google_play/resources/enums.py
@@ -6,13 +6,6 @@ def __str__(self):
         return str(self.value)
 
 
-class TrackName(ResourceEnum):
-    INTERNAL = 'internal'
-    ALPHA = 'alpha'
-    BETA = 'beta'
-    PRODUCTION = 'production'
-
-
 class ReleaseStatus(ResourceEnum):
     STATUS_UNSPECIFIED = 'statusUnspecified'
     DRAFT = 'draft'
diff --git a/src/codemagic/google_play/resources/track.py b/src/codemagic/google_play/resources/track.py
index 11359331..da91334b 100644
--- a/src/codemagic/google_play/resources/track.py
+++ b/src/codemagic/google_play/resources/track.py
@@ -5,8 +5,6 @@
 from typing import List
 from typing import Optional
 
-from codemagic.google_play import VersionCodeFromTrackError
-
 from .enums import ReleaseStatus
 from .resource import Resource
 
@@ -77,11 +75,11 @@ def __post_init__(self):
         if isinstance(self.releases, list):
             self.releases = [Release(**release) for release in self.releases]
 
-    @property
-    def max_version_code(self) -> int:
+    def get_max_version_code(self) -> int:
+        error_prefix = f'Failed to get version code from "{self.track}" track'
         if not self.releases:
-            raise VersionCodeFromTrackError(self.track, 'No release information')
+            raise ValueError(f'{error_prefix}: track has no releases')
         version_codes = [release.versionCodes for release in self.releases if release.versionCodes]
         if not version_codes:
-            raise VersionCodeFromTrackError(self.track, 'No releases with uploaded App bundles or APKs')
+            raise ValueError(f'{error_prefix}: releases with version code do not exist')
         return max(map(int, chain(*version_codes)))
diff --git a/src/codemagic/tools/google_play.py b/src/codemagic/tools/google_play.py
deleted file mode 100644
index 61ebc394..00000000
--- a/src/codemagic/tools/google_play.py
+++ /dev/null
@@ -1,164 +0,0 @@
-#!/usr/bin/env python3
-
-from __future__ import annotations
-
-import argparse
-import json
-from typing import List
-from typing import Optional
-from typing import Sequence
-
-from codemagic import cli
-from codemagic.google_play import GooglePlayDeveloperAPIClientError
-from codemagic.google_play import ResourcePrinter
-from codemagic.google_play.api_client import GooglePlayDeveloperAPIClient
-from codemagic.google_play.resources import TrackName
-
-
-class Types:
-    class PackageName(str):
-        pass
-
-    class CredentialsArgument(cli.EnvironmentArgumentValue[str]):
-        environment_variable_key = 'GCLOUD_SERVICE_ACCOUNT_CREDENTIALS'
-
-        @classmethod
-        def _is_valid(cls, value: str) -> bool:
-            try:
-                json_content = json.loads(value)
-            except json.decoder.JSONDecodeError:
-                return False
-            else:
-                return json_content.get('type') == 'service_account'
-
-
-class GooglePlayArgument(cli.Argument):
-    GCLOUD_SERVICE_ACCOUNT_CREDENTIALS = cli.ArgumentProperties(
-        key='credentials',
-        flags=('--credentials',),
-        type=Types.CredentialsArgument,
-        description=(
-            'Gcloud service account credentials with `JSON` key type '
-            'to access Google Play Developer API'
-        ),
-        argparse_kwargs={'required': False},
-    )
-    PACKAGE_NAME = cli.ArgumentProperties(
-        key='package_name',
-        flags=('--package-name',),
-        type=Types.PackageName,
-        description='Package name of the app in Google Play Console (Ex: com.google.example)',
-        argparse_kwargs={'required': True},
-    )
-    LOG_REQUESTS = cli.ArgumentProperties(
-        key='log_requests',
-        flags=('--log-api-calls',),
-        type=bool,
-        description='Turn on logging for Google Play Developer API requests',
-        argparse_kwargs={'required': False, 'action': 'store_true'},
-    )
-    JSON_OUTPUT = cli.ArgumentProperties(
-        key='json_output',
-        flags=('--json',),
-        type=bool,
-        description='Whether to show the request response in JSON format',
-        argparse_kwargs={'required': False, 'action': 'store_true'},
-    )
-
-
-class BuildNumberArgument(cli.Argument):
-    TRACKS = cli.ArgumentProperties(
-        key='tracks',
-        flags=('--tracks',),
-        type=TrackName,
-        description=(
-            'Get the build number from the specified track(s). '
-            'If not specified, the highest build number across all tracks '
-            f'({", ".join(list(map(str, TrackName)))}) is returned'
-        ),
-        argparse_kwargs={
-            'required': False,
-            'nargs': '+',
-            'choices': list(TrackName),
-        },
-    )
-
-
-class GooglePlayError(cli.CliAppException):
-    pass
-
-
-@cli.common_arguments(*GooglePlayArgument)
-class GooglePlay(cli.CliApp):
-    """
-    Utility to get the latest build numbers from Google Play using Google Play Developer API
-    """
-
-    def __init__(self,
-                 credentials: str,
-                 package_name: Types.PackageName,
-                 log_requests: bool = False,
-                 json_output: bool = False,
-                 **kwargs):
-        super().__init__(**kwargs)
-        self.credentials = credentials
-        self.package_name = package_name
-        print_command = self.logger.info if kwargs.get('enable_logging') else None
-        printer = ResourcePrinter(bool(log_requests), bool(json_output), print_command)
-        self.api_client = GooglePlayDeveloperAPIClient(credentials, package_name, printer)
-
-    @classmethod
-    def from_cli_args(cls, cli_args: argparse.Namespace) -> GooglePlay:
-        credentials_argument = GooglePlayArgument.GCLOUD_SERVICE_ACCOUNT_CREDENTIALS.from_args(cli_args)
-        package_name_argument = GooglePlayArgument.PACKAGE_NAME.from_args(cli_args)
-
-        if credentials_argument is None:
-            raise GooglePlayArgument.GCLOUD_SERVICE_ACCOUNT_CREDENTIALS.raise_argument_error()
-        if package_name_argument is None:
-            raise GooglePlayArgument.PACKAGE_NAME.raise_argument_error()
-
-        return GooglePlay(
-            credentials=credentials_argument.value,
-            package_name=package_name_argument,
-            log_requests=cli_args.log_requests,
-            json_output=cli_args.json_output,
-            **cls._parent_class_kwargs(cli_args),
-        )
-
-    @cli.action('get-latest-build-number', BuildNumberArgument.TRACKS)
-    def get_latest_build_number(self, tracks: Sequence[TrackName] = None) -> Optional[int]:
-        """
-        Get latest build number from Google Play Developer API matching given constraints
-        """
-
-        try:
-            edit = self.api_client.create_edit()
-        except GooglePlayDeveloperAPIClientError as api_error:
-            raise GooglePlayError(str(api_error))
-
-        track_version_codes: List[int] = []
-        track_names: Sequence[TrackName] = tracks or list(TrackName)
-        for track_name in track_names:
-            try:
-                track = self.api_client.get_track_information(edit.id, track_name)
-                version_code: int = track.max_version_code
-            except GooglePlayDeveloperAPIClientError as api_error:
-                self.logger.warning(api_error)
-            else:
-                self.logger.info(f'Latest version code for {track_name.value} track: {str(version_code)}')
-                track_version_codes.append(version_code)
-        try:
-            self.api_client.delete_edit(edit.id)
-        except GooglePlayDeveloperAPIClientError as api_error:
-            self.logger.warning(api_error)
-
-        try:
-            latest_build_number = max(track_version_codes)
-        except ValueError:
-            raise GooglePlayError('Version code info is missing from all tracks')
-        self.echo(str(latest_build_number))
-        return latest_build_number
-
-
-if __name__ == '__main__':
-    GooglePlay.invoke_cli()
diff --git a/src/codemagic/tools/google_play/__init__.py b/src/codemagic/tools/google_play/__init__.py
new file mode 100644
index 00000000..5f507dfb
--- /dev/null
+++ b/src/codemagic/tools/google_play/__init__.py
@@ -0,0 +1,1 @@
+from .google_play import GooglePlay
diff --git a/src/codemagic/tools/google_play/__main__.py b/src/codemagic/tools/google_play/__main__.py
new file mode 100644
index 00000000..d8cee147
--- /dev/null
+++ b/src/codemagic/tools/google_play/__main__.py
@@ -0,0 +1,3 @@
+if __name__ == '__main__':
+    from .google_play import GooglePlay
+    GooglePlay.invoke_cli()
diff --git a/src/codemagic/tools/google_play/action_groups/__init__.py b/src/codemagic/tools/google_play/action_groups/__init__.py
new file mode 100644
index 00000000..1d283ddd
--- /dev/null
+++ b/src/codemagic/tools/google_play/action_groups/__init__.py
@@ -0,0 +1,1 @@
+from .tracks_action_group import TracksActionGroup
diff --git a/src/codemagic/tools/google_play/action_groups/google_play_action_groups.py b/src/codemagic/tools/google_play/action_groups/google_play_action_groups.py
new file mode 100644
index 00000000..18dc048a
--- /dev/null
+++ b/src/codemagic/tools/google_play/action_groups/google_play_action_groups.py
@@ -0,0 +1,8 @@
+from codemagic import cli
+
+
+class GooglePlayActionGroups(cli.ActionGroup):
+    TRACKS = cli.ActionGroupProperties(
+        name='tracks',
+        description='Manage your Google Play release tracks',
+    )
diff --git a/src/codemagic/tools/google_play/action_groups/tracks_action_group.py b/src/codemagic/tools/google_play/action_groups/tracks_action_group.py
new file mode 100644
index 00000000..72a24b77
--- /dev/null
+++ b/src/codemagic/tools/google_play/action_groups/tracks_action_group.py
@@ -0,0 +1,73 @@
+import json
+from abc import ABCMeta
+from typing import List
+
+from codemagic import cli
+from codemagic.google_play import GooglePlayDeveloperAPIClientError
+from codemagic.google_play.resources import Track
+
+from ..arguments import GooglePlayArgument
+from ..arguments import TracksArgument
+from ..errors import GooglePlayError
+from ..google_play_base_action import GooglePlayBaseAction
+from .google_play_action_groups import GooglePlayActionGroups
+
+
+class TracksActionGroup(GooglePlayBaseAction, metaclass=ABCMeta):
+    @cli.action(
+        'get',
+        TracksArgument.PACKAGE_NAME,
+        TracksArgument.TRACK_NAME,
+        GooglePlayArgument.JSON_OUTPUT,
+        action_group=GooglePlayActionGroups.TRACKS,
+    )
+    def get_track(
+        self,
+        package_name: str,
+        track_name: str,
+        json_output: bool = False,
+        should_print: bool = True,
+    ) -> Track:
+        """
+        Get information about specified track from Google Play Developer API
+        """
+
+        try:
+            track = self.api_client.get_track(package_name, track_name)
+        except GooglePlayDeveloperAPIClientError as api_error:
+            raise GooglePlayError(str(api_error))
+
+        if should_print:
+            self.logger.info(track.json() if json_output else str(track))
+
+        return track
+
+    @cli.action(
+        'list',
+        TracksArgument.PACKAGE_NAME,
+        GooglePlayArgument.JSON_OUTPUT,
+        action_group=GooglePlayActionGroups.TRACKS,
+    )
+    def list_tracks(
+        self,
+        package_name: str,
+        json_output: bool = False,
+        should_print: bool = True,
+    ) -> List[Track]:
+        """
+        Get information about specified track from Google Play Developer API
+        """
+
+        try:
+            tracks = self.api_client.list_tracks(package_name)
+        except GooglePlayDeveloperAPIClientError as api_error:
+            raise GooglePlayError(str(api_error))
+
+        if should_print:
+            if json_output:
+                self.echo(json.dumps([t.dict() for t in tracks], indent=4))
+            else:
+                for track in tracks:
+                    self.echo(str(track))
+
+        return tracks
diff --git a/src/codemagic/tools/google_play/actions/__init__.py b/src/codemagic/tools/google_play/actions/__init__.py
new file mode 100644
index 00000000..9597525d
--- /dev/null
+++ b/src/codemagic/tools/google_play/actions/__init__.py
@@ -0,0 +1,1 @@
+from .get_latest_build_number_action import GetLatestBuildNumberAction
diff --git a/src/codemagic/tools/google_play/argument_types.py b/src/codemagic/tools/google_play/argument_types.py
new file mode 100644
index 00000000..515c4a40
--- /dev/null
+++ b/src/codemagic/tools/google_play/argument_types.py
@@ -0,0 +1,20 @@
+import json
+
+from codemagic import cli
+
+
+class PackageName(str):
+    pass
+
+
+class CredentialsArgument(cli.EnvironmentArgumentValue[str]):
+    environment_variable_key = 'GCLOUD_SERVICE_ACCOUNT_CREDENTIALS'
+
+    @classmethod
+    def _is_valid(cls, value: str) -> bool:
+        try:
+            json_content = json.loads(value)
+        except json.decoder.JSONDecodeError:
+            return False
+        else:
+            return json_content.get('type') == 'service_account'
diff --git a/src/codemagic/tools/google_play/arguments.py b/src/codemagic/tools/google_play/arguments.py
new file mode 100644
index 00000000..efbeb910
--- /dev/null
+++ b/src/codemagic/tools/google_play/arguments.py
@@ -0,0 +1,55 @@
+from codemagic import cli
+
+from .argument_types import CredentialsArgument
+from .argument_types import PackageName
+
+
+class GooglePlayArgument(cli.Argument):
+    GCLOUD_SERVICE_ACCOUNT_CREDENTIALS = cli.ArgumentProperties(
+        key='credentials',
+        flags=('--credentials',),
+        type=CredentialsArgument,
+        description=(
+            'Gcloud service account credentials with `JSON` key type '
+            'to access Google Play Developer API'
+        ),
+        argparse_kwargs={'required': False},
+    )
+    JSON_OUTPUT = cli.ArgumentProperties(
+        key='json_output',
+        flags=('--json', '-j'),
+        type=bool,
+        description='Whether to show the request response in JSON format',
+        argparse_kwargs={'required': False, 'action': 'store_true'},
+    )
+
+
+class TracksArgument(cli.Argument):
+    PACKAGE_NAME = cli.ArgumentProperties(
+        key='package_name',
+        flags=('--package-name', '-p'),
+        type=PackageName,
+        description='Package name of the app in Google Play Console. For example `com.example.app`',
+        argparse_kwargs={'required': True},
+    )
+    TRACK_NAME = cli.ArgumentProperties(
+        key='track_name',
+        flags=('--track', '-t'),
+        description='Release track name. For example `alpha` or `production`',
+        argparse_kwargs={'required': True},
+    )
+
+
+class LatestBuildNumberArgument(cli.Argument):
+    TRACKS = cli.ArgumentProperties(
+        key='tracks',
+        flags=('--tracks', '-t'),
+        description=(
+            'Get the build number from the specified track(s). '
+            'If not specified, the highest build number across all tracks is returned'
+        ),
+        argparse_kwargs={
+            'required': False,
+            'nargs': '+',
+        },
+    )
diff --git a/src/codemagic/tools/google_play/errors.py b/src/codemagic/tools/google_play/errors.py
new file mode 100644
index 00000000..c2df5d4d
--- /dev/null
+++ b/src/codemagic/tools/google_play/errors.py
@@ -0,0 +1,5 @@
+from codemagic import cli
+
+
+class GooglePlayError(cli.CliAppException):
+    pass
diff --git a/src/codemagic/tools/google_play/google_play.py b/src/codemagic/tools/google_play/google_play.py
new file mode 100644
index 00000000..bcba8dfb
--- /dev/null
+++ b/src/codemagic/tools/google_play/google_play.py
@@ -0,0 +1,43 @@
+#!/usr/bin/env python3
+
+from __future__ import annotations
+
+import argparse
+from typing import Union
+
+from codemagic import cli
+from codemagic.google_play.api_client import GooglePlayDeveloperAPIClient
+
+from .action_groups import TracksActionGroup
+from .actions import GetLatestBuildNumberAction
+from .arguments import GooglePlayArgument
+
+
+@cli.common_arguments(GooglePlayArgument.GCLOUD_SERVICE_ACCOUNT_CREDENTIALS)
+class GooglePlay(
+    cli.CliApp,
+    GetLatestBuildNumberAction,
+    TracksActionGroup,
+):
+    """
+    Utility to get the latest build numbers from Google Play using Google Play Developer API
+    """
+
+    def __init__(self, credentials: Union[str, dict], **kwargs):
+        super().__init__(**kwargs)
+        self.api_client = GooglePlayDeveloperAPIClient(credentials)
+
+    @classmethod
+    def from_cli_args(cls, cli_args: argparse.Namespace) -> GooglePlay:
+        credentials_argument = GooglePlayArgument.GCLOUD_SERVICE_ACCOUNT_CREDENTIALS.from_args(cli_args)
+        if credentials_argument is None:
+            raise GooglePlayArgument.GCLOUD_SERVICE_ACCOUNT_CREDENTIALS.raise_argument_error()
+
+        return GooglePlay(
+            credentials=credentials_argument.value,
+            **cls._parent_class_kwargs(cli_args),
+        )
+
+
+if __name__ == '__main__':
+    GooglePlay.invoke_cli()
diff --git a/src/codemagic/tools/google_play/google_play_base_action.py b/src/codemagic/tools/google_play/google_play_base_action.py
new file mode 100644
index 00000000..87713ad7
--- /dev/null
+++ b/src/codemagic/tools/google_play/google_play_base_action.py
@@ -0,0 +1,55 @@
+import logging
+from abc import ABCMeta
+from abc import abstractmethod
+from typing import List
+from typing import Optional
+from typing import Sequence
+
+from codemagic.google_play.api_client import GooglePlayDeveloperAPIClient
+from codemagic.google_play.resources import Track
+
+
+class GooglePlayBaseAction(metaclass=ABCMeta):
+    api_client: GooglePlayDeveloperAPIClient
+    logger: logging.Logger
+
+    # Define signatures for self-reference to other action groups
+
+    @classmethod
+    def echo(cls, message: str, *args, **kwargs) -> None:
+        ...
+
+    # Action signatures in alphabetical order
+
+    @abstractmethod
+    def get_latest_build_number(
+        self,
+        package_name: str,
+        tracks: Optional[Sequence[str]] = None,
+    ) -> Optional[int]:
+        from .actions import GetLatestBuildNumberAction
+        _ = GetLatestBuildNumberAction.get_latest_build_number  # Implementation
+        raise NotImplementedError()
+
+    @abstractmethod
+    def get_track(
+        self,
+        package_name: str,
+        track_name: str,
+        json_output: bool = False,
+        should_print: bool = True,
+    ) -> Track:
+        from .action_groups import TracksActionGroup
+        _ = TracksActionGroup.get_track  # Implementation
+        raise NotImplementedError
+
+    @abstractmethod
+    def list_tracks(
+        self,
+        package_name: str,
+        json_output: bool = False,
+        should_print: bool = True,
+    ) -> List[Track]:
+        from .action_groups import TracksActionGroup
+        _ = TracksActionGroup.list_tracks  # Implementation
+        raise NotImplementedError()


Test patch:
diff --git a/src/codemagic/tools/google_play/actions/get_latest_build_number_action.py b/src/codemagic/tools/google_play/actions/get_latest_build_number_action.py
new file mode 100644
index 00000000..f74e00dc
--- /dev/null
+++ b/src/codemagic/tools/google_play/actions/get_latest_build_number_action.py
@@ -0,0 +1,101 @@
+from abc import ABCMeta
+from typing import Dict
+from typing import Optional
+from typing import Sequence
+
+from codemagic import cli
+from codemagic.cli import Colors
+from codemagic.google_play.resources import Track
+
+from ..arguments import LatestBuildNumberArgument
+from ..arguments import TracksArgument
+from ..errors import GooglePlayError
+from ..google_play_base_action import GooglePlayBaseAction
+
+
+class GetLatestBuildNumberAction(GooglePlayBaseAction, metaclass=ABCMeta):
+    @cli.action(
+        'get-latest-build-number',
+        TracksArgument.PACKAGE_NAME,
+        LatestBuildNumberArgument.TRACKS,
+    )
+    def get_latest_build_number(
+        self,
+        package_name: str,
+        tracks: Optional[Sequence[str]] = None,
+    ) -> Optional[int]:
+        """
+        Get latest build number from Google Play Developer API matching given constraints
+        """
+
+        requested_track_names = tuple(tracks or [])
+        self._log_get_package_action_started(package_name, requested_track_names)
+        package_tracks = self.list_tracks(package_name, should_print=False)
+        track_version_codes = self._get_track_version_codes(requested_track_names, package_tracks)
+        self._show_missing_tracks_warnings(requested_track_names, tuple(track_version_codes.keys()))
+        latest_build_number = self._get_latest_build_number(package_name, requested_track_names, track_version_codes)
+
+        self.echo(str(latest_build_number))
+        return latest_build_number
+
+    def _log_get_package_action_started(self, package_name: str, requested_tracks: Sequence[str]):
+        if not requested_tracks:
+            message = f'Get package "{package_name}" latest build number across all tracks'
+        elif len(requested_tracks) == 1:
+            message = f'Get package "{package_name}" latest build number from track "{requested_tracks[0]}"'
+        else:
+            formatted_specified_tracks = ', '.join(f'"{track}"' for track in requested_tracks)
+            message = f'Get package "{package_name}" latest build number from tracks {formatted_specified_tracks}'
+        self.logger.info(Colors.BLUE(message))
+
+    def _get_track_version_codes(
+        self,
+        requested_tracks: Sequence[str],
+        package_tracks: Sequence[Track],
+    ) -> Dict[str, Optional[int]]:
+        track_version_codes: Dict[str, Optional[int]] = {}
+
+        for track in package_tracks:
+            track_name = track.track
+            if requested_tracks and track_name not in requested_tracks:
+                continue
+
+            try:
+                version_code = track.get_max_version_code()
+            except ValueError as ve:
+                self.logger.warning(ve)
+                track_version_codes[track_name] = None
+            else:
+                self.logger.info(f'Found latest version code from "{track_name}" track: {version_code}')
+                track_version_codes[track_name] = version_code
+
+        return track_version_codes
+
+    def _show_missing_tracks_warnings(self, requested_tracks: Sequence[str], found_tracks: Sequence[str]):
+        missing_track_names = set(requested_tracks).difference(found_tracks)
+        for missing_track_name in missing_track_names:
+            message = f'Failed to get version code from track "{missing_track_name}": track was not found'
+            self.logger.warning(Colors.YELLOW(message))
+
+    def _get_latest_build_number(
+        self,
+        package_name: str,
+        requested_tracks: Sequence[str],
+        track_version_codes: Dict[str, Optional[int]],
+    ) -> int:
+        version_codes = [version_code for version_code in track_version_codes.values() if version_code is not None]
+        if not version_codes:
+            error = self._get_missing_version_error(package_name, requested_tracks)
+            raise GooglePlayError(error)
+        return max(version_codes)
+
+    @classmethod
+    def _get_missing_version_error(cls, package_name: str, requested_tracks: Sequence[str]) -> str:
+        if not requested_tracks:
+            return f'Version code info is missing from all tracks for package "{package_name}'
+        elif len(requested_tracks) == 1:
+            _track = requested_tracks[0]
+            return f'Version code info is missing from track "{_track}" for package "{package_name}'
+        else:
+            _tracks = ', '.join(f'"{track}"' for track in requested_tracks)
+            return f'Version code info is missing from tracks {_tracks} for package "{package_name}'
diff --git a/tests/conftest.py b/tests/conftest.py
index 5feb507a..7f0be914 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 import argparse
+import json
 import logging
 import os
 import pathlib
@@ -18,7 +19,6 @@
 from codemagic.apple.app_store_connect import IssuerId  # noqa: E402
 from codemagic.apple.app_store_connect import KeyIdentifier  # noqa: E402
 from codemagic.google_play.api_client import GooglePlayDeveloperAPIClient  # noqa: E402
-from codemagic.google_play.api_client import ResourcePrinter  # noqa: E402
 from codemagic.utilities import log  # noqa: E402
 
 log.initialize_logging(
@@ -71,8 +71,8 @@ def _appstore_api_client() -> AppStoreConnectApiClient:
     )
 
 
-@lru_cache()
-def _google_play_api_client() -> GooglePlayDeveloperAPIClient:
+@lru_cache(1)
+def _google_play_api_credentials() -> dict:
     if 'TEST_GCLOUD_SERVICE_ACCOUNT_CREDENTIALS_PATH' in os.environ:
         credentials_path = pathlib.Path(os.environ['TEST_GCLOUD_SERVICE_ACCOUNT_CREDENTIALS_PATH'])
         credentials = credentials_path.expanduser().read_text()
@@ -83,11 +83,7 @@ def _google_play_api_client() -> GooglePlayDeveloperAPIClient:
             'TEST_GCLOUD_SERVICE_ACCOUNT_CREDENTIALS_PATH',
             'TEST_GCLOUD_SERVICE_ACCOUNT_CREDENTIALS_CONTENT',
         )
-    return GooglePlayDeveloperAPIClient(
-        credentials,
-        os.environ['TEST_GCLOUD_PACKAGE_NAME'],
-        ResourcePrinter(False, False),
-    )
+    return json.loads(credentials)
 
 
 def _logger():
@@ -113,7 +109,8 @@ def app_store_api_client() -> AppStoreConnectApiClient:
 
 @pytest.fixture
 def google_play_api_client() -> GooglePlayDeveloperAPIClient:
-    return _google_play_api_client()
+    credentials = _google_play_api_credentials()
+    return GooglePlayDeveloperAPIClient(credentials)
 
 
 @pytest.fixture()
@@ -126,11 +123,6 @@ def class_appstore_api_client(request):
     request.cls.api_client = _appstore_api_client()
 
 
-@pytest.fixture(scope='class')
-def class_google_play_api_client(request):
-    request.cls.api_client = _google_play_api_client()
-
-
 @pytest.fixture
 def logger() -> logging.Logger:
     return _logger()
diff --git a/tests/google_play/resources/test_track_resource.py b/tests/google_play/resources/test_track_resource.py
index 1e26c6fe..106dcf59 100644
--- a/tests/google_play/resources/test_track_resource.py
+++ b/tests/google_play/resources/test_track_resource.py
@@ -2,7 +2,6 @@
 
 import pytest
 
-from codemagic.google_play import VersionCodeFromTrackError
 from codemagic.google_play.resources import Track
 
 
@@ -13,21 +12,21 @@ def test_track_initialization(api_track):
 
 def test_max_version_code(api_track):
     track = Track(**api_track)
-    assert track.max_version_code == 29
+    assert track.get_max_version_code() == 29
 
 
 def test_max_version_code_error_no_releases(api_track):
     api_track.pop('releases')
     track = Track(**api_track)
-    with pytest.raises(VersionCodeFromTrackError) as e:
-        track.max_version_code
-    assert 'No release information' in str(e.value)
+    with pytest.raises(ValueError) as e:
+        track.get_max_version_code()
+    assert str(e.value) == 'Failed to get version code from "internal" track: track has no releases'
 
 
 def test_max_version_code_error_no_version_codes(api_track):
     for release in api_track['releases']:
         release.pop('versionCodes')
     track = Track(**api_track)
-    with pytest.raises(VersionCodeFromTrackError) as e:
-        track.max_version_code
-    assert 'No releases with uploaded App bundles or APKs' in str(e.value)
+    with pytest.raises(ValueError) as e:
+        track.get_max_version_code()
+    assert str(e.value) == 'Failed to get version code from "internal" track: releases with version code do not exist'
diff --git a/tests/google_play/test_api_client.py b/tests/google_play/test_api_client.py
index 7d706015..a1f74a8b 100644
--- a/tests/google_play/test_api_client.py
+++ b/tests/google_play/test_api_client.py
@@ -1,31 +1,30 @@
 import os
-import unittest
 
 import pytest
 
 from codemagic.google_play.api_client import GooglePlayDeveloperAPIClient
-from codemagic.google_play.resources import TrackName
 
 
 @pytest.mark.skipif(not os.environ.get('RUN_LIVE_API_TESTS'), reason='Live Google Play Developer API access')
-@pytest.mark.usefixtures('class_google_play_api_client')
-class ApiTests(unittest.TestCase):
-    api_client: GooglePlayDeveloperAPIClient
+def test_list_tracks(google_play_api_client: GooglePlayDeveloperAPIClient):
+    tracks = google_play_api_client.list_tracks(package_name='io.codemagic.artemii.capybara')
+    found_track_names = {t.track for t in tracks}
+    assert found_track_names == {'production', 'beta', 'alpha', 'internal'}
 
-    def _service_exists(self):
-        return self.api_client._service_instance is not None
 
-    def test_get_edit_and_track_information(self):
-        assert not self._service_exists()
-        edit = self.api_client.create_edit()
-        assert self._service_exists()
-        assert edit.id is not None
-        assert edit.expiryTimeSeconds is not None
-
-        track = self.api_client.get_track_information(edit.id, TrackName.INTERNAL)
-        assert track.releases
-        for release in track.releases:
-            assert release.versionCodes
-
-        self.api_client.delete_edit(edit.id)
-        assert not self._service_exists()
+@pytest.mark.skipif(not os.environ.get('RUN_LIVE_API_TESTS'), reason='Live Google Play Developer API access')
+def test_get_track(google_play_api_client: GooglePlayDeveloperAPIClient):
+    track = google_play_api_client.get_track(
+        package_name='io.codemagic.artemii.capybara',
+        track_name='alpha',
+    )
+    assert track.dict() == {
+        'releases': [
+            {
+                'name': '65 (0.0.42)',
+                'status': 'completed',
+                'versionCodes': ['65'],
+            },
+        ],
+        'track': 'alpha',
+    }
diff --git a/tests/tools/mocks/google_play_tracks.json b/tests/tools/mocks/google_play_tracks.json
new file mode 100644
index 00000000..21e8541b
--- /dev/null
+++ b/tests/tools/mocks/google_play_tracks.json
@@ -0,0 +1,46 @@
+[
+    {
+        "track": "production",
+        "releases": [
+            {
+                "status": "draft"
+            }
+        ]
+    },
+    {
+        "track": "beta",
+        "releases": [
+            {
+                "status": "completed",
+                "name": "66 (0.0.42)",
+                "versionCodes": [
+                    "66"
+                ]
+            }
+        ]
+    },
+    {
+        "track": "alpha",
+        "releases": [
+            {
+                "status": "completed",
+                "name": "65 (0.0.42)",
+                "versionCodes": [
+                    "65"
+                ]
+            }
+        ]
+    },
+    {
+        "track": "internal",
+        "releases": [
+            {
+                "status": "completed",
+                "name": "67 (0.0.42)",
+                "versionCodes": [
+                    "67"
+                ]
+            }
+        ]
+    }
+]
diff --git a/tests/tools/test_google_play.py b/tests/tools/test_google_play.py
index 62ae85c9..d384b3ab 100644
--- a/tests/tools/test_google_play.py
+++ b/tests/tools/test_google_play.py
@@ -1,19 +1,31 @@
 from __future__ import annotations
 
 import argparse
+import json
 import os
+import pathlib
 from tempfile import NamedTemporaryFile
+from typing import List
 from unittest import mock
 
 import pytest
 
+from codemagic.google_play.resources import Track
 from codemagic.tools.google_play import GooglePlay
-from codemagic.tools.google_play import GooglePlayArgument
-from codemagic.tools.google_play import Types
+from codemagic.tools.google_play.argument_types import CredentialsArgument
+from codemagic.tools.google_play.arguments import GooglePlayArgument
+from codemagic.tools.google_play.errors import GooglePlayError
 
 credentials_argument = GooglePlayArgument.GCLOUD_SERVICE_ACCOUNT_CREDENTIALS
 
 
+@pytest.fixture
+def mock_tracks() -> List[Track]:
+    mock_response_path = pathlib.Path(__file__).parent / 'mocks' / 'google_play_tracks.json'
+    tracks_response = json.loads(mock_response_path.read_text())
+    return [Track(**track) for track in tracks_response]
+
+
 @pytest.fixture(autouse=True)
 def register_args(cli_argument_group):
     for arg in GooglePlay.CLASS_ARGUMENTS:
@@ -23,10 +35,7 @@ def register_args(cli_argument_group):
 @pytest.fixture()
 def namespace_kwargs():
     ns_kwargs = {
-        credentials_argument.key: Types.CredentialsArgument('{"type":"service_account"}'),
-        GooglePlayArgument.PACKAGE_NAME.key: Types.PackageName('package.name'),
-        GooglePlayArgument.LOG_REQUESTS.key: False,
-        GooglePlayArgument.JSON_OUTPUT.key: False,
+        credentials_argument.key: CredentialsArgument('{"type":"service_account"}'),
     }
     for arg in GooglePlay.CLASS_ARGUMENTS:
         if not hasattr(arg.type, 'environment_variable_key'):
@@ -35,33 +44,22 @@ def namespace_kwargs():
     return ns_kwargs
 
 
-def _test_missing_argument(argument, _namespace_kwargs):
-    cli_args = argparse.Namespace(**dict(_namespace_kwargs.items()))
+def test_missing_credentials_arg(namespace_kwargs):
+    namespace_kwargs[credentials_argument.key] = None
+    cli_args = argparse.Namespace(**dict(namespace_kwargs.items()))
+
     with pytest.raises(argparse.ArgumentError) as exception_info:
         GooglePlay.from_cli_args(cli_args)
-    message = str(exception_info.value)
-    assert argument.key.upper() in message
-    if hasattr(argument.type, 'environment_variable_key'):
-        assert argument.type.environment_variable_key in message
-    assert ','.join(argument.flags) in message
 
-
-@pytest.mark.parametrize('argument', [
-    credentials_argument,
-    GooglePlayArgument.PACKAGE_NAME,
-])
-def test_missing_arg(cli_argument_group, namespace_kwargs, argument):
-    namespace_kwargs[argument.key] = None
-    _test_missing_argument(argument, namespace_kwargs)
-
-
-def test_missing_creedentials_arg(namespace_kwargs):
-    namespace_kwargs[credentials_argument.key] = None
-    _test_missing_argument(credentials_argument, namespace_kwargs)
+    message = str(exception_info.value)
+    assert credentials_argument.key.upper() in message
+    if hasattr(credentials_argument.type, 'environment_variable_key'):
+        assert credentials_argument.type.environment_variable_key in message
+    assert ','.join(credentials_argument.flags) in message
 
 
 def test_invalid_credentials_from_env(namespace_kwargs):
-    os.environ[Types.CredentialsArgument.environment_variable_key] = 'invalid credentials'
+    os.environ[CredentialsArgument.environment_variable_key] = 'invalid credentials'
     namespace_kwargs[credentials_argument.key] = None
     cli_args = argparse.Namespace(**dict(namespace_kwargs.items()))
     with pytest.raises(argparse.ArgumentError) as exception_info:
@@ -70,7 +68,7 @@ def test_invalid_credentials_from_env(namespace_kwargs):
 
 
 def test_credentials_invalid_path(namespace_kwargs):
-    os.environ[Types.CredentialsArgument.environment_variable_key] = '@file:this-is-not-a-file'
+    os.environ[CredentialsArgument.environment_variable_key] = '@file:this-is-not-a-file'
     namespace_kwargs[credentials_argument.key] = None
     cli_args = argparse.Namespace(**dict(namespace_kwargs.items()))
     with pytest.raises(argparse.ArgumentError) as exception_info:
@@ -78,49 +76,113 @@ def test_credentials_invalid_path(namespace_kwargs):
     assert str(exception_info.value) == 'argument --credentials: File "this-is-not-a-file" does not exist'
 
 
-@mock.patch('codemagic.tools.google_play.GooglePlayDeveloperAPIClient')
+@mock.patch('codemagic.tools.google_play.google_play.GooglePlayDeveloperAPIClient')
 def test_read_private_key(mock_google_play_api_client, namespace_kwargs):
-    credentials = '{"type":"service_account"}'
-    namespace_kwargs[credentials_argument.key] = Types.CredentialsArgument(credentials)
-    _do_credentials_assertions(credentials, mock_google_play_api_client, namespace_kwargs)
+    namespace_kwargs[credentials_argument.key] = CredentialsArgument('{"type":"service_account"}')
+    _ = GooglePlay.from_cli_args(argparse.Namespace(**namespace_kwargs))
+    mock_google_play_api_client.assert_called_once_with('{"type":"service_account"}')
 
 
 @pytest.mark.parametrize('configure_variable', [
     lambda filename, ns_kwargs: os.environ.update(
-        {credentials_argument.type.environment_variable_key: f'@file:{filename}'}),
+        {credentials_argument.type.environment_variable_key: f'@file:{filename}'},
+    ),
     lambda filename, ns_kwargs: ns_kwargs.update(
-        {credentials_argument.key: Types.CredentialsArgument(f'@file:{filename}')}),
+        {credentials_argument.key: CredentialsArgument(f'@file:{filename}')},
+    ),
 ])
-@mock.patch('codemagic.tools.google_play.GooglePlayDeveloperAPIClient')
+@mock.patch('codemagic.tools.google_play.google_play.GooglePlayDeveloperAPIClient')
 def test_private_key_path_arg(mock_google_play_api_client, configure_variable, namespace_kwargs):
-    credentials = '{"type":"service_account"}'
     with NamedTemporaryFile(mode='w') as tf:
-        tf.write(credentials)
+        tf.write('{"type":"service_account"}')
         tf.flush()
         namespace_kwargs[credentials_argument.key] = None
         configure_variable(tf.name, namespace_kwargs)
-        _do_credentials_assertions(credentials, mock_google_play_api_client, namespace_kwargs)
+
+        _ = GooglePlay.from_cli_args(argparse.Namespace(**namespace_kwargs))
+        mock_google_play_api_client.assert_called_once_with('{"type":"service_account"}')
 
 
 @pytest.mark.parametrize('configure_variable', [
     lambda ns_kwargs: os.environ.update(
-        {credentials_argument.type.environment_variable_key: '@env:CREDENTIALS'}),
+        {credentials_argument.type.environment_variable_key: '@env:CREDENTIALS'},
+    ),
     lambda ns_kwargs: ns_kwargs.update(
-        {credentials_argument.key: Types.CredentialsArgument('@env:CREDENTIALS')}),
+        {credentials_argument.key: CredentialsArgument('@env:CREDENTIALS')},
+    ),
 ])
-@mock.patch('codemagic.tools.google_play.GooglePlayDeveloperAPIClient')
+@mock.patch('codemagic.tools.google_play.google_play.GooglePlayDeveloperAPIClient')
 def test_private_key_env_arg(mock_google_play_api_client, configure_variable, namespace_kwargs):
-    credentials = '{"type":"service_account"}'
-    os.environ['CREDENTIALS'] = credentials
+    os.environ['CREDENTIALS'] = '{"type":"service_account"}'
     namespace_kwargs[credentials_argument.key] = None
     configure_variable(namespace_kwargs)
-    _do_credentials_assertions(credentials, mock_google_play_api_client, namespace_kwargs)
-
 
-def _do_credentials_assertions(credentials_value, moc_google_play_api_client, cli_namespace):
-    cli_args = argparse.Namespace(**dict(cli_namespace.items()))
-    _ = GooglePlay.from_cli_args(cli_args)
-    credentials_arg, _, _ = moc_google_play_api_client.call_args[0]
-    print(type(credentials_arg))
-    assert isinstance(credentials_arg, str)
-    assert credentials_arg == credentials_value
+    _ = GooglePlay.from_cli_args(argparse.Namespace(**namespace_kwargs))
+    mock_google_play_api_client.assert_called_once_with('{"type":"service_account"}')
+
+
+def test_get_track(mock_tracks):
+    google_play = GooglePlay({'type': 'service_account'})
+    mock_track = mock_tracks[0]
+    with mock.patch.object(google_play.api_client, 'get_track', return_value=mock_track) as mock_get_track:
+        track = google_play.get_track('com.example.app', mock_track.track)
+        mock_get_track.assert_called_once_with('com.example.app', mock_track.track)
+    assert track == mock_track
+
+
+def test_list_tracks(mock_tracks):
+    google_play = GooglePlay({'type': 'service_account'})
+    with mock.patch.object(google_play.api_client, 'list_tracks', return_value=mock_tracks) as mock_list_tracks:
+        tracks = google_play.list_tracks('com.example.app')
+        mock_list_tracks.assert_called_once_with('com.example.app')
+    assert tracks == mock_tracks
+
+
+@pytest.mark.parametrize('tracks, expected_version_code', [
+    ('alpha', 65),
+    ('beta', 66),
+    ('internal', 67),
+    ((), 67),
+    (('alpha', 'production'), 65),
+    (('beta', 'production'), 66),
+    (('internal', 'production'), 67),
+    (('alpha', 'beta', 'internal'), 67),
+    (('alpha', 'beta', 'internal', 'production'), 67),
+])
+def test_get_latest_build_number(tracks, expected_version_code, mock_tracks):
+    tracks = (tracks,) if isinstance(tracks, str) else tracks
+    google_play = GooglePlay({'type': 'service_account'})
+    with mock.patch.object(google_play.api_client, 'list_tracks', return_value=mock_tracks) as mock_list_tracks:
+        build_number = google_play.get_latest_build_number('com.example.app', tracks)
+        mock_list_tracks.assert_called_once_with('com.example.app')
+    assert build_number == expected_version_code
+
+
+def test_get_latest_build_number_no_tracks():
+    google_play = GooglePlay({'type': 'service_account'})
+    with mock.patch.object(google_play.api_client, 'list_tracks', return_value=[]) as mock_list_tracks:
+        with pytest.raises(GooglePlayError):
+            google_play.get_latest_build_number('com.example.app')
+        mock_list_tracks.assert_called_once_with('com.example.app')
+
+
+@pytest.mark.parametrize('track_releases', [None, []])
+def test_get_latest_build_number_no_releases(track_releases, mock_tracks):
+    mock_track = mock_tracks[0]
+    mock_track.releases = track_releases
+    google_play = GooglePlay({'type': 'service_account'})
+    with mock.patch.object(google_play.api_client, 'list_tracks', return_value=[mock_track]) as mock_list_tracks:
+        with pytest.raises(GooglePlayError):
+            google_play.get_latest_build_number('com.example.app')
+        mock_list_tracks.assert_called_once_with('com.example.app')
+
+
+@pytest.mark.parametrize('track_releases', [None, []])
+def test_get_latest_build_number_no_version_codes(track_releases, mock_tracks):
+    # Production track does not have releases with version code
+    production_track = next(t for t in mock_tracks if t.track == 'production')
+    google_play = GooglePlay({'type': 'service_account'})
+    with mock.patch.object(google_play.api_client, 'list_tracks', return_value=[production_track]) as mock_list_tracks:
+        with pytest.raises(GooglePlayError):
+            google_play.get_latest_build_number('com.example.app')
+        mock_list_tracks.assert_called_once_with('com.example.app')