From 22a7c7cb0c60b2fb91fee121fd43c2c545af4cac Mon Sep 17 00:00:00 2001 From: Chris Hammond Date: Wed, 11 Sep 2024 20:28:20 +0100 Subject: [PATCH 1/6] Resolves #187 and some other general RTR snags. Also bumps dependencies. --- caracara/modules/rtr/batch_session.py | 113 ++++++++++----- poetry.lock | 200 +++++++++++++------------- pyproject.toml | 2 +- 3 files changed, 182 insertions(+), 133 deletions(-) diff --git a/caracara/modules/rtr/batch_session.py b/caracara/modules/rtr/batch_session.py index f68fc95..07e0c80 100644 --- a/caracara/modules/rtr/batch_session.py +++ b/caracara/modules/rtr/batch_session.py @@ -1,12 +1,13 @@ """Real Time Response (RTR) batch session abstraction class.""" import concurrent.futures -from dataclasses import dataclass import logging +from dataclasses import dataclass from datetime import datetime, timedelta +from itertools import repeat from functools import partial, wraps from threading import current_thread -from typing import Dict, List +from typing import Dict, List, Tuple, Optional from falconpy import ( RealTimeResponse, @@ -46,11 +47,6 @@ class InnerRTRBatchSession: # pylint: disable=too-few-public-methods a list of InnerRTRBatchSession objects. """ - batch_id: str = None - devices: Dict = None - expiry: datetime = None - logger: logging.Logger = None - def __init__( self, batch_id: str, @@ -82,7 +78,7 @@ def generic_rtr_worker( func: partial, logger: logging.Logger, device_ids: List[str] = None, -): +) -> Tuple[str, Dict]: """ Execute a partial function against an RTR batch session. @@ -102,7 +98,7 @@ def generic_rtr_worker( func.keywords['optional_hosts'] = device_ids_in_batch response = func(batch_id=session.batch_id)['body'] logger.debug("%s | %s", thread_name, response) - return response + return thread_name, response class RTRBatchSession: @@ -127,14 +123,14 @@ class RTRBatchSession: @_batch_session_required def device_ids(self) -> List[str]: """Return a list of device IDs from all inner batch sessions.""" - return [x.devices.keys() for x in self.batch_sessions] + return [x.devices for x in self.batch_sessions] def connect( self, device_ids: List[str], queueing: bool = False, timeout: int = default_timeout, - ): + ) -> bool: """ Establish a connection to one or more hosts. @@ -148,23 +144,44 @@ def connect( batches.append(device_ids[i:i+MAX_BATCH_SESSION_HOSTS]) self.logger.info("Divided up devices into %d batches", len(batches)) - def worker(batch_device_ids: List[str], batch_func: partial): + def worker( + batch_device_ids: List[str], + batch_func: partial, + ) -> Tuple[str, Optional[InnerRTRBatchSession]]: thread_name = current_thread().name self.logger.info( "%s | Batch worker started with a list of %d devices", thread_name, len(batch_device_ids), ) - response = batch_func(host_ids=batch_device_ids)['body'] - resources = response['resources'] - self.logger.info("%s | Connected to %s systems", thread_name, len(resources)) + response = batch_func(host_ids=batch_device_ids)["body"] + self.logger.debug("%s | %s", thread_name, str(response)) + resources = response["resources"] + + # Identify devices that failed to connect and/or returned an error + # Resolves GitHub issue #187 + if not resources: + self.logger.info("%s | Resource list is empty", thread_name) + return thread_name, None + + successful_devices = { + device_id: device_data + for device_id, device_data in resources.items() + if not device_data.get("errors") + } + + if not successful_devices: + self.logger.info("%s | Successful device list is empty", thread_name) + return thread_name, None + + self.logger.info("%s | Connected to %s systems", thread_name, len(successful_devices)) self.logger.debug("%s | %s", thread_name, response) batch_data = InnerRTRBatchSession( batch_id=response['batch_id'], - devices=resources, + devices=successful_devices, expiry=datetime.now() + timedelta(seconds=SESSION_EXPIRY), logger=self.logger, ) - return batch_data + return thread_name, batch_data batch_func = partial( self.api.batch_init_sessions, @@ -176,17 +193,27 @@ def worker(batch_device_ids: List[str], batch_func: partial): with concurrent.futures.ThreadPoolExecutor( max_workers=MAX_BATCH_SESSION_THREADS ) as executor: - completed = executor.map(worker, batches, [batch_func]) + completed = executor.map(worker, batches, repeat(batch_func)) self.batch_sessions = [] - for complete in completed: - self.logger.info("Completed a batch of RTR connections") - self.batch_sessions.append(complete) + for thread_name, thread_data in completed: + self.logger.info("%s | Completed a batch of RTR connections", thread_name) + if thread_data is None: + self.logger.info("%s | Batch contained no successful connections", thread_name) + else: + self.logger.info( + "%s | Batch contained %d successful connections", + thread_name, + len(thread_data.devices), + ) + self.batch_sessions.append(thread_data) device_count = sum(len(d.devices) for d in self.batch_sessions) self.logger.info("Connected to %d devices", device_count) self.logger.debug(self.batch_sessions) + return len(self.batch_sessions) > 0 + @_batch_session_required def disconnect(self): """Disconnect the RTR batch session.""" @@ -223,15 +250,29 @@ def get( completed: List[Dict] = executor.map( partial_worker, self.batch_sessions, - [partial_get_func], + repeat(partial_get_func), ) batch_get_cmd_reqs: List[BatchGetCmdRequest] = [] - for complete in completed: - self.logger.info("Executed commands on a batch of %d hosts", len(complete)) + for thread_name, response in completed: + try: + devices = response['combined']['resources'] + except KeyError: + self.logger.warning( + "%s | Batch contained no successful get command executions", + thread_name, + ) + continue + + self.logger.info( + "%s | Executed get command on a batch of %d devices", + thread_name, + len(devices), + ) + batch_get_cmd_req = BatchGetCmdRequest( - batch_get_cmd_req_id=complete['batch_get_cmd_req_id'], - devices=complete['combined']['resources'], + batch_get_cmd_req_id=response['batch_get_cmd_req_id'], + devices=devices, ) batch_get_cmd_reqs.append(batch_get_cmd_req) @@ -282,7 +323,7 @@ def worker(batch_get_cmd_req: BatchGetCmdRequest, func: partial) -> List[GetFile timeout_duration=f'{timeout}s', ) with concurrent.futures.ThreadPoolExecutor(max_workers=threads) as executor: - completed = executor.map(worker, batch_get_cmd_reqs, [partial_func]) + completed = executor.map(worker, batch_get_cmd_reqs, repeat(partial_func)) all_get_files: List[GetFile] = [] @@ -349,7 +390,7 @@ def worker(session: InnerRTRBatchSession, func: partial): with concurrent.futures.ThreadPoolExecutor( max_workers=MAX_BATCH_SESSION_THREADS ) as executor: - completed = executor.map(worker, self.batch_sessions, [batch_func]) + completed = executor.map(worker, self.batch_sessions, repeat(batch_func)) for complete in completed: self.logger.info("Refreshed session %s", complete) @@ -403,13 +444,21 @@ def run_generic_command( completed: List[Dict] = executor.map( partial_worker, self.batch_sessions, - [partial_cmd_func], + repeat(partial_cmd_func), ) all_responses: Dict = {} - for complete in completed: - self.logger.info("Executed commands on a batch of %d hosts", len(complete)) - all_responses.update(complete['combined']['resources']) + for thread_name, response in completed: + try: + devices = response['combined']['resources'] + except KeyError: + self.logger.warning( + "%s | Batch contained no successful command executions", + thread_name, + ) + continue + self.logger.info("%s | Executed commands on a batch of %d hosts", thread_name, len(devices)) + all_responses.update(devices) return all_responses diff --git a/poetry.lock b/poetry.lock index 2f56deb..dc9a47b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -182,89 +182,89 @@ files = [ [[package]] name = "certifi" -version = "2024.7.4" +version = "2024.8.30" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, - {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, ] [[package]] name = "cffi" -version = "1.17.0" +version = "1.17.1" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" files = [ - {file = "cffi-1.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f9338cc05451f1942d0d8203ec2c346c830f8e86469903d5126c1f0a13a2bcbb"}, - {file = "cffi-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0ce71725cacc9ebf839630772b07eeec220cbb5f03be1399e0457a1464f8e1a"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c815270206f983309915a6844fe994b2fa47e5d05c4c4cef267c3b30e34dbe42"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6bdcd415ba87846fd317bee0774e412e8792832e7805938987e4ede1d13046d"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a98748ed1a1df4ee1d6f927e151ed6c1a09d5ec21684de879c7ea6aa96f58f2"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a048d4f6630113e54bb4b77e315e1ba32a5a31512c31a273807d0027a7e69ab"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24aa705a5f5bd3a8bcfa4d123f03413de5d86e497435693b638cbffb7d5d8a1b"}, - {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:856bf0924d24e7f93b8aee12a3a1095c34085600aa805693fb7f5d1962393206"}, - {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:4304d4416ff032ed50ad6bb87416d802e67139e31c0bde4628f36a47a3164bfa"}, - {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:331ad15c39c9fe9186ceaf87203a9ecf5ae0ba2538c9e898e3a6967e8ad3db6f"}, - {file = "cffi-1.17.0-cp310-cp310-win32.whl", hash = "sha256:669b29a9eca6146465cc574659058ed949748f0809a2582d1f1a324eb91054dc"}, - {file = "cffi-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:48b389b1fd5144603d61d752afd7167dfd205973a43151ae5045b35793232aa2"}, - {file = "cffi-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5d97162c196ce54af6700949ddf9409e9833ef1003b4741c2b39ef46f1d9720"}, - {file = "cffi-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ba5c243f4004c750836f81606a9fcb7841f8874ad8f3bf204ff5e56332b72b9"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb9333f58fc3a2296fb1d54576138d4cf5d496a2cc118422bd77835e6ae0b9cb"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:435a22d00ec7d7ea533db494da8581b05977f9c37338c80bc86314bec2619424"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1df34588123fcc88c872f5acb6f74ae59e9d182a2707097f9e28275ec26a12d"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df8bb0010fdd0a743b7542589223a2816bdde4d94bb5ad67884348fa2c1c67e8"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8b5b9712783415695663bd463990e2f00c6750562e6ad1d28e072a611c5f2a6"}, - {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ffef8fd58a36fb5f1196919638f73dd3ae0db1a878982b27a9a5a176ede4ba91"}, - {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e67d26532bfd8b7f7c05d5a766d6f437b362c1bf203a3a5ce3593a645e870b8"}, - {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45f7cd36186db767d803b1473b3c659d57a23b5fa491ad83c6d40f2af58e4dbb"}, - {file = "cffi-1.17.0-cp311-cp311-win32.whl", hash = "sha256:a9015f5b8af1bb6837a3fcb0cdf3b874fe3385ff6274e8b7925d81ccaec3c5c9"}, - {file = "cffi-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:b50aaac7d05c2c26dfd50c3321199f019ba76bb650e346a6ef3616306eed67b0"}, - {file = "cffi-1.17.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aec510255ce690d240f7cb23d7114f6b351c733a74c279a84def763660a2c3bc"}, - {file = "cffi-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2770bb0d5e3cc0e31e7318db06efcbcdb7b31bcb1a70086d3177692a02256f59"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db9a30ec064129d605d0f1aedc93e00894b9334ec74ba9c6bdd08147434b33eb"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a47eef975d2b8b721775a0fa286f50eab535b9d56c70a6e62842134cf7841195"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3e0992f23bbb0be00a921eae5363329253c3b86287db27092461c887b791e5e"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6107e445faf057c118d5050560695e46d272e5301feffda3c41849641222a828"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb862356ee9391dc5a0b3cbc00f416b48c1b9a52d252d898e5b7696a5f9fe150"}, - {file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c1c13185b90bbd3f8b5963cd8ce7ad4ff441924c31e23c975cb150e27c2bf67a"}, - {file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17c6d6d3260c7f2d94f657e6872591fe8733872a86ed1345bda872cfc8c74885"}, - {file = "cffi-1.17.0-cp312-cp312-win32.whl", hash = "sha256:c3b8bd3133cd50f6b637bb4322822c94c5ce4bf0d724ed5ae70afce62187c492"}, - {file = "cffi-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:dca802c8db0720ce1c49cce1149ff7b06e91ba15fa84b1d59144fef1a1bc7ac2"}, - {file = "cffi-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ce01337d23884b21c03869d2f68c5523d43174d4fc405490eb0091057943118"}, - {file = "cffi-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cab2eba3830bf4f6d91e2d6718e0e1c14a2f5ad1af68a89d24ace0c6b17cced7"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14b9cbc8f7ac98a739558eb86fabc283d4d564dafed50216e7f7ee62d0d25377"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b00e7bcd71caa0282cbe3c90966f738e2db91e64092a877c3ff7f19a1628fdcb"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41f4915e09218744d8bae14759f983e466ab69b178de38066f7579892ff2a555"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4760a68cab57bfaa628938e9c2971137e05ce48e762a9cb53b76c9b569f1204"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:011aff3524d578a9412c8b3cfaa50f2c0bd78e03eb7af7aa5e0df59b158efb2f"}, - {file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a003ac9edc22d99ae1286b0875c460351f4e101f8c9d9d2576e78d7e048f64e0"}, - {file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ef9528915df81b8f4c7612b19b8628214c65c9b7f74db2e34a646a0a2a0da2d4"}, - {file = "cffi-1.17.0-cp313-cp313-win32.whl", hash = "sha256:70d2aa9fb00cf52034feac4b913181a6e10356019b18ef89bc7c12a283bf5f5a"}, - {file = "cffi-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:b7b6ea9e36d32582cda3465f54c4b454f62f23cb083ebc7a94e2ca6ef011c3a7"}, - {file = "cffi-1.17.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:964823b2fc77b55355999ade496c54dde161c621cb1f6eac61dc30ed1b63cd4c"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:516a405f174fd3b88829eabfe4bb296ac602d6a0f68e0d64d5ac9456194a5b7e"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dec6b307ce928e8e112a6bb9921a1cb00a0e14979bf28b98e084a4b8a742bd9b"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4094c7b464cf0a858e75cd14b03509e84789abf7b79f8537e6a72152109c76e"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2404f3de742f47cb62d023f0ba7c5a916c9c653d5b368cc966382ae4e57da401"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa9d43b02a0c681f0bfbc12d476d47b2b2b6a3f9287f11ee42989a268a1833c"}, - {file = "cffi-1.17.0-cp38-cp38-win32.whl", hash = "sha256:0bb15e7acf8ab35ca8b24b90af52c8b391690ef5c4aec3d31f38f0d37d2cc499"}, - {file = "cffi-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:93a7350f6706b31f457c1457d3a3259ff9071a66f312ae64dc024f049055f72c"}, - {file = "cffi-1.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a2ddbac59dc3716bc79f27906c010406155031a1c801410f1bafff17ea304d2"}, - {file = "cffi-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6327b572f5770293fc062a7ec04160e89741e8552bf1c358d1a23eba68166759"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbc183e7bef690c9abe5ea67b7b60fdbca81aa8da43468287dae7b5c046107d4"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bdc0f1f610d067c70aa3737ed06e2726fd9d6f7bfee4a351f4c40b6831f4e82"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d872186c1617d143969defeadac5a904e6e374183e07977eedef9c07c8953bf"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d46ee4764b88b91f16661a8befc6bfb24806d885e27436fdc292ed7e6f6d058"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f76a90c345796c01d85e6332e81cab6d70de83b829cf1d9762d0a3da59c7932"}, - {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e60821d312f99d3e1569202518dddf10ae547e799d75aef3bca3a2d9e8ee693"}, - {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:eb09b82377233b902d4c3fbeeb7ad731cdab579c6c6fda1f763cd779139e47c3"}, - {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:24658baf6224d8f280e827f0a50c46ad819ec8ba380a42448e24459daf809cf4"}, - {file = "cffi-1.17.0-cp39-cp39-win32.whl", hash = "sha256:0fdacad9e0d9fc23e519efd5ea24a70348305e8d7d85ecbb1a5fa66dc834e7fb"}, - {file = "cffi-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:7cbc78dc018596315d4e7841c8c3a7ae31cc4d638c9b627f87d52e8abaaf2d29"}, - {file = "cffi-1.17.0.tar.gz", hash = "sha256:f3157624b7558b914cb039fd1af735e5e8049a87c817cc215109ad1c8779df76"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, ] [package.dependencies] @@ -543,13 +543,13 @@ pyflakes = ">=3.2.0,<3.3.0" [[package]] name = "idna" -version = "3.7" +version = "3.8" description = "Internationalized Domain Names in Applications (IDNA)" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" files = [ - {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, - {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, + {file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"}, + {file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"}, ] [[package]] @@ -719,30 +719,30 @@ files = [ [[package]] name = "pbr" -version = "6.0.0" +version = "6.1.0" description = "Python Build Reasonableness" optional = false python-versions = ">=2.6" files = [ - {file = "pbr-6.0.0-py2.py3-none-any.whl", hash = "sha256:4a7317d5e3b17a3dccb6a8cfe67dab65b20551404c52c8ed41279fa4f0cb4cda"}, - {file = "pbr-6.0.0.tar.gz", hash = "sha256:d1377122a5a00e2f940ee482999518efe16d745d423a670c27773dfbc3c9a7d9"}, + {file = "pbr-6.1.0-py2.py3-none-any.whl", hash = "sha256:a776ae228892d8013649c0aeccbb3d5f99ee15e005a4cbb7e61d55a067b28a2a"}, + {file = "pbr-6.1.0.tar.gz", hash = "sha256:788183e382e3d1d7707db08978239965e8b9e4e5ed42669bf4758186734d5f24"}, ] [[package]] name = "platformdirs" -version = "4.2.2" +version = "4.3.2" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, - {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, + {file = "platformdirs-4.3.2-py3-none-any.whl", hash = "sha256:eb1c8582560b34ed4ba105009a4badf7f6f85768b30126f351328507b2beb617"}, + {file = "platformdirs-4.3.2.tar.gz", hash = "sha256:9e5e27a08aa095dd127b9f2e764d74254f482fef22b0970773bfba79d091ab8c"}, ] [package.extras] -docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] -type = ["mypy (>=1.8)"] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] [[package]] name = "pluggy" @@ -993,13 +993,13 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pylint" -version = "3.2.6" +version = "3.2.7" description = "python code static checker" optional = false python-versions = ">=3.8.0" files = [ - {file = "pylint-3.2.6-py3-none-any.whl", hash = "sha256:03c8e3baa1d9fb995b12c1dbe00aa6c4bcef210c2a2634374aedeb22fb4a8f8f"}, - {file = "pylint-3.2.6.tar.gz", hash = "sha256:a5d01678349454806cff6d886fb072294f56a58c4761278c97fb557d708e1eb3"}, + {file = "pylint-3.2.7-py3-none-any.whl", hash = "sha256:02f4aedeac91be69fb3b4bea997ce580a4ac68ce58b89eaefeaf06749df73f4b"}, + {file = "pylint-3.2.7.tar.gz", hash = "sha256:1b7a721b575eaeaa7d39db076b6e7743c993ea44f57979127c517c6c572c803e"}, ] [package.dependencies] @@ -1109,13 +1109,13 @@ test = ["coverage[toml] (>=5.2)", "hypothesis", "pytest (>=6.0)", "pytest-benchm [[package]] name = "pytest" -version = "8.3.2" +version = "8.3.3" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, - {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, + {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, + {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, ] [package.dependencies] @@ -1326,13 +1326,13 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "rich" -version = "13.7.1" +version = "13.8.1" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.7.0" files = [ - {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, - {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, + {file = "rich-13.8.1-py3-none-any.whl", hash = "sha256:1760a3c0848469b97b558fc61c85233e3dafb69c7a071b4d60c38099d3cd4c06"}, + {file = "rich-13.8.1.tar.gz", hash = "sha256:8260cda28e3db6bf04d2d1ef4dbc03ba80a824c88b0e7668a0f23126a424844a"}, ] [package.dependencies] @@ -1383,17 +1383,17 @@ files = [ [[package]] name = "stevedore" -version = "5.2.0" +version = "5.3.0" description = "Manage dynamic plugins for Python applications" optional = false python-versions = ">=3.8" files = [ - {file = "stevedore-5.2.0-py3-none-any.whl", hash = "sha256:1c15d95766ca0569cad14cb6272d4d31dae66b011a929d7c18219c176ea1b5c9"}, - {file = "stevedore-5.2.0.tar.gz", hash = "sha256:46b93ca40e1114cea93d738a6c1e365396981bb6bb78c27045b7587c9473544d"}, + {file = "stevedore-5.3.0-py3-none-any.whl", hash = "sha256:1efd34ca08f474dad08d9b19e934a22c68bb6fe416926479ba29e5013bcc8f78"}, + {file = "stevedore-5.3.0.tar.gz", hash = "sha256:9a64265f4060312828151c204efbe9b7a9852a0d9228756344dbc7e4023e375a"}, ] [package.dependencies] -pbr = ">=2.0.0,<2.1.0 || >2.1.0" +pbr = ">=2.0.0" [[package]] name = "tabulate" @@ -1444,13 +1444,13 @@ files = [ [[package]] name = "tomlkit" -version = "0.13.0" +version = "0.13.2" description = "Style preserving TOML library" optional = false python-versions = ">=3.8" files = [ - {file = "tomlkit-0.13.0-py3-none-any.whl", hash = "sha256:7075d3042d03b80f603482d69bf0c8f345c2b30e41699fd8883227f89972b264"}, - {file = "tomlkit-0.13.0.tar.gz", hash = "sha256:08ad192699734149f5b97b45f1f18dad7eb1b6d16bc72ad0c2335772650d7b72"}, + {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, + {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index 3455cc4..2d02ffd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "caracara" -version = "0.8.0" +version = "0.8.1" description = "The CrowdStrike Falcon Developer Toolkit" authors = [ "CrowdStrike " ] readme = "README.md" From 80da94803b521a79b1ff787126f1a0e8853c3a63 Mon Sep 17 00:00:00 2001 From: Chris Hammond Date: Wed, 11 Sep 2024 20:43:43 +0100 Subject: [PATCH 2/6] Resolves linting issues --- caracara/modules/rtr/batch_session.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/caracara/modules/rtr/batch_session.py b/caracara/modules/rtr/batch_session.py index 07e0c80..a05783e 100644 --- a/caracara/modules/rtr/batch_session.py +++ b/caracara/modules/rtr/batch_session.py @@ -396,7 +396,7 @@ def worker(session: InnerRTRBatchSession, func: partial): self.logger.info("Refreshed session %s", complete) @_batch_session_required - def run_generic_command( + def run_generic_command( # pylint: disable=too-many-locals self, command_string: str, device_ids: List[str] = None, @@ -457,7 +457,11 @@ def run_generic_command( thread_name, ) continue - self.logger.info("%s | Executed commands on a batch of %d hosts", thread_name, len(devices)) + self.logger.info( + "%s | Executed commands on a batch of %d hosts", + thread_name, + len(devices) + ) all_responses.update(devices) return all_responses From 5bc830cc6418298b7b4b9094f2e4b371d6c3a74e Mon Sep 17 00:00:00 2001 From: Chris Hammond Date: Wed, 16 Oct 2024 11:21:12 -0400 Subject: [PATCH 3/6] Caracara 0.9.0: Preparation for Python 3.13 (and huge code cleanup) (#206) * Major code formatting cleanup, deprecation of Python 3.8, and removal of setuptools requirement * Add note about Python version compatibility * Temporarily set Python 3.13 CI version to 3.13 RC2 * Changes to sate isort and black together * Unified development dependencies * Bumps py7zr to 0.22.0 as we no longer support Python 3.7. Also, bumps caracara-filters to 1.0.0+. * Bumps dependencies --- .flake8 | 1 + .github/workflows/code-quality.yml | 37 +- README.md | 2 + caracara/__init__.py | 1 + caracara/client.py | 11 +- caracara/common/__init__.py | 5 +- caracara/common/batching.py | 27 +- caracara/common/constants.py | 1 + caracara/common/csdialog.py | 35 +- caracara/common/decorators.py | 16 +- caracara/common/exceptions.py | 68 +- caracara/common/interpolation.py | 10 +- caracara/common/meta.py | 4 +- caracara/common/module.py | 2 +- caracara/common/pagination.py | 89 +- caracara/common/policy_wrapper.py | 6 +- caracara/common/sorting.py | 1 + caracara/filters/decorators.py | 20 +- caracara/modules/__init__.py | 19 +- caracara/modules/custom_ioa/__init__.py | 7 +- caracara/modules/custom_ioa/custom_ioa.py | 62 +- caracara/modules/custom_ioa/rule_types.py | 13 +- caracara/modules/custom_ioa/rules.py | 46 +- caracara/modules/flight_control/__init__.py | 3 +- .../modules/flight_control/flight_control.py | 14 +- caracara/modules/hosts/__init__.py | 3 +- caracara/modules/hosts/_containment.py | 15 +- caracara/modules/hosts/_data_history.py | 19 +- caracara/modules/hosts/_groups.py | 91 +- caracara/modules/hosts/_hiding.py | 36 +- caracara/modules/hosts/_online_state.py | 29 +- caracara/modules/hosts/_tagging.py | 39 +- caracara/modules/hosts/hosts.py | 59 +- .../modules/prevention_policies/__init__.py | 7 +- .../prevention_policies.py | 24 +- .../modules/prevention_policies/template.py | 19 +- .../modules/response_policies/__init__.py | 7 +- .../response_policies/response_policies.py | 38 +- .../modules/response_policies/template.py | 22 +- caracara/modules/rtr/__init__.py | 11 +- caracara/modules/rtr/batch_session.py | 99 ++- caracara/modules/rtr/constants.py | 1 + caracara/modules/rtr/get_file.py | 9 +- caracara/modules/rtr/rtr.py | 44 +- .../sensor_download/sensor_download.py | 10 +- .../sensor_update_policies/__init__.py | 8 +- .../sensor_update_policies.py | 19 +- caracara/modules/users/users.py | 10 +- examples/common/__init__.py | 7 +- examples/common/example.py | 46 +- examples/common/prompts.py | 1 + examples/common/timer.py | 3 +- examples/common/utils.py | 8 +- .../flight_control/describe_child_cids.py | 3 +- examples/hosts/find_devices.py | 18 +- examples/hosts/find_stale_sensors.py | 22 +- examples/hosts/list_all_devices.py | 11 +- examples/hosts/list_all_group_member_ids.py | 23 +- examples/hosts/list_all_group_members.py | 23 +- examples/hosts/list_all_groups.py | 11 +- examples/hosts/list_device_states.py | 11 +- examples/hosts/list_hidden_devices.py | 11 +- examples/hosts/list_login_history.py | 24 +- examples/hosts/list_network_history.py | 24 +- examples/hosts/list_windows_devices.py | 19 +- examples/hosts/show_agent_versions.py | 13 +- .../create_prevention_policy.py | 3 +- .../describe_prevention_policies.py | 10 +- .../create_response_policy.py | 5 +- .../describe_response_policies.py | 10 +- examples/rtr/clear_queued_sessions.py | 5 +- examples/rtr/describe_put_files.py | 7 +- examples/rtr/describe_queued_sessions.py | 5 +- examples/rtr/describe_scripts.py | 5 +- examples/rtr/download_event_log.py | 13 +- examples/rtr/queue_command.py | 30 +- .../get_maintenance_token.py | 10 +- examples/users/add_user.py | 4 +- examples/users/delete_user.py | 3 +- examples/users/describe_roles.py | 7 +- examples/users/describe_users.py | 7 +- poetry.lock | 782 ++++++++++-------- pyproject.toml | 24 +- tests/integration_tests/test_general.py | 8 +- tests/integration_tests/test_hosts.py | 12 +- tests/unit_tests/test_custom_ioas.py | 295 ++++--- tests/unit_tests/test_hosts.py | 258 +++--- tests/unit_tests/test_prevention_policies.py | 52 +- tests/unit_tests/test_response_policies.py | 65 +- 89 files changed, 1578 insertions(+), 1439 deletions(-) diff --git a/.flake8 b/.flake8 index 7da1f96..ad98539 100644 --- a/.flake8 +++ b/.flake8 @@ -1,2 +1,3 @@ [flake8] max-line-length = 100 +extend-ignore = E203 diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index dac9a13..dfdfa1e 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -15,27 +15,56 @@ jobs: codequality: strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + # TODO: update this to 3.13 (final release) when this is released on 1 October 2024 + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13.0-rc.2"] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install Poetry via pipx run: pipx install poetry + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: 'poetry' + - name: Install dependencies run: poetry install + - name: Lint package source with flake8 - run: poetry run flake8 caracara/ --show-source --statistics + run: | + poetry run flake8 caracara/ --show-source --statistics + poetry run flake8 examples/ --show-source --statistics + poetry run flake8 tests/ --show-source --statistics + - name: Lint package source with pylint if: success() || failure() - run: poetry run pylint caracara/ + run: | + poetry run pylint caracara/ + poetry run pylint examples/ + poetry run pylint tests/ + - name: Lint package docstrings and comments with pydocstyle if: success() || failure() - run: poetry run pydocstyle caracara/ + run: | + poetry run pydocstyle caracara/ + poetry run pydocstyle examples/ + + - name: Lint imports with isort + if: success() || failure() + run: | + poetry run isort -c caracara/ + poetry run isort -c examples/ + poetry run isort -c tests/ + + - name: Lint package with black + if: success() || failure() + run: | + poetry run black -l 100 --check caracara/ + poetry run black -l 100 --check examples/ + poetry run black -l 100 --check tests/ + - name: Analyse code for security issues with bandit if: success() || failure() run: | diff --git a/README.md b/README.md index d6e1595..9dbdec4 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,8 @@ A few of the developer experience enhancements provided by the Caracara toolkit Caracara supports all major Python packaging solutions. Instructions for [Poetry](https://python-poetry.org) and [Pip](https://pypi.org/project/pip/) are provided below. +Caracara supports Python versions that are still supported by the Python Software Foundation, i.e., **Python 3.8 and up**. +

Installing Caracara from PyPI using Poetry (Recommended!)

diff --git a/caracara/__init__.py b/caracara/__init__.py index 7ac8d3b..861caf2 100644 --- a/caracara/__init__.py +++ b/caracara/__init__.py @@ -17,6 +17,7 @@ Developer Toolkit for FalconPy """ + __all__ = ["Client", "Policy"] from caracara.client import Client diff --git a/caracara/client.py b/caracara/client.py index 1bc8d47..077bf45 100644 --- a/caracara/client.py +++ b/caracara/client.py @@ -13,15 +13,12 @@ import logging try: - from falconpy import ( - OAuth2, - confirm_base_region, - confirm_base_url - ) + from falconpy import OAuth2, confirm_base_region, confirm_base_url except ImportError as no_falconpy: raise SystemExit("The crowdstrike-falconpy library is not installed.") from no_falconpy from caracara_filters import FQLGenerator + from caracara.common.interpolation import VariableInterpolator from caracara.common.meta import user_agent_string from caracara.common.module import ModuleMapper @@ -117,7 +114,9 @@ def __init__( # pylint: disable=R0913,R0914,R0915 self.logger.info( "Client ID: %s; Cloud: %s; Member CID: %s", - client_id, cloud_name, member_cid, + client_id, + cloud_name, + member_cid, ) self.logger.debug("SSL verification is %s", ssl_verify) self.logger.debug("Timeout: %s", str(timeout)) diff --git a/caracara/common/__init__.py b/caracara/common/__init__.py index 136be72..4b8ba5b 100644 --- a/caracara/common/__init__.py +++ b/caracara/common/__init__.py @@ -1,4 +1,5 @@ """Caracara: Common functions and data imports.""" + __all__ = [ "DEFAULT_DATA_BATCH_SIZE", "FalconApiModule", @@ -7,7 +8,7 @@ "user_agent_string", ] -from caracara.common.constants import SCROLL_BATCH_SIZE, DEFAULT_DATA_BATCH_SIZE -from caracara.common.policy_wrapper import Policy +from caracara.common.constants import DEFAULT_DATA_BATCH_SIZE, SCROLL_BATCH_SIZE from caracara.common.meta import user_agent_string from caracara.common.module import FalconApiModule +from caracara.common.policy_wrapper import Policy diff --git a/caracara/common/batching.py b/caracara/common/batching.py index 212dbd6..612ab1e 100644 --- a/caracara/common/batching.py +++ b/caracara/common/batching.py @@ -8,17 +8,16 @@ For paginated results that send back IDs, the pagination code file contains the required code to pull this data down as quickly as possible. """ + import concurrent.futures import logging import multiprocessing - from functools import partial from threading import current_thread from typing import Callable, Dict, List, Tuple from caracara.common.constants import DEFAULT_DATA_BATCH_SIZE - BATCH_LOGGER = logging.getLogger(__name__) @@ -59,7 +58,9 @@ def batch_get_data( BATCH_LOGGER.debug(str(lookup_ids)) # Divide the list of item IDs into a list of lists, each of size data_batch_size - batches = [lookup_ids[i:i+data_batch_size] for i in range(0, len(lookup_ids), data_batch_size)] + batches = [ + lookup_ids[i : i + data_batch_size] for i in range(0, len(lookup_ids), data_batch_size) + ] BATCH_LOGGER.info("Divided the item IDs into %d batches", len(batches)) threads = batch_data_pull_threads() @@ -72,7 +73,9 @@ def worker( thread_name = current_thread().name BATCH_LOGGER.info( "%s | Batch worker started with a list of %d items. Function: %s", - thread_name, len(worker_lookup_ids), batch_func.__name__, + thread_name, + len(worker_lookup_ids), + batch_func.__name__, ) body: Dict = batch_func(ids=worker_lookup_ids)["body"] # Gracefully handle a lack of returned resources, usually as a result of an error @@ -100,14 +103,14 @@ def worker( resources_dict = {} for resource in resources: - if 'id' in resource: - resources_dict[resource['id']] = resource - elif 'device_id' in resource: - resources_dict[resource['device_id']] = resource - elif 'child_cid' in resource: - resources_dict[resource['child_cid']] = resource - elif 'uuid' in resource: - resources_dict[resource['uuid']] = resource + if "id" in resource: + resources_dict[resource["id"]] = resource + elif "device_id" in resource: + resources_dict[resource["device_id"]] = resource + elif "child_cid" in resource: + resources_dict[resource["child_cid"]] = resource + elif "uuid" in resource: + resources_dict[resource["uuid"]] = resource else: raise KeyError("No ID field to build the dictionary from") diff --git a/caracara/common/constants.py b/caracara/common/constants.py index 48b8459..d3f2402 100644 --- a/caracara/common/constants.py +++ b/caracara/common/constants.py @@ -1,4 +1,5 @@ """Common constants to be shared throughout Caracara.""" + from enum import Enum, EnumMeta # Batch size of data downloaded via a multi-threaded data pull diff --git a/caracara/common/csdialog.py b/caracara/common/csdialog.py index 73ce08f..622ab52 100644 --- a/caracara/common/csdialog.py +++ b/caracara/common/csdialog.py @@ -1,5 +1,7 @@ """Caracara: CrowdStrike Style Radio Dialog for Prompt Toolkit.""" + from __future__ import annotations + from typing import Any, Sequence, TypeVar from prompt_toolkit.application import Application @@ -13,7 +15,6 @@ from prompt_toolkit.styles import BaseStyle, Style, merge_styles from prompt_toolkit.widgets import Button, Dialog, Label, RadioList - # Prompt Toolkit uses _T for the generic type _T = TypeVar("_T") @@ -41,21 +42,23 @@ def _return_none() -> None: get_app().exit() -CS_STYLE = Style.from_dict({ - 'button': 'fg:#1A1A1A bold', - 'button.arrow': 'fg:#FC0000 bold', - 'button.focused': 'fg:#FFFFFF bg:#2F8BAA', - 'button.text': 'fg:#1A1A1A bold', - 'dialog': 'bg:#58595B', - 'dialog.body': 'bg:#F3F3F4', - 'dialog.body label': 'fg:#FC0000', - 'dialog frame.label': 'bg:#F3F4F4 #1A1A1A', - 'dialog shadow': 'bg:#68696B', - 'frame.label': 'fg:#FC0000', - 'label': '#FC0000', - 'radio-list': '#1A1A1A', - 'radio-checked': 'fg:#2F8BAA', -}) +CS_STYLE = Style.from_dict( + { + "button": "fg:#1A1A1A bold", + "button.arrow": "fg:#FC0000 bold", + "button.focused": "fg:#FFFFFF bg:#2F8BAA", + "button.text": "fg:#1A1A1A bold", + "dialog": "bg:#58595B", + "dialog.body": "bg:#F3F3F4", + "dialog.body label": "fg:#FC0000", + "dialog frame.label": "bg:#F3F4F4 #1A1A1A", + "dialog shadow": "bg:#68696B", + "frame.label": "fg:#FC0000", + "label": "#FC0000", + "radio-list": "#1A1A1A", + "radio-checked": "fg:#2F8BAA", + } +) def csradiolist_dialog( # pylint: disable=too-many-arguments diff --git a/caracara/common/decorators.py b/caracara/common/decorators.py index f33c935..9731507 100644 --- a/caracara/common/decorators.py +++ b/caracara/common/decorators.py @@ -1,4 +1,5 @@ """Caracara module-agnostic decorators.""" + from functools import wraps from inspect import signature from typing import Callable @@ -10,10 +11,10 @@ def platform_name_check(func: Callable): """Decorate a function to ensure that a platform_name argument is within the specified list.""" # Load the function's signature, and confirm that the platform_name parameter has been added sig = signature(func) - if 'platform_name' not in sig.parameters: - raise ValueError(f'The function {func.__name__} does not have a platform_name parameter') + if "platform_name" not in sig.parameters: + raise ValueError(f"The function {func.__name__} does not have a platform_name parameter") - if 'self' not in sig.parameters: + if "self" not in sig.parameters: raise ValueError( f"The function {func.__name__} must be a class function with a self parameter" ) @@ -25,10 +26,10 @@ def wrapper(*args, **kwargs): # Apply any default parameters (e.g., platform_name=None where this is not specified) _args.apply_defaults() # Get references to the self and platform_name arguments for logging and processing purposes - self = _args.arguments['self'] - platform_name = _args.arguments['platform_name'] + self = _args.arguments["self"] + platform_name = _args.arguments["platform_name"] - if hasattr(self, 'logger'): + if hasattr(self, "logger"): self.logger.debug(f"Entering filter_string wrapper for function {func.__name__}") if platform_name and isinstance(platform_name, str): @@ -41,9 +42,10 @@ def wrapper(*args, **kwargs): # If we get this far, either the platform_name given is reasonable, or it was None (which # is reasonable if the function signature allows this) - if hasattr(self, 'logger'): + if hasattr(self, "logger"): self.logger.debug("Exiting decorator") # Return to the original function return func(*_args.args, **_args.kwargs) + return wrapper diff --git a/caracara/common/exceptions.py b/caracara/common/exceptions.py index 18f9700..749b325 100644 --- a/caracara/common/exceptions.py +++ b/caracara/common/exceptions.py @@ -1,4 +1,5 @@ """Caracara exceptions.""" + from typing import Dict, List from caracara.common.constants import OnlineState @@ -13,10 +14,7 @@ class GenericAPIError(BaseCaracaraError): def __init__(self, errors: List[Dict] = None): """Construct an instance of the GenericAPIError class.""" - self.errors = [{ - "code": 500, - "message": "An unexpected error has occurred" - }] + self.errors = [{"code": 500, "message": "An unexpected error has occurred"}] if errors: self.errors = errors super().__init__(self.errors) @@ -39,10 +37,12 @@ class MustProvideFilterOrID(GenericAPIError): def __init__(self): """Construct an instance of the MustProvideFilterOrID class.""" - self.errors = [{ - "code": 500, - "message": "You must provide either a Falcon Filter or an ID to use this method" - }] + self.errors = [ + { + "code": 500, + "message": "You must provide either a Falcon Filter or an ID to use this method", + } + ] super().__init__(self.errors) @@ -51,10 +51,12 @@ class MustProvideFilter(GenericAPIError): def __init__(self): """Construct an instance of the MustProvideFilter class.""" - self.errors = [{ - "code": 500, - "message": "You must provide a Falcon Filter in order to use this method" - }] + self.errors = [ + { + "code": 500, + "message": "You must provide a Falcon Filter in order to use this method", + } + ] super().__init__(self.errors) @@ -63,10 +65,12 @@ class HostGroupNotFound(GenericAPIError): def __init__(self): """Construct an instance of the HostGroupNotFound class.""" - self.errors = [{ - "code": 404, - "message": "The Falcon Filter you provided returned no Host Group matches" - }] + self.errors = [ + { + "code": 404, + "message": "The Falcon Filter you provided returned no Host Group matches", + } + ] super().__init__(self.errors) @@ -75,10 +79,12 @@ class DeviceNotFound(GenericAPIError): def __init__(self): """Construct an instance of the DeviceNotFound class.""" - self.errors = [{ - "code": 404, - "message": "The Falcon Filter you provided returned no device matches" - }] + self.errors = [ + { + "code": 404, + "message": "The Falcon Filter you provided returned no device matches", + } + ] super().__init__(self.errors) @@ -93,10 +99,7 @@ def __init__(self, arg_name: str = None): arg_name = f"The required argument {arg_name}" arg_str = f"{arg_name} was not provided" - self.errors = [{ - "code": 500, - "message": arg_str - }] + self.errors = [{"code": 500, "message": arg_str}] super().__init__(self.errors) @@ -111,10 +114,7 @@ def __init__(self, arg_names: List[str] = None): arg_names = f"The required arguments {', '.join(arg_names)}" arg_str = f"{arg_names} were not provided" - self.errors = [{ - "code": 500, - "message": arg_str - }] + self.errors = [{"code": 500, "message": arg_str}] super().__init__(self.errors) @@ -123,9 +123,11 @@ class InvalidOnlineState(GenericAPIError): def __init__(self, online_state_string): """Construct an instance of the InvalidOnlineState class.""" - self.errors = [{ - "code": 500, - "message": f"Invalid online state '{online_state_string}'. \ - Expected one of {OnlineState.VALUES}." - }] + self.errors = [ + { + "code": 500, + "message": f"Invalid online state '{online_state_string}'. \ + Expected one of {OnlineState.VALUES}.", + } + ] super().__init__(self.errors) diff --git a/caracara/common/interpolation.py b/caracara/common/interpolation.py index ade9648..0c06fdc 100644 --- a/caracara/common/interpolation.py +++ b/caracara/common/interpolation.py @@ -24,6 +24,7 @@ It may also be advantageous for some scripting implementations, where the user may wish to configure Caracara dynamically or interactively by using environment variables. """ + import logging import os import re @@ -45,7 +46,7 @@ def __init__(self): an escape character. The pattern will return just the inner text (without the $ or {}). """ - self._pattern = re.compile(r'(? str: actual curly brace character, plus a third curly brace for the f-string variable name. """ - interpolation_str = f'${{{match}}}' + interpolation_str = f"${{{match}}}" self.logger.debug("Interpolating the environment variable: %s", match) - output_string = output_string.replace( - interpolation_str, - os.environ.get(match, match) - ) + output_string = output_string.replace(interpolation_str, os.environ.get(match, match)) # Unescape by replacing double dollar signs with single dollar signs output_string = output_string.replace("$$", "$") diff --git a/caracara/common/meta.py b/caracara/common/meta.py index 41b68aa..93b39ec 100644 --- a/caracara/common/meta.py +++ b/caracara/common/meta.py @@ -4,9 +4,9 @@ a custom user agent string based on Caracara's version. """ -import pkg_resources +import importlib.metadata -_pkg_version = pkg_resources.get_distribution("caracara").version +_pkg_version = importlib.metadata.version("caracara") def user_agent_string(): diff --git a/caracara/common/module.py b/caracara/common/module.py index f31c971..8761686 100644 --- a/caracara/common/module.py +++ b/caracara/common/module.py @@ -4,8 +4,8 @@ Caracara API module. All modules, including Hosts, Prevention Policies, etc. derive from this abstract base class. """ -import logging +import logging from abc import ABC, abstractmethod from falconpy import OAuth2 diff --git a/caracara/common/pagination.py b/caracara/common/pagination.py index 417e780..d4e93a6 100644 --- a/caracara/common/pagination.py +++ b/caracara/common/pagination.py @@ -87,19 +87,19 @@ Param Name = the name of the kwarg that should be swapped out in each call Value List = a list of strings to be iterated over """ + import concurrent.futures import logging - from functools import partial from threading import current_thread -from typing import Callable, Dict, List +from typing import Callable, Dict, List, Union from caracara.common.batching import batch_data_pull_threads from caracara.common.constants import PAGINATION_LIMIT, SCROLL_BATCH_SIZE def all_pages_numbered_offset( - func: Callable[[Dict[str, Dict]], List[Dict] or List[str]], + func: Callable[[Dict[str, Dict]], Union[List[Dict], List[str]]], logger: logging.Logger, body: Dict = None, limit: int = PAGINATION_LIMIT, @@ -121,8 +121,8 @@ def all_pages_numbered_offset( if body: req_body.update(body) - response = func(body=req_body)['body'] - resources = response['resources'] + response = func(body=req_body)["body"] + resources = response["resources"] logger.info(f"Retrieved a batch of {len(resources)} items") logger.debug(response) all_resources.extend(resources) @@ -131,8 +131,8 @@ def all_pages_numbered_offset( if not all_resources: return [] - if response['meta']['pagination']['total'] > len(all_resources): - offset = response['meta']['pagination']['offset'] + if response["meta"]["pagination"]["total"] > len(all_resources): + offset = response["meta"]["pagination"]["offset"] else: found_all = True @@ -140,7 +140,7 @@ def all_pages_numbered_offset( def _numbered_offset_parallel_worker( - func: Callable[[Dict[str, Dict]], List[Dict] or List[str]] or partial, + func: Union[Callable[[Dict[str, Dict]], Union[List[Dict], List[str]]], partial], limit: int, logger: logging.Logger, batch_offset: int, @@ -149,23 +149,28 @@ def _numbered_offset_parallel_worker( if isinstance(func, partial): logger.info( "%s | Batch worker started with limit %d and offset %d; partial function: %s", - thread_name, limit, batch_offset, func.func.__name__, + thread_name, + limit, + batch_offset, + func.func.__name__, ) else: logger.info( "%s | Batch worker started with offset %s; function: %s", - thread_name, limit, func.__name__, + thread_name, + limit, + func.__name__, ) - response = func(offset=batch_offset, limit=limit)['body'] + response = func(offset=batch_offset, limit=limit)["body"] logger.debug("%s | %s", thread_name, response) - resources = response.get('resources', []) + resources = response.get("resources", []) logger.info("%s | Retrieved %d resources", thread_name, len(resources)) return resources def all_pages_numbered_offset_parallel( - func: Callable[[Dict[str, Dict]], List[Dict] or List[str]] or partial, + func: Union[Callable[[Dict[str, Dict]], Union[List[Dict], List[str]]], partial], logger: logging.Logger, limit: int = PAGINATION_LIMIT, ) -> List[Dict]: @@ -174,27 +179,29 @@ def all_pages_numbered_offset_parallel( if isinstance(func, partial): logger.info( "Pagination Style 1: Grabbing all pages from the partial %s function (limit: %d)", - func.func.__name__, limit, + func.func.__name__, + limit, ) else: logger.info( "Pagination Style 1: Grabbing all pages from the %s function (limit: %d)", - func.__name__, limit, + func.__name__, + limit, ) # Get the first page to figure out how many items to pull logger.info("Grabbing first batch of items 1 to up to %s", limit) - response: Dict = func(offset=0, limit=limit)['body'] + response: Dict = func(offset=0, limit=limit)["body"] logger.debug(response) - all_resources: List[Dict] = response.get('resources', []) + all_resources: List[Dict] = response.get("resources", []) logger.info("Retrieved a batch of %d items", len(all_resources)) if not all_resources: logger.info("No resources received; returning an empty list") return [] - total = response['meta']['pagination']['total'] + total = response["meta"]["pagination"]["total"] logger.info("Total number of resources: %s", total) if total == len(all_resources): # Received all resources in the first batch @@ -233,7 +240,7 @@ def all_pages_numbered_offset_parallel( def all_pages_token_offset( - func: Callable[[Dict[str, Dict]], List[Dict] or List[str]] or partial, + func: Union[Callable[[Dict[str, Dict]], Union[List[Dict], List[str]]], partial], logger: logging.Logger, limit: int = SCROLL_BATCH_SIZE, offset_key_named_after: bool = False, @@ -248,12 +255,14 @@ def all_pages_token_offset( if isinstance(func, partial): logger.info( "Pagination Style 2: Grabbing all pages from the partial %s function (limit: %d)", - func.func.__name__, limit, + func.func.__name__, + limit, ) else: logger.info( "Pagination Style 2: Grabbing all pages from the %s function (limit: %d)", - func.__name__, limit, + func.__name__, + limit, ) complete = False @@ -266,25 +275,27 @@ def all_pages_token_offset( current_page += 1 logger.info( "Fetching page %d: %d to up to %d", - current_page, len(item_ids) + 1, limit * current_page, + current_page, + len(item_ids) + 1, + limit * current_page, ) if offset_key_named_after: - response = func(limit=limit, after=offset)['body'] + response = func(limit=limit, after=offset)["body"] else: - response = func(limit=limit, offset=offset)['body'] + response = func(limit=limit, offset=offset)["body"] logger.debug(response) - resources = response.get('resources', []) + resources = response.get("resources", []) item_ids.extend(resources) if not item_ids: # Nothing was returned, so bail out in case the pagination data does not exist return [] - pagination_data = response['meta']['pagination'] - if pagination_data['total'] > len(item_ids): + pagination_data = response["meta"]["pagination"] + if pagination_data["total"] > len(item_ids): if offset_key_named_after: - offset = pagination_data['after'] + offset = pagination_data["after"] else: - offset = pagination_data['offset'] + offset = pagination_data["offset"] else: complete = True @@ -295,7 +306,7 @@ def all_pages_token_offset( def _generic_parallel_list_execution_worker( - func: Callable[[Dict[str, Dict]], List[Dict] or List[str]] or partial, + func: Union[Callable[[Dict[str, Dict]], Union[List[Dict], List[str]]], partial], logger: logging.Logger, param_name: str, param_value: str, @@ -304,27 +315,29 @@ def _generic_parallel_list_execution_worker( if isinstance(func, partial): logger.info( "%s | Batch worker started with partial function: %s", - thread_name, func.func.__name__, + thread_name, + func.func.__name__, ) else: logger.info( "%s | Batch worker started with function: %s", - thread_name, func.__name__, + thread_name, + func.__name__, ) - response = func(**{param_name: param_value})['body'] + response = func(**{param_name: param_value})["body"] logger.debug("%s | %s", thread_name, response) - resources = response.get('resources', []) + resources = response.get("resources", []) logger.info("%s | Retrieved %d resources", thread_name, len(resources)) return resources def generic_parallel_list_execution( - func: Callable[[Dict[str, Dict]], List[Dict] or List[str]] or partial, - logger: logging.Logger, - param_name: str, - value_list: List[str], + func: Union[Callable[[Dict[str, Dict]], Union[List[Dict], List[str]]], partial], + logger: logging.Logger, + param_name: str, + value_list: List[str], ): """Call a function many times in a thread pool based on a list of kwarg values. diff --git a/caracara/common/policy_wrapper.py b/caracara/common/policy_wrapper.py index e3450b2..6a03b7c 100644 --- a/caracara/common/policy_wrapper.py +++ b/caracara/common/policy_wrapper.py @@ -3,9 +3,11 @@ This file contains wrapper classes that can represent policies in a generic way. It is to be extended by the respective modules (response_policies, prevention_policies, etc.) """ + # Disable TODO warning, to be removed when a wrapper is implemented for `ioa_rule_groups` # pylint: disable=W0511 from __future__ import annotations + from abc import ABC, abstractmethod from datetime import datetime from typing import Any, Dict, List @@ -401,7 +403,7 @@ def flat_dump(self) -> Dict: """ settings: List[Dict] = [] for settings_group in self.settings_groups: - settings.extend(settings_group.flat_dump()['settings']) + settings.extend(settings_group.flat_dump()["settings"]) data = { "description": self.description, @@ -410,6 +412,6 @@ def flat_dump(self) -> Dict: "settings": settings, } if self.policy_id: - data['id'] = self.policy_id + data["id"] = self.policy_id return data diff --git a/caracara/common/sorting.py b/caracara/common/sorting.py index 9e44062..2d9311a 100644 --- a/caracara/common/sorting.py +++ b/caracara/common/sorting.py @@ -1,4 +1,5 @@ """Caracara Policies: Sorting Options.""" + SORT_ASC = "precedence.asc" SORT_DESC = "precedence.desc" diff --git a/caracara/filters/decorators.py b/caracara/filters/decorators.py index a97e056..32bb21b 100644 --- a/caracara/filters/decorators.py +++ b/caracara/filters/decorators.py @@ -1,4 +1,5 @@ """Filter-Related Function Decorators.""" + from functools import wraps from inspect import signature from typing import Callable @@ -10,10 +11,10 @@ def filter_string(func: Callable): """Decorate a function to ensure that a Falcon Filter object is converted to an FQL string.""" # Load the function's signature, and confirm that self and filters are present sig = signature(func) - if 'filters' not in sig.parameters: - raise ValueError(f'The function {func.__name__} does not have a filters parameter') + if "filters" not in sig.parameters: + raise ValueError(f"The function {func.__name__} does not have a filters parameter") - if 'self' not in sig.parameters: + if "self" not in sig.parameters: raise ValueError( f"The function {func.__name__} must be a class function with a self parameter" ) @@ -25,16 +26,16 @@ def wrapper(*args, **kwargs): # Apply any default parameters (e.g., the filters=None where filters is not specified) _args.apply_defaults() # Get references to the self and filters arguments for logging and processing purposes - self = _args.arguments['self'] - filters = _args.arguments['filters'] + self = _args.arguments["self"] + filters = _args.arguments["filters"] - if hasattr(self, 'logger'): + if hasattr(self, "logger"): self.logger.debug(f"Entering filter_string wrapper for function {func.__name__}") if not filters: filter_str = None elif isinstance(filters, FalconFilter): - if hasattr(self, 'logger'): + if hasattr(self, "logger"): self.logger.debug("Rewriting FalconFilter to a string via its get_fql() function") filter_str = filters.get_fql() elif isinstance(filters, str): @@ -45,13 +46,14 @@ def wrapper(*args, **kwargs): "Expected a FalconFilter or a string" ) - if hasattr(self, 'logger'): + if hasattr(self, "logger"): self.logger.debug("Exiting decorator") # Overwrite the filters argument in the bound signature with the string representation. # This ensures that FalconPy is always passed a string, and never a FalconFilter object. - _args.arguments['filters'] = filter_str + _args.arguments["filters"] = filter_str # Return the original function with the modified arguments return func(*_args.args, **_args.kwargs) + return wrapper diff --git a/caracara/modules/__init__.py b/caracara/modules/__init__.py index c4a6313..eaf3ce4 100644 --- a/caracara/modules/__init__.py +++ b/caracara/modules/__init__.py @@ -4,16 +4,17 @@ Proides pipework to link together the various modules within Caracara and expose them to the Client object at setup. """ + __all__ = [ - 'CustomIoaApiModule', - 'FlightControlApiModule', - 'HostsApiModule', - 'PreventionPoliciesApiModule', - 'ResponsePoliciesApiModule', - 'RTRApiModule', - 'SensorDownloadApiModule', - 'SensorUpdatePoliciesApiModule', - 'UsersApiModule', + "CustomIoaApiModule", + "FlightControlApiModule", + "HostsApiModule", + "PreventionPoliciesApiModule", + "ResponsePoliciesApiModule", + "RTRApiModule", + "SensorDownloadApiModule", + "SensorUpdatePoliciesApiModule", + "UsersApiModule", ] from caracara.modules.custom_ioa import CustomIoaApiModule diff --git a/caracara/modules/custom_ioa/__init__.py b/caracara/modules/custom_ioa/__init__.py index dc68376..9915287 100644 --- a/caracara/modules/custom_ioa/__init__.py +++ b/caracara/modules/custom_ioa/__init__.py @@ -1,8 +1,9 @@ """Caracara Custom Indicator of Attack (IOA) module.""" + __all__ = [ - 'CustomIoaApiModule', - 'CustomIoaRule', - 'IoaRuleGroup', + "CustomIoaApiModule", + "CustomIoaRule", + "IoaRuleGroup", ] from caracara.modules.custom_ioa.custom_ioa import CustomIoaApiModule diff --git a/caracara/modules/custom_ioa/custom_ioa.py b/caracara/modules/custom_ioa/custom_ioa.py index c065a3a..6e59240 100644 --- a/caracara/modules/custom_ioa/custom_ioa.py +++ b/caracara/modules/custom_ioa/custom_ioa.py @@ -1,9 +1,10 @@ """Caracara Indicator of Attack (IOA) API module.""" + from functools import partial from time import monotonic -from typing import Dict, List +from typing import Dict, List, Union -from falconpy import OAuth2, CustomIOA +from falconpy import CustomIOA, OAuth2 from caracara.common.batching import batch_get_data from caracara.common.constants import DEFAULT_COMMENT @@ -21,12 +22,14 @@ def instr(func) -> dict: This is an internal function, and therefore developers should not expect this function to remain consistent. """ + def handle_errors(*args, **kwargs): response = func(*args, **kwargs) errors = response["body"].get("errors", []) if len(errors) == 0: return response raise ValueError(errors) + return handle_errors @@ -85,7 +88,9 @@ def create_rule_group( group_create = group.dump_create(comment=comment) response = instr(self.custom_ioa_api.create_rule_group)(body=group_create) new_group = IoaRuleGroup.from_data_dict( - data_dict=response["body"]["resources"][0], rule_type_map=self._get_rule_types_cached()) + data_dict=response["body"]["resources"][0], + rule_type_map=self._get_rule_types_cached(), + ) new_group.rules = group.rules for rule in new_group.rules: rule.group = new_group @@ -124,7 +129,9 @@ def update_rule_group( group_update = group.dump_update(comment=comment) response = instr(self.custom_ioa_api.update_rule_group)(body=group_update) new_group = IoaRuleGroup.from_data_dict( - data_dict=response["body"]["resources"][0], rule_type_map=self._get_rule_types_cached()) + data_dict=response["body"]["resources"][0], + rule_type_map=self._get_rule_types_cached(), + ) # Retain staged rules (as these aren't in the cloud yet, but will be soon) new_group.rules = group.rules @@ -136,7 +143,9 @@ def update_rule_group( return new_group def delete_rule_groups( - self, rule_groups: List[IoaRuleGroup or str], comment: str = DEFAULT_COMMENT + self, + rule_groups: List[Union[IoaRuleGroup, str]], + comment: str = DEFAULT_COMMENT, ): """Delete a list of rule groups in the cloud. @@ -170,22 +179,25 @@ def _create_update_delete_rules(self, group: IoaRuleGroup, comment: str) -> IoaR # Create the new rules new_rules = [] for rule in to_be_created: - resp = instr(self.custom_ioa_api.create_rule)( - body=rule.dump_create(comment=comment)) + resp = instr(self.custom_ioa_api.create_rule)(body=rule.dump_create(comment=comment)) raw_rule = resp["body"]["resources"][0] new_rule = CustomIoaRule.from_data_dict( - raw_rule, rule_type=self._get_rule_types_cached()[raw_rule["ruletype_id"]]) + raw_rule, + rule_type=self._get_rule_types_cached()[raw_rule["ruletype_id"]], + ) new_rule.rulegroup_id = group.id_ new_rules.append(new_rule) # Update the existing rules, if there are any if len(existing_rules) > 0: - response = instr(self.custom_ioa_api.update_rules)(body={ - "comment": comment, - "rule_updates": [rule.dump_update() for rule in existing_rules], - "rulegroup_version": group.version + 1, - "rulegroup_id": group.id_, - }) + response = instr(self.custom_ioa_api.update_rules)( + body={ + "comment": comment, + "rule_updates": [rule.dump_update() for rule in existing_rules], + "rulegroup_version": group.version + 1, + "rulegroup_id": group.id_, + } + ) raw_group = response["body"]["resources"][0] # Create the object representing the updated group @@ -199,13 +211,14 @@ def _create_update_delete_rules(self, group: IoaRuleGroup, comment: str) -> IoaR if len(group.rules_to_delete) > 0: ids_to_delete = [rule.instance_id for rule in group.rules_to_delete] instr(self.custom_ioa_api.delete_rules)( - rule_group_id=group.id_, ids=ids_to_delete, comment=comment) + rule_group_id=group.id_, ids=ids_to_delete, comment=comment + ) # If successful (i.e. no exceptions raised), clear the deletion queue group.rules_to_delete = [] return new_group @filter_string - def describe_rule_groups_raw(self, filters: str or FalconFilter = None) -> Dict[str, dict]: + def describe_rule_groups_raw(self, filters: Union[FalconFilter, str] = None) -> Dict[str, dict]: """Return all IOA Rule Groups as raw dicts returned from the API, optionally filtered. Arguments @@ -232,7 +245,10 @@ def describe_rule_groups_raw(self, filters: str or FalconFilter = None) -> Dict[ return result @filter_string - def describe_rule_groups(self, filters: str or FalconFilter = None) -> Dict[str, IoaRuleGroup]: + def describe_rule_groups( + self, + filters: Union[FalconFilter, str] = None, + ) -> Dict[str, IoaRuleGroup]: """Return all IOA Rule Groups, optionally filtered. Arguments @@ -249,9 +265,10 @@ def describe_rule_groups(self, filters: str or FalconFilter = None) -> Dict[str, groups = {} rule_types = self._get_rule_types_cached() - for (group_id, raw_group) in raw_groups.items(): + for group_id, raw_group in raw_groups.items(): groups[group_id] = IoaRuleGroup.from_data_dict( - data_dict=raw_group, rule_type_map=rule_types) + data_dict=raw_group, rule_type_map=rule_types + ) return groups def get_rule_types_raw(self, rule_type_ids: List[str]) -> Dict[str, dict]: @@ -267,9 +284,7 @@ def get_rule_types_raw(self, rule_type_ids: List[str]) -> Dict[str, dict]: `Dict[str, dict]`: Dictionary mapping ID to its associated rule type as a raw dict returned by the api. """ - rule_types = batch_get_data( - rule_type_ids, instr(self.custom_ioa_api.get_rule_types) - ) + rule_types = batch_get_data(rule_type_ids, instr(self.custom_ioa_api.get_rule_types)) return rule_types @@ -299,7 +314,8 @@ def get_rule_type_ids(self) -> List[str]: `List[str]` list of rule type IDs """ rule_type_ids = all_pages_numbered_offset_parallel( - instr(self.custom_ioa_api.query_rule_types), logger=self.logger) + instr(self.custom_ioa_api.query_rule_types), logger=self.logger + ) return rule_type_ids def describe_rule_types(self) -> Dict[str, RuleType]: diff --git a/caracara/modules/custom_ioa/rule_types.py b/caracara/modules/custom_ioa/rule_types.py index 295a8e1..761a85c 100644 --- a/caracara/modules/custom_ioa/rule_types.py +++ b/caracara/modules/custom_ioa/rule_types.py @@ -1,5 +1,7 @@ """Module defining wrappers around custom IOA rule types.""" + from __future__ import annotations + from dataclasses import dataclass from typing import Dict, List, Optional @@ -79,8 +81,9 @@ def get_field( `Optional[RuleTypeField]`: The rule type field if one can be found, `None` otherwise. """ for field in self.fields: - if (field_name_or_label in [field.name, field.label] - and (field_type is None or field.type == field_type)): + if field_name_or_label in [field.name, field.label] and ( + field_type is None or field.type == field_type + ): return field return None @@ -94,9 +97,7 @@ def dump(self) -> dict: "name": self.name, "long_desc": self.long_desc, "platform": self.platform, - "disposition_map": [ - {"id": k, "label": v} for k, v in self.disposition_map.items() - ], + "disposition_map": [{"id": k, "label": v} for k, v in self.disposition_map.items()], "fields": [field.dump() for field in self.fields], "released": self.released, "channel": self.channel, @@ -146,7 +147,7 @@ def dump(self) -> dict: "label": self.label, "name": self.name, "type": self.type, - "options": [option.dump() for option in self.options] + "options": [option.dump() for option in self.options], } def to_concrete_field(self) -> dict: diff --git a/caracara/modules/custom_ioa/rules.py b/caracara/modules/custom_ioa/rules.py index 17b5b43..5314e48 100644 --- a/caracara/modules/custom_ioa/rules.py +++ b/caracara/modules/custom_ioa/rules.py @@ -1,7 +1,9 @@ """Module that provides wrappers around IOA Rule Groups and Custom IOA Rules.""" + from __future__ import annotations + from datetime import datetime -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Union from caracara.modules.custom_ioa.rule_types import RuleType @@ -89,7 +91,7 @@ def from_data_dict(data_dict: dict, rule_type_map: Dict[str, RuleType]) -> IoaRu rule_group = IoaRuleGroup( name=data_dict["name"], description=data_dict["description"], - platform=data_dict["platform"] + platform=data_dict["platform"], ) rule_group.comment = data_dict["comment"] @@ -127,9 +129,7 @@ def add_rule(self, rule: CustomIoaRule): The rule to add to this rule group """ if rule.group is not None: - raise ValueError( - "This rule has already been added to a group!" - ) + raise ValueError("This rule has already been added to a group!") rule.group = self self.rules.append(rule) @@ -437,7 +437,8 @@ def validation(self): # Check that at least one excludable field is non-default regex_values = [ value["value"] - for field in self.fields.values() if field["type"] == "excludable" + for field in self.fields.values() + if field["type"] == "excludable" for value in field["values"] ] if len(regex_values) > 0 and all(value == ".*" for value in regex_values): @@ -454,7 +455,7 @@ def get_possible_actions(self) -> List[str]: """Return a list of possible actions to set using the `set_action` method.""" return list(self.rule_type.disposition_map.values()) - def set_action(self, action: str or int): + def set_action(self, action: Union[int, str]): """Set the action/disposition for this rule. An action must be set for the API to accept this rule. @@ -471,8 +472,9 @@ def set_action(self, action: str or int): else: raise KeyError("Invalid action/disposition id!") else: - matches = [id_ for (id_, label) - in self.rule_type.disposition_map.items() if label == action] + matches = [ + id_ for (id_, label) in self.rule_type.disposition_map.items() if label == action + ] if len(matches) > 0: self.disposition_id = matches[0] self.action_label = action @@ -500,19 +502,18 @@ def set_excludable_field(self, name_or_label: str, include: str, exclude: Option if field is None: raise ValueError( f"Rule type {repr(self.rule_type.name)} has no fields with name or label " - f"{repr(name_or_label)} and type \"excludable\"" + f'{repr(name_or_label)} and type "excludable"' ) - values = [{ - "label": "include", - "value": include, - }] + values = [ + { + "label": "include", + "value": include, + } + ] if exclude is not None: - values.append({ - "label": "exclude", - "value": exclude - }) + values.append({"label": "exclude", "value": exclude}) self.fields[(field.name, "excludable")] = { "name": field.name, @@ -551,19 +552,20 @@ def set_set_field(self, name_or_label: str, selected_options: List[str]): if field is None: raise ValueError( f"Rule type {repr(self.rule_type.name)} has no fields with name or label " - f"{repr(name_or_label)} and type \"excludable\"" + f'{repr(name_or_label)} and type "excludable"' ) values = [] for value in selected_options: # Find all options that have 'label' or 'value' matching the selected option - matching_options = [option for option in field.options if value in - [option.label, option.value]] + matching_options = [ + option for option in field.options if value in [option.label, option.value] + ] if len(matching_options) == 0: raise KeyError( f"No option matching {repr(value)} in field {repr(field.name)} in rule with " - f"type \"set\"" + f'type "set"' ) option = matching_options[0] diff --git a/caracara/modules/flight_control/__init__.py b/caracara/modules/flight_control/__init__.py index 2184ae8..53ea4b1 100644 --- a/caracara/modules/flight_control/__init__.py +++ b/caracara/modules/flight_control/__init__.py @@ -1,6 +1,7 @@ """Caracara FlightControlApiModule.""" + __all__ = [ - 'FlightControlApiModule', + "FlightControlApiModule", ] from caracara.modules.flight_control.flight_control import FlightControlApiModule diff --git a/caracara/modules/flight_control/flight_control.py b/caracara/modules/flight_control/flight_control.py index ae1c450..0a23ad1 100644 --- a/caracara/modules/flight_control/flight_control.py +++ b/caracara/modules/flight_control/flight_control.py @@ -6,16 +6,10 @@ This module handles interactions with the Parent CID's management APIs. To authenticate to a child/member CID, use the member_cid kwarg when initialising a Caracara Client object. """ -from typing import ( - Dict, - List, - Union, -) - -from falconpy import ( - FlightControl, - OAuth2, -) + +from typing import Dict, List, Union + +from falconpy import FlightControl, OAuth2 from caracara.common.batching import batch_get_data from caracara.common.module import FalconApiModule, ModuleMapper diff --git a/caracara/modules/hosts/__init__.py b/caracara/modules/hosts/__init__.py index 98e2b2e..6b130b8 100644 --- a/caracara/modules/hosts/__init__.py +++ b/caracara/modules/hosts/__init__.py @@ -1,6 +1,7 @@ """Caracara HostsApiModule.""" + __all__ = [ - 'HostsApiModule', + "HostsApiModule", ] from caracara.modules.hosts.hosts import HostsApiModule diff --git a/caracara/modules/hosts/_containment.py b/caracara/modules/hosts/_containment.py index 1352d1f..eb340e1 100644 --- a/caracara/modules/hosts/_containment.py +++ b/caracara/modules/hosts/_containment.py @@ -3,13 +3,12 @@ In order to avoid the main hosts.py file getting too unwieldly, this file contains the implementations of the host containment functions. """ + # Disable the protected access rule because this file is an extension of the class in hosts.py. # pylint: disable=protected-access from __future__ import annotations -from typing import ( - Dict, - TYPE_CHECKING, -) + +from typing import TYPE_CHECKING, Dict, Union from caracara.common.exceptions import MustProvideFilter from caracara.filters import FalconFilter @@ -22,13 +21,13 @@ @filter_string def contain( self: HostsApiModule, - filters: FalconFilter or str = None, + filters: Union[FalconFilter, str] = None, ) -> Dict: """Contain a host or list of hosts within your Falcon tenant. Arguments --------- - filters: FalconFilter or str, optional + filters: Union[FalconFilter, str], optional Filters to apply to the device search. Returns @@ -47,13 +46,13 @@ def contain( @filter_string def release( self: HostsApiModule, - filters: FalconFilter or str = None, + filters: Union[FalconFilter, str] = None, ) -> Dict: """Lift containment for a host or list of hosts within your Falcon tenant. Arguments --------- - filters: FalconFilter or str, optional + filters: Union[FalconFilter, str], optional Filters to apply to the device search. Returns diff --git a/caracara/modules/hosts/_data_history.py b/caracara/modules/hosts/_data_history.py index cb59bc9..ec95be0 100644 --- a/caracara/modules/hosts/_data_history.py +++ b/caracara/modules/hosts/_data_history.py @@ -4,11 +4,10 @@ the implementations of the functions required to retrieve data that Falcon gathers on hosts' network and login histories. """ + from __future__ import annotations -from typing import ( - Dict, - TYPE_CHECKING, -) + +from typing import TYPE_CHECKING, Dict, Union from caracara.common.batching import batch_get_data from caracara.filters import FalconFilter @@ -21,13 +20,13 @@ @filter_string def describe_network_address_history( self: HostsApiModule, - filters: FalconFilter or str = None, + filters: Union[FalconFilter, str] = None, ) -> Dict: """Return a dictionary of network address history for all devices in your Falcon tenant. Arguments --------- - filters: FalconFilter or str, optional + filters: Union[FalconFilter, str], optional Filters to apply to the device search. Returns @@ -43,13 +42,13 @@ def describe_network_address_history( @filter_string def describe_login_history( self: HostsApiModule, - filters: FalconFilter or str = None, + filters: Union[FalconFilter, str] = None, ) -> Dict: """Return a dictionary containing login history for every device in your Falcon tenant. Arguments --------- - filters: FalconFilter or str, optional + filters: Union[FalconFilter, str], optional Filters to apply to the device search. Returns @@ -66,7 +65,7 @@ def describe_login_history( @filter_string def describe_state( self: HostsApiModule, - filters: FalconFilter or str = None, + filters: Union[FalconFilter, str] = None, ) -> Dict[str, Dict]: """Return a dictionary containing online state for devices matching the provided filter. @@ -74,7 +73,7 @@ def describe_state( Arguments --------- - filters: FalconFilter or str, optional + filters: Union[FalconFilter, str], optional Filters to apply to the device search. Returns diff --git a/caracara/modules/hosts/_groups.py b/caracara/modules/hosts/_groups.py index f44cf22..045c80d 100644 --- a/caracara/modules/hosts/_groups.py +++ b/caracara/modules/hosts/_groups.py @@ -3,15 +3,13 @@ In order to avoid the main hosts.py file getting too unwieldly, this file contains the implementations of all functions associated with host groups. """ + # Disable the protected access rule because this file is an extension of the class in hosts.py. # pylint: disable=protected-access from __future__ import annotations + from functools import partial -from typing import ( - Dict, - List, - TYPE_CHECKING, -) +from typing import TYPE_CHECKING, Dict, List, Union from caracara.common.batching import batch_get_data from caracara.common.constants import HOST_GROUP_SCROLL_BATCH_SIZE @@ -47,7 +45,7 @@ def _create_host_group( name=group_name, description=description, group_type=group_type, - assignment_rule=assignment_rule + assignment_rule=assignment_rule, )["body"] if returned["errors"]: raise GenericAPIError(returned["errors"]) @@ -76,21 +74,21 @@ def _perform_group_action( @filter_string def add_to_group( self: HostsApiModule, - filters: FalconFilter or str = None, - group_ids: List[str] or str = None, - device_filters: FalconFilter or str = None, - device_ids: List[str] or str = None, + filters: Union[FalconFilter, str] = None, + group_ids: Union[List[str], str] = None, + device_filters: Union[FalconFilter, str] = None, + device_ids: Union[List[str], str] = None, ) -> Dict: """Add a host or list of hosts to a host group within your Falcon tenant. Arguments --------- - filters: FalconFilter or str, optional + filters: Union[FalconFilter, str], optional Group filter to apply to the host group search. Not required if group_ids are provided. group_ids: List[str] or str, optional List of host group IDs to update. Comma delimited strings are converted. Not required if a group filter is provided. Takes precedence over provided filters. - device_filters: FalconFilter or str, optional + device_filters: Union[FalconFilter, str], optional Filters to apply to the device search. Not required if device_ids are provided. device_ids: List[str] or str, optional List of device IDs to add to the host group. Comma delimited strings are converted. @@ -140,24 +138,19 @@ def create_group( ------- dict: A dictionary containing details for the host group creation result. """ - return self._create_host_group( - group_name, - description, - group_type, - assignment_rule - ) + return self._create_host_group(group_name, description, group_type, assignment_rule) def delete_group( self: HostsApiModule, group_ids: List[str] = None, - filters: FalconFilter or str = None, + filters: Union[FalconFilter, str] = None, ) -> Dict: """Delete a host group from within your Falcon tenant. Arguments --------- - filters: FalconFilter or str, optional + filters: Union[FalconFilter, str], optional Group filter to apply to the host group search. Not required if group_ids are provided. group_ids: List[str] or str, optional List of host group IDs to update. Comma delimited strings are converted. @@ -175,7 +168,7 @@ def delete_group( returned = self.host_group_api.delete_host_groups( ids=group_ids if group_ids else self.get_group_ids(filters) - )["body"] + )["body"] if returned["errors"]: raise GenericAPIError(returned["errors"]) @@ -185,13 +178,13 @@ def delete_group( @filter_string def describe_group_member_ids( self: HostsApiModule, - filters: FalconFilter or str = None, + filters: Union[FalconFilter, str] = None, ) -> Dict[str, List[str]]: """Return a dictionary with member IDs for all host groups matching the provided filter. Arguments --------- - filters: FalconFilter or str, optional + filters: Union[FalconFilter, str], optional Filters to apply to the host group search. Returns @@ -210,13 +203,13 @@ def describe_group_member_ids( @filter_string def describe_group_members( self: HostsApiModule, - filters: FalconFilter or str = None, + filters: Union[FalconFilter, str] = None, ) -> Dict: """Return a dictionary with member details for all host groups matching the provided filter. Arguments --------- - filters: FalconFilter or str, optional + filters: Union[FalconFilter, str], optional Filters to apply to the host group search. Returns @@ -230,13 +223,13 @@ def describe_group_members( @filter_string def describe_groups( self: HostsApiModule, - filters: FalconFilter or str = None, + filters: Union[FalconFilter, str] = None, ) -> Dict[str, Dict]: """Return a dictionary containing detail for every host group matching the provided filter. Arguments --------- - filters: FalconFilter or str, optional + filters: Union[FalconFilter, str], optional Filters to apply to the host group search. Returns @@ -253,13 +246,13 @@ def describe_groups( @filter_string def get_group_ids( self: HostsApiModule, - filters: FalconFilter or str = None, + filters: Union[FalconFilter, str] = None, ) -> List[str]: """Return a list of IDs (string) for every host group within your Falcon tenant. Arguments --------- - filters: FalconFilter or str, optional + filters: Union[FalconFilter, str], optional Filters to apply to the host group search. Returns @@ -269,9 +262,7 @@ def get_group_ids( self.logger.info("Searching for host group IDs using the filter string %s", filters) func = partial(self.host_group_api.query_host_groups, filter=filters) id_list: List[str] = all_pages_numbered_offset_parallel( - func=func, - logger=self.logger, - limit=HOST_GROUP_SCROLL_BATCH_SIZE + func=func, logger=self.logger, limit=HOST_GROUP_SCROLL_BATCH_SIZE ) if not id_list: raise HostGroupNotFound @@ -308,13 +299,13 @@ def get_group_member_ids( @filter_string def get_group_members( self: HostsApiModule, - filters: FalconFilter or str = None, + filters: Union[FalconFilter, str] = None, ) -> Dict[str, Dict]: """Return a dictionary containing every host group and their members. Arguments --------- - filters: FalconFilter or str, optional + filters: Union[FalconFilter, str], optional Filters to apply to the host group member search. Returns @@ -327,14 +318,14 @@ def get_group_members( all_group_data: Dict[str, Dict] = {} for group_id, group_data in groups.items(): all_group_data[group_id] = group_data - all_group_data[group_id]['devices'] = [] + all_group_data[group_id]["devices"] = [] for device in device_data: - if 'groups' not in device: + if "groups" not in device: continue - for group_identifier in device['groups']: + for group_identifier in device["groups"]: if group_identifier == group_id: - all_group_data[group_identifier]['devices'].append(device) + all_group_data[group_identifier]["devices"].append(device) return all_group_data @@ -368,28 +359,30 @@ def group( description, "static", assignment_rule, - )[0]["id"] + )[ + 0 + ]["id"] return self.add_to_group(group_ids=new_group, device_ids=device_ids)["resources"] @filter_string def remove_from_group( self: HostsApiModule, - filters: FalconFilter or str = None, - group_ids: List[str] or str = None, - device_filters: FalconFilter or str = None, - device_ids: List[str] or str = None, + filters: Union[FalconFilter, str] = None, + group_ids: Union[List[str], str] = None, + device_filters: Union[FalconFilter, str] = None, + device_ids: Union[List[str], str] = None, ) -> Dict: """Remove a host or list of hosts to a host group within your Falcon tenant. Arguments --------- - filters: FalconFilter or str, optional + filters: Union[FalconFilter, str], optional Group filter to apply to the host group search. Not required if group_ids are provided. group_ids: List[str] or str, optional List of host group IDs to update. Comma delimited strings are converted. Not required if a group filter is provided. Takes precedence over provided filters. - device_filters: FalconFilter or str, optional + device_filters: Union[FalconFilter, str], optional Filters to apply to the device search. Not required if device_ids are provided. device_ids: List[str] or str, optional List of device IDs to add to the host group. Comma delimited strings are converted. @@ -420,7 +413,7 @@ def remove_from_group( def ungroup( self: HostsApiModule, - group_ids: List[str] or str = None, + group_ids: Union[List[str], str] = None, remove_groups: bool = False, ) -> Dict[str, Dict]: """Remove all members from host groups and then remove the group (if specified). @@ -451,7 +444,7 @@ def ungroup( self.logger.info( "Removed %d members from group %s", len(returned[group_id]["result"]), - group_id + group_id, ) if remove_groups: self.logger.info("Remove group %s", group_id) @@ -468,7 +461,7 @@ def update_group( group_name: str = None, group_description: str = None, assignment_rule: str = None, - ) -> Dict: +) -> Dict: """Update the name, assignment rule or description for a Host Group wihtin your tenant. Arguments @@ -491,5 +484,5 @@ def update_group( id=group_id, assignment_rule=assignment_rule, description=group_description, - name=group_name + name=group_name, )["body"]["resources"] diff --git a/caracara/modules/hosts/_hiding.py b/caracara/modules/hosts/_hiding.py index 66daa3a..f47c0b2 100644 --- a/caracara/modules/hosts/_hiding.py +++ b/caracara/modules/hosts/_hiding.py @@ -4,22 +4,17 @@ the implementations of the functions required to hide and unhide hosts from the Falcon UI. """ + # Disable the protected access rule because this file is an extension of the class in hosts.py. # pylint: disable=protected-access from __future__ import annotations + from functools import partial -from typing import ( - Dict, - List, - TYPE_CHECKING, -) +from typing import TYPE_CHECKING, Dict, List, Union from caracara.common.batching import batch_get_data from caracara.common.constants import SCROLL_BATCH_SIZE -from caracara.common.exceptions import ( - DeviceNotFound, - MustProvideFilter, -) +from caracara.common.exceptions import DeviceNotFound, MustProvideFilter from caracara.common.pagination import all_pages_numbered_offset_parallel from caracara.filters import FalconFilter from caracara.filters.decorators import filter_string @@ -31,13 +26,13 @@ @filter_string def describe_hidden_devices( self: HostsApiModule, - filters: FalconFilter or str = None, + filters: Union[FalconFilter, str] = None, ) -> Dict: """Return a dictionary containing details for every hidden device in your Falcon tenant. Arguments --------- - filters: FalconFilter or str, optional + filters: Union[FalconFilter, str], optional Filters to apply to the hidden device search. Returns @@ -54,13 +49,13 @@ def describe_hidden_devices( @filter_string def get_hidden_ids( self: HostsApiModule, - filters: FalconFilter or str = None, + filters: Union[FalconFilter, str] = None, ) -> List[str]: """Return a list of IDs (string) for every hidden device in your Falcon tenant. Arguments --------- - filters: FalconFilter or str, optional + filters: Union[FalconFilter, str], optional Filters to apply to the device search. Returns @@ -83,13 +78,13 @@ def get_hidden_ids( @filter_string def hide( self: HostsApiModule, - filters: FalconFilter or str = None, + filters: Union[FalconFilter, str] = None, ) -> Dict: """Hide a host or list of hosts within your Falcon tenant. Arguments --------- - filters: FalconFilter or str, optional + filters: Union[FalconFilter, str], optional Filters to apply to the device search. Returns @@ -108,13 +103,13 @@ def hide( @filter_string def unhide( self: HostsApiModule, - filters: FalconFilter or str = None, + filters: Union[FalconFilter, str] = None, ) -> Dict: """Unhide a host or list of hosts within your Falcon tenant. Arguments --------- - filters: FalconFilter or str, optional + filters: Union[FalconFilter, str], optional Filters to apply to the device search. Returns @@ -124,7 +119,6 @@ def unhide( if not filters: raise MustProvideFilter - return self._perform_action( - action_name="unhide_host", - device_ids=self.get_hidden_ids(filters) - )["resources"] + return self._perform_action(action_name="unhide_host", device_ids=self.get_hidden_ids(filters))[ + "resources" + ] diff --git a/caracara/modules/hosts/_online_state.py b/caracara/modules/hosts/_online_state.py index d1f52a6..ed6e822 100644 --- a/caracara/modules/hosts/_online_state.py +++ b/caracara/modules/hosts/_online_state.py @@ -3,16 +3,15 @@ In order to avoid the main hosts.py file getting too unwieldly, this file contains the implementations of the host online state query functions. """ + # Disable the protected access rule because this file is an extension of the class in hosts.py. # pylint: disable=protected-access from __future__ import annotations -from typing import ( - List, - TYPE_CHECKING, -) + +from typing import TYPE_CHECKING, List, Union from caracara.common.batching import batch_get_data -from caracara.common.constants import OnlineState, ONLINE_STATE_DATA_BATCH_SIZE +from caracara.common.constants import ONLINE_STATE_DATA_BATCH_SIZE, OnlineState from caracara.common.exceptions import InvalidOnlineState if TYPE_CHECKING: @@ -20,8 +19,8 @@ def validate_online_state( - self: HostsApiModule, - online_state: OnlineState or str, + self: HostsApiModule, + online_state: Union[OnlineState, str], ): """Raise an exception if the online_state_string is not a valid online state. @@ -36,8 +35,8 @@ def validate_online_state( def get_online_state( - self: HostsApiModule, - device_ids: List[str], + self: HostsApiModule, + device_ids: List[str], ) -> List[str]: """Return a dictionary containing online state details for every device specified by ID. @@ -67,7 +66,7 @@ def get_online_state( def filter_device_ids_by_online_state( self: HostsApiModule, device_ids: List[str], - online_state: OnlineState or str, + online_state: Union[OnlineState, str], ) -> List[str]: """Filter a list of device IDs by an online state. @@ -86,7 +85,9 @@ def filter_device_ids_by_online_state( device_state_data = self.get_online_state(device_ids) - return list(filter( - lambda device_id: device_state_data[device_id]["state"] == online_state, - device_state_data, - )) + return list( + filter( + lambda device_id: device_state_data[device_id]["state"] == online_state, + device_state_data, + ) + ) diff --git a/caracara/modules/hosts/_tagging.py b/caracara/modules/hosts/_tagging.py index 0dc5218..18e8753 100644 --- a/caracara/modules/hosts/_tagging.py +++ b/caracara/modules/hosts/_tagging.py @@ -3,18 +3,14 @@ In order to avoid the main hosts.py file getting too unwieldly, this file contains the implementations of functions that can tag and untag hosts within the Falcon UI. """ + # Disable the protected access rule because this file is an extension of the class in hosts.py. # pylint: disable=protected-access from __future__ import annotations -from typing import ( - Dict, - List, - TYPE_CHECKING, -) - -from caracara.common.exceptions import ( - MustProvideFilter, -) + +from typing import TYPE_CHECKING, Dict, List, Union + +from caracara.common.exceptions import MustProvideFilter from caracara.filters import FalconFilter from caracara.filters.decorators import filter_string @@ -22,7 +18,7 @@ from caracara.modules.hosts import HostsApiModule -def _create_tag_list(potential_list: List[str] or str) -> List[str]: +def _create_tag_list(potential_list: Union[List[str], str]) -> List[str]: """Create a properly formatted list from a list, a string or a comma-delimited string.""" tag_list = potential_list if not isinstance(potential_list, list): @@ -32,23 +28,19 @@ def _create_tag_list(potential_list: List[str] or str) -> List[str]: def _update_device_tags( - self: HostsApiModule, - action_name: str, - device_ids: List[str], - tag_list: List[str] + self: HostsApiModule, action_name: str, device_ids: List[str], tag_list: List[str] ) -> Dict: """Tag or untag a device within the tenant.""" return self.hosts_api.update_device_tags( - action_name=action_name, - ids=device_ids, - tags=tag_list + action_name=action_name, ids=device_ids, tags=tag_list )["body"] @filter_string def tag( self: HostsApiModule, - tags: List[str] or str, filters: FalconFilter or str = None, + tags: Union[List[str], str], + filters: Union[FalconFilter, str] = None, ) -> Dict: """Tag a host or list of hosts within your Falcon tenant. @@ -56,7 +48,7 @@ def tag( --------- tags: List of strings Tags to be applied to the discovered devices. - filters: FalconFilter or str, optional + filters: Union[FalconFilter, str], optional Filters to apply to the device search. Returns @@ -69,14 +61,15 @@ def tag( return self._update_device_tags( action_name="add", tag_list=self._create_tag_list(tags), - device_ids=self.get_device_ids(filters) + device_ids=self.get_device_ids(filters), )["resources"] @filter_string def untag( self: HostsApiModule, - tags: List[str] or str, filters: FalconFilter or str = None, + tags: Union[List[str], str], + filters: Union[FalconFilter, str] = None, ) -> Dict: """Untag a host or list of hosts within your Falcon tenant. @@ -84,7 +77,7 @@ def untag( --------- tags: List of strings Tags to be applied to the discovered devices. - filters: FalconFilter or str, optional + filters: Union[FalconFilter, str], optional Filters to apply to the device search. Returns @@ -97,5 +90,5 @@ def untag( return self._update_device_tags( action_name="remove", tag_list=self._create_tag_list(tags), - device_ids=self.get_device_ids(filters) + device_ids=self.get_device_ids(filters), )["resources"] diff --git a/caracara/modules/hosts/hosts.py b/caracara/modules/hosts/hosts.py index 7fa95f1..5eab8fd 100644 --- a/caracara/modules/hosts/hosts.py +++ b/caracara/modules/hosts/hosts.py @@ -10,29 +10,17 @@ This module handles interactions with the CrowdStrike Falcon Hosts and Host Group APIs. """ + from functools import partial -from typing import ( - Dict, - List, - Union, - Optional, -) - -from falconpy import ( - Hosts, - HostGroup, - OAuth2, -) +from typing import Dict, List, Optional, Union + +from falconpy import HostGroup, Hosts, OAuth2 from caracara.common.batching import batch_get_data from caracara.common.constants import OnlineState -from caracara.common.exceptions import ( - GenericAPIError, -) +from caracara.common.exceptions import GenericAPIError from caracara.common.module import FalconApiModule, ModuleMapper -from caracara.common.pagination import ( - all_pages_token_offset, -) +from caracara.common.pagination import all_pages_token_offset from caracara.filters import FalconFilter from caracara.filters.decorators import filter_string @@ -57,10 +45,7 @@ def __init__(self, api_authentication: OAuth2, mapper: ModuleMapper): self.host_group_api = HostGroup(auth_object=self.api_authentication) # Import containment functions - from caracara.modules.hosts._containment import ( - contain, - release, - ) + from caracara.modules.hosts._containment import contain, release # Import data history functions from caracara.modules.hosts._data_history import ( @@ -98,8 +83,8 @@ def __init__(self, api_authentication: OAuth2, mapper: ModuleMapper): # Import functions to filter by device online state from caracara.modules.hosts._online_state import ( - get_online_state, filter_device_ids_by_online_state, + get_online_state, validate_online_state, ) @@ -115,9 +100,9 @@ def __init__(self, api_authentication: OAuth2, mapper: ModuleMapper): _create_tag_list = staticmethod(_create_tag_list) def _perform_action( - self, - action_name: str, - device_ids: List[str], + self, + action_name: str, + device_ids: List[str], ) -> Dict: """Perform the specified action against the list of targets.""" returned = self.hosts_api.perform_action(ids=device_ids, action_name=action_name)["body"] @@ -130,7 +115,7 @@ def _perform_action( @filter_string def describe_devices( self, - filters: FalconFilter or str = None, + filters: Union[FalconFilter, str] = None, online_state: Optional[Union[OnlineState, str]] = None, enrich_with_online_state: Optional[bool] = False, ) -> Dict[str, Dict]: @@ -138,7 +123,7 @@ def describe_devices( Arguments --------- - filters: FalconFilter or str, optional + filters: Union[FalconFilter, str], optional Filters to apply to the device search. online_state: OnlineState or str, optional Device online state to filter devices on. Options are "online", "offline", "unknown" @@ -157,10 +142,12 @@ def describe_devices( # Filter by online state, if applicable. if online_state is not None: self.validate_online_state(online_state) - device_ids = list(filter( - lambda key: device_state_data[key]["state"] == online_state, - device_state_data, - )) + device_ids = list( + filter( + lambda key: device_state_data[key]["state"] == online_state, + device_state_data, + ) + ) device_data = self.get_device_data(device_ids) @@ -172,8 +159,8 @@ def describe_devices( return device_data def get_device_data( - self, - device_ids: List[str], + self, + device_ids: List[str], ) -> Dict[str, Dict]: """Return a dictionary containing details for every device specified by ID. @@ -198,14 +185,14 @@ def get_device_data( @filter_string def get_device_ids( self, - filters: FalconFilter or str = None, + filters: Union[FalconFilter, str] = None, online_state: Optional[Union[OnlineState, str]] = None, ) -> List[str]: """Return a list of IDs (string) for every device in your Falcon tenant. Arguments --------- - filters: FalconFilter or str, optional + filters: Union[FalconFilter, str], optional Filters to apply to the device search. online_state: OnlineState or str, optional Device online state to filter devices on. Options are "online", "offline", "unknown" diff --git a/caracara/modules/prevention_policies/__init__.py b/caracara/modules/prevention_policies/__init__.py index 96d75ba..389fcb6 100644 --- a/caracara/modules/prevention_policies/__init__.py +++ b/caracara/modules/prevention_policies/__init__.py @@ -1,6 +1,9 @@ """Caracara PreventionPoliciesApiModule.""" + __all__ = [ - 'PreventionPoliciesApiModule', + "PreventionPoliciesApiModule", ] -from caracara.modules.prevention_policies.prevention_policies import PreventionPoliciesApiModule +from caracara.modules.prevention_policies.prevention_policies import ( + PreventionPoliciesApiModule, +) diff --git a/caracara/modules/prevention_policies/prevention_policies.py b/caracara/modules/prevention_policies/prevention_policies.py index 1a61539..cccdfbe 100644 --- a/caracara/modules/prevention_policies/prevention_policies.py +++ b/caracara/modules/prevention_policies/prevention_policies.py @@ -1,11 +1,9 @@ """Falcon Prevention Policies API.""" + from functools import partial -from typing import List, Dict +from typing import Dict, List, Union -from falconpy import ( - OAuth2, - PreventionPolicies, -) +from falconpy import OAuth2, PreventionPolicies from caracara.common.decorators import platform_name_check from caracara.common.module import FalconApiModule, ModuleMapper @@ -35,7 +33,7 @@ def __init__(self, api_authentication: OAuth2, mapper: ModuleMapper): @filter_string def describe_policies_raw( - self, filters: str or FalconFilter = None, sort: str = SORT_ASC + self, filters: Union[FalconFilter, str] = None, sort: str = SORT_ASC ) -> List[Dict]: """Return a list of dictionaries containing all prevention policies in the Falcon tenant.""" if sort not in SORTING_OPTIONS: @@ -53,7 +51,7 @@ def describe_policies_raw( @filter_string def describe_policies( - self, filters: str or FalconFilter = None, sort: str = SORT_ASC + self, filters: Union[FalconFilter, str] = None, sort: str = SORT_ASC ) -> List[Policy]: """Return a list of all prevention policies packaged as custom Python Policy objects.""" raw_policies_dict = self.describe_policies_raw(filters=filters, sort=sort) @@ -76,10 +74,10 @@ def new_policy(self, platform_name: str) -> Policy: def push_policy(self, policy: Policy) -> Policy: """Push a policy to the CrowdStrike Cloud, and return the resultant policy.""" self.logger.info("Creating the prevention policy named %s", policy.name) - response = self.prevention_policies_api.create_policies(body={ - "resources": [ - policy.flat_dump() - ], - })['body'] - new_policy = Policy(data_dict=response['resources'][0], style="prevention") + response = self.prevention_policies_api.create_policies( + body={ + "resources": [policy.flat_dump()], + } + )["body"] + new_policy = Policy(data_dict=response["resources"][0], style="prevention") return new_policy diff --git a/caracara/modules/prevention_policies/template.py b/caracara/modules/prevention_policies/template.py index f4929d2..6593129 100644 --- a/caracara/modules/prevention_policies/template.py +++ b/caracara/modules/prevention_policies/template.py @@ -3,17 +3,16 @@ This file contains platform-specific templates representing standard, blank Prevention policies. """ + from caracara.common.policy_wrapper import ( + SETTINGS_TYPE_MAP, ChangeablePolicySetting, Policy, PolicySettingGroup, - SETTINGS_TYPE_MAP, ) - COMMAND_TEMPLATES = { - "AdditionalUserModeData": - { + "AdditionalUserModeData": { "description": ( "Allows the sensor to get more data from a user-mode component it loads into all " "eligible processes, which augments online machine learning and turns on additional " @@ -99,7 +98,7 @@ "CustomBlacklisting": { "description": ( "Block processes matching hashes that you add to IOC Management with the action " - "set to \"Block\" or \"Block, hide detection\"." + 'set to "Block" or "Block, hide detection".' ), "name": "Custom Blocking", "setting_type": "toggle", @@ -583,7 +582,7 @@ "A command line process associated with Windows logon bypass was prevented " "from executing." ), - "name": "Windows Logon Bypass (\"Sticky Keys\")", + "name": 'Windows Logon Bypass ("Sticky Keys")', "setting_type": "toggle", "value": { "enabled": False, @@ -756,12 +755,12 @@ def generate_prevention_template(platform_name: str) -> Policy: for setting_id in group_settings: setting_data = COMMAND_TEMPLATES[setting_id] - setting_desc = setting_data['description'] + setting_desc = setting_data["description"] if isinstance(setting_desc, dict): setting_desc = setting_desc[platform_name] - setting_name = setting_data['name'] - setting_type = setting_data['setting_type'] - setting_value = setting_data['value'] + setting_name = setting_data["name"] + setting_type = setting_data["setting_type"] + setting_value = setting_data["value"] setting_template: ChangeablePolicySetting = SETTINGS_TYPE_MAP[setting_type] loadable_setting_data = { "description": setting_desc, diff --git a/caracara/modules/response_policies/__init__.py b/caracara/modules/response_policies/__init__.py index c989df4..a6d4063 100644 --- a/caracara/modules/response_policies/__init__.py +++ b/caracara/modules/response_policies/__init__.py @@ -1,6 +1,9 @@ """Caracara PoliciesApiModule.""" + __all__ = [ - 'ResponsePoliciesApiModule', + "ResponsePoliciesApiModule", ] -from caracara.modules.response_policies.response_policies import ResponsePoliciesApiModule +from caracara.modules.response_policies.response_policies import ( + ResponsePoliciesApiModule, +) diff --git a/caracara/modules/response_policies/response_policies.py b/caracara/modules/response_policies/response_policies.py index 876c2e2..f3a1439 100644 --- a/caracara/modules/response_policies/response_policies.py +++ b/caracara/modules/response_policies/response_policies.py @@ -1,11 +1,9 @@ """Falcon Response Policies API.""" + from functools import partial -from typing import Dict, List +from typing import Dict, List, Union -from falconpy import ( - OAuth2, - ResponsePolicies, -) +from falconpy import OAuth2, ResponsePolicies from caracara.common.decorators import platform_name_check from caracara.common.module import FalconApiModule, ModuleMapper @@ -35,7 +33,7 @@ def __init__(self, api_authentication: OAuth2, mapper: ModuleMapper): @filter_string def describe_policies_raw( - self, filters: str or FalconFilter = None, sort: str = SORT_ASC + self, filters: Union[FalconFilter, str] = None, sort: str = SORT_ASC ) -> List[Dict]: """Return a list of dictionaries containing all response policies in the Falcon tenant.""" if sort not in SORTING_OPTIONS: @@ -53,7 +51,7 @@ def describe_policies_raw( @filter_string def describe_policies( - self, filters: str or FalconFilter = None, sort: str = SORT_ASC + self, filters: Union[FalconFilter, str] = None, sort: str = SORT_ASC ) -> List[Policy]: """Return a list of all response policies packaged as custom Python Policy objects.""" raw_policies_dict = self.describe_policies_raw(filters=filters, sort=sort) @@ -78,15 +76,15 @@ def new_policy(self, platform_name: str) -> Policy: def push_policy(self, policy: Policy) -> Policy: """Push a policy to the CrowdStrike Cloud, and return the resultant policy.""" self.logger.info("Creating the response policy named %s", policy.name) - response = self.response_policies_api.create_policies(body={ - "resources": [ - policy.flat_dump() - ], - })['body'] - new_policy = Policy(data_dict=response['resources'][0], style="response") + response = self.response_policies_api.create_policies( + body={ + "resources": [policy.flat_dump()], + } + )["body"] + new_policy = Policy(data_dict=response["resources"][0], style="response") return new_policy - def add_policy_to_group(self, policy: Policy or str, group: str) -> Policy: + def add_policy_to_group(self, policy: Union[Policy, str], group: str) -> Policy: """ Add a policy to a group. @@ -115,7 +113,7 @@ def add_policy_to_group(self, policy: Policy or str, group: str) -> Policy: updated_policy_dict = self.response_policies_api.query_combined_policies( filter=f"id: '{policy_id}'", - )['body']['resources'][0] + )["body"]["resources"][0] new_policy = Policy(data_dict=updated_policy_dict, style="response") return new_policy @@ -134,10 +132,10 @@ def modify_policy(self, policy: Policy) -> Policy: ) self.logger.info("Updating the response policy named %s", policy.name) - response = self.response_policies_api.update_policies(body={ - "resources": [ - policy.flat_dump() - ], - })['body']['resources'][0] + response = self.response_policies_api.update_policies( + body={ + "resources": [policy.flat_dump()], + } + )["body"]["resources"][0] new_policy = Policy(data_dict=response, style="response") return new_policy diff --git a/caracara/modules/response_policies/template.py b/caracara/modules/response_policies/template.py index fdef66f..85c3f0d 100644 --- a/caracara/modules/response_policies/template.py +++ b/caracara/modules/response_policies/template.py @@ -3,14 +3,14 @@ This file contains platform-specific templates representing standard, blank Response policies. """ + from caracara.common.policy_wrapper import ( + SETTINGS_TYPE_MAP, ChangeablePolicySetting, Policy, PolicySettingGroup, - SETTINGS_TYPE_MAP, ) - COMMAND_TEMPLATES = { "RealTimeFunctionality": { "description": ( @@ -95,9 +95,7 @@ "Enable/Disable": [ "RealTimeFunctionality", ], - "Custom scripts": [ - "CustomScripts" - ], + "Custom scripts": ["CustomScripts"], "High risk commands": [ "GetCommand", "PutCommand", @@ -108,23 +106,19 @@ "Enable/Disable": [ "RealTimeFunctionality", ], - "Custom scripts": [ - "CustomScripts" - ], + "Custom scripts": ["CustomScripts"], "High risk commands": [ "GetCommand", "PutCommand", "ExecCommand", "PutAndRunCommand", - ] + ], }, "Windows": { "Enable/Disable": [ "RealTimeFunctionality", ], - "Custom scripts": [ - "CustomScripts" - ], + "Custom scripts": ["CustomScripts"], "High risk commands": [ "GetCommand", "PutCommand", @@ -132,7 +126,7 @@ "XMemDumpCommand", "ExecCommand", "PutAndRunCommand", - ] + ], }, } @@ -158,7 +152,7 @@ def generate_response_template(platform_name: str) -> Policy: for setting_id in group_settings: setting_data = COMMAND_TEMPLATES[setting_id] - setting_type = setting_data['setting_type'] + setting_type = setting_data["setting_type"] setting_template: ChangeablePolicySetting = SETTINGS_TYPE_MAP[setting_type] loadable_setting_data = { **COMMAND_TEMPLATES[setting_id], diff --git a/caracara/modules/rtr/__init__.py b/caracara/modules/rtr/__init__.py index f1e15e2..179b125 100644 --- a/caracara/modules/rtr/__init__.py +++ b/caracara/modules/rtr/__init__.py @@ -1,10 +1,11 @@ """Caracara Real Time Response (RTR) module.""" + __all__ = [ - 'BatchGetCmdRequest', - 'GetFile', - 'InnerRTRBatchSession', - 'RTRApiModule', - 'RTRBatchSession', + "BatchGetCmdRequest", + "GetFile", + "InnerRTRBatchSession", + "RTRApiModule", + "RTRBatchSession", ] from caracara.modules.rtr.batch_session import ( diff --git a/caracara/modules/rtr/batch_session.py b/caracara/modules/rtr/batch_session.py index f68fc95..c7a1923 100644 --- a/caracara/modules/rtr/batch_session.py +++ b/caracara/modules/rtr/batch_session.py @@ -1,17 +1,14 @@ """Real Time Response (RTR) batch session abstraction class.""" + import concurrent.futures -from dataclasses import dataclass import logging - +from dataclasses import dataclass from datetime import datetime, timedelta from functools import partial, wraps from threading import current_thread from typing import Dict, List -from falconpy import ( - RealTimeResponse, - RealTimeResponseAdmin, -) +from falconpy import RealTimeResponse, RealTimeResponseAdmin from caracara.common.batching import batch_data_pull_threads from caracara.modules.rtr.constants import ( @@ -33,6 +30,7 @@ def wrapper(self, *args, **kwargs): self.auto_refresh_sessions(self.default_timeout) return func(self, *args, **kwargs) + return wrapper @@ -51,13 +49,7 @@ class InnerRTRBatchSession: # pylint: disable=too-few-public-methods expiry: datetime = None logger: logging.Logger = None - def __init__( - self, - batch_id: str, - devices: Dict, - expiry: datetime, - logger: logging.Logger - ): + def __init__(self, batch_id: str, devices: Dict, expiry: datetime, logger: logging.Logger): """Configure an inner batch of RTR sessions.""" self.batch_id = batch_id self.devices = devices @@ -93,14 +85,16 @@ def generic_rtr_worker( logger = logger.getChild(__name__) logger.info( "%s | Executing %s function against RTR batch: %s (args: %s; kwargs: %s)", - thread_name, func.func.__name__, session.batch_id, func.args, func.keywords, + thread_name, + func.func.__name__, + session.batch_id, + func.args, + func.keywords, ) if device_ids: - device_ids_in_batch = list( - filter(lambda x: x in session.devices.keys(), device_ids) - ) - func.keywords['optional_hosts'] = device_ids_in_batch - response = func(batch_id=session.batch_id)['body'] + device_ids_in_batch = list(filter(lambda x: x in session.devices.keys(), device_ids)) + func.keywords["optional_hosts"] = device_ids_in_batch + response = func(batch_id=session.batch_id)["body"] logger.debug("%s | %s", thread_name, response) return response @@ -145,21 +139,22 @@ def connect( batches = [] for i in range(0, len(device_ids), MAX_BATCH_SESSION_HOSTS): - batches.append(device_ids[i:i+MAX_BATCH_SESSION_HOSTS]) + batches.append(device_ids[i : i + MAX_BATCH_SESSION_HOSTS]) self.logger.info("Divided up devices into %d batches", len(batches)) def worker(batch_device_ids: List[str], batch_func: partial): thread_name = current_thread().name self.logger.info( "%s | Batch worker started with a list of %d devices", - thread_name, len(batch_device_ids), + thread_name, + len(batch_device_ids), ) - response = batch_func(host_ids=batch_device_ids)['body'] - resources = response['resources'] + response = batch_func(host_ids=batch_device_ids)["body"] + resources = response["resources"] self.logger.info("%s | Connected to %s systems", thread_name, len(resources)) self.logger.debug("%s | %s", thread_name, response) batch_data = InnerRTRBatchSession( - batch_id=response['batch_id'], + batch_id=response["batch_id"], devices=resources, expiry=datetime.now() + timedelta(seconds=SESSION_EXPIRY), logger=self.logger, @@ -170,7 +165,7 @@ def worker(batch_device_ids: List[str], batch_func: partial): self.api.batch_init_sessions, queue_offline=queueing, timeout=timeout, - timeout_duration=f'{timeout}s', + timeout_duration=f"{timeout}s", ) with concurrent.futures.ThreadPoolExecutor( @@ -214,7 +209,7 @@ def get( self.api.batch_get_command, file_path=file_path, timeout=timeout, - timeout_duration=f'{timeout}s', + timeout_duration=f"{timeout}s", ) partial_worker = partial(generic_rtr_worker, logger=self.logger, device_ids=device_ids) with concurrent.futures.ThreadPoolExecutor( @@ -230,8 +225,8 @@ def get( for complete in completed: self.logger.info("Executed commands on a batch of %d hosts", len(complete)) batch_get_cmd_req = BatchGetCmdRequest( - batch_get_cmd_req_id=complete['batch_get_cmd_req_id'], - devices=complete['combined']['resources'], + batch_get_cmd_req_id=complete["batch_get_cmd_req_id"], + devices=complete["combined"]["resources"], ) batch_get_cmd_reqs.append(batch_get_cmd_req) @@ -240,7 +235,7 @@ def get( def get_status( self, batch_get_cmd_reqs: List[BatchGetCmdRequest], - timeout: int = default_timeout + timeout: int = default_timeout, ) -> List[GetFile]: """ Get a list of successful file uploads based on a list of batch get command requests. @@ -255,20 +250,21 @@ def worker(batch_get_cmd_req: BatchGetCmdRequest, func: partial) -> List[GetFile logger = self.logger.getChild(__name__) logger.info( "%s | Getting the status of batch get request %s", - thread_name, batch_get_cmd_req.batch_get_cmd_req_id, + thread_name, + batch_get_cmd_req.batch_get_cmd_req_id, ) - response = func(batch_get_cmd_req_id=batch_get_cmd_req.batch_get_cmd_req_id)['body'] + response = func(batch_get_cmd_req_id=batch_get_cmd_req.batch_get_cmd_req_id)["body"] logger.debug("%s | %s", thread_name, response) - resources: Dict = response['resources'] + resources: Dict = response["resources"] get_files: List[GetFile] = [] for device_id, get_data in resources.items(): get_file = GetFile( device_id=device_id, - filename=get_data['name'], - session_id=get_data['session_id'], - sha256=get_data['sha256'], - size=get_data['size'], + filename=get_data["name"], + session_id=get_data["session_id"], + sha256=get_data["sha256"], + size=get_data["size"], batch_session=self, ) get_files.append(get_file) @@ -279,7 +275,7 @@ def worker(batch_get_cmd_req: BatchGetCmdRequest, func: partial) -> List[GetFile partial_func = partial( self.api.batch_get_command_status, timeout=timeout, - timeout_duration=f'{timeout}s', + timeout_duration=f"{timeout}s", ) with concurrent.futures.ThreadPoolExecutor(max_workers=threads) as executor: completed = executor.map(worker, batch_get_cmd_reqs, [partial_func]) @@ -301,10 +297,10 @@ def get_status_by_req_id( response = self.api.batch_get_command_status( batch_get_cmd_req_id=batch_get_cmd_req_id, timeout=timeout, - timeout_duration=f'{timeout}s', - )['body'] + timeout_duration=f"{timeout}s", + )["body"] self.logger.debug(response) - resources: List[Dict] = response['resources'] + resources: List[Dict] = response["resources"] self.logger.info("Batch GET has retrieved %d files so far", len(resources)) self.logger.debug(resources) @@ -315,10 +311,10 @@ def get_status_by_req_id( for device_id in resources.keys(): get_file = GetFile( device_id=device_id, - filename=resources[device_id]['name'], - session_id=resources[device_id]['session_id'], - sha256=resources[device_id]['sha256'], - size=resources[device_id]['size'], + filename=resources[device_id]["name"], + session_id=resources[device_id]["session_id"], + sha256=resources[device_id]["sha256"], + size=resources[device_id]["size"], batch_session=self, ) get_files.append(get_file) @@ -343,7 +339,7 @@ def worker(session: InnerRTRBatchSession, func: partial): batch_func = partial( self.api.batch_refresh_sessions, timeout=timeout, - timeout_duration=f'{timeout}s', + timeout_duration=f"{timeout}s", ) with concurrent.futures.ThreadPoolExecutor( @@ -362,7 +358,7 @@ def run_generic_command( timeout: int = default_timeout, ) -> Dict: """Execute an RTR command against all systems in the batch session.""" - base_command = command_string.split(' ')[0] + base_command = command_string.split(" ")[0] if base_command not in RTR_COMMANDS: raise ValueError(f"{base_command} is not a valid RTR command") @@ -374,11 +370,11 @@ def run_generic_command( # -Raw for the runscript command) permissions_level = RTR_COMMANDS[base_command] if isinstance(permissions_level, dict): - command_parameter_1 = command_string.split(' ')[1].split('=')[0] + command_parameter_1 = command_string.split(" ")[1].split("=")[0] if command_parameter_1 in permissions_level: permissions_level = permissions_level[command_parameter_1] else: - permissions_level = permissions_level['_default'] + permissions_level = permissions_level["_default"] if permissions_level == "admin": cmd_func = self.admin_api.batch_admin_command @@ -394,7 +390,7 @@ def run_generic_command( base_command=base_command, command_string=command_string, timeout=timeout, - timeout_duration=f'{timeout}s', + timeout_duration=f"{timeout}s", ) partial_worker = partial(generic_rtr_worker, logger=self.logger, device_ids=device_ids) with concurrent.futures.ThreadPoolExecutor( @@ -409,7 +405,7 @@ def run_generic_command( all_responses: Dict = {} for complete in completed: self.logger.info("Executed commands on a batch of %d hosts", len(complete)) - all_responses.update(complete['combined']['resources']) + all_responses.update(complete["combined"]["resources"]) return all_responses @@ -427,7 +423,8 @@ def run_raw_script( self.logger.info( "Running a raw script via RTR. Timeout: %d; script timeout: %d", - timeout, script_timeout, + timeout, + script_timeout, ) command_string = f"runscript -Raw=```{script_text}``` -Timeout={script_timeout}" diff --git a/caracara/modules/rtr/constants.py b/caracara/modules/rtr/constants.py index 56585f2..4b477b3 100644 --- a/caracara/modules/rtr/constants.py +++ b/caracara/modules/rtr/constants.py @@ -1,4 +1,5 @@ """Real Time Response (RTR) Constants.""" + RTR_COMMANDS = { "cat": "read_only", "cd": "read_only", diff --git a/caracara/modules/rtr/get_file.py b/caracara/modules/rtr/get_file.py index ab68b67..e6c4e9e 100644 --- a/caracara/modules/rtr/get_file.py +++ b/caracara/modules/rtr/get_file.py @@ -1,14 +1,13 @@ """RTR Batch GET abstraction module.""" + from __future__ import annotations import os - from dataclasses import dataclass from typing import TYPE_CHECKING import py7zr - if TYPE_CHECKING: # This trick will avoid us from causing a cyclical reference within the class # Credit: https://stackoverflow.com/a/39757388 @@ -77,7 +76,7 @@ def download( filename=self.filename, ) - with open(full_output_path_7z, 'wb') as output_7z_file: + with open(full_output_path_7z, "wb") as output_7z_file: output_7z_file.write(file_contents) if not extract: @@ -86,8 +85,8 @@ def download( with py7zr.SevenZipFile( # nosec - The password 'infected' is generic and always the same file=full_output_path_7z, - mode='r', - password='infected', + mode="r", + password="infected", ) as archive: inner_filename = archive.getnames()[0] target_dir = os.path.dirname(full_output_path_7z) diff --git a/caracara/modules/rtr/rtr.py b/caracara/modules/rtr/rtr.py index dc5f9c3..32f4ee1 100644 --- a/caracara/modules/rtr/rtr.py +++ b/caracara/modules/rtr/rtr.py @@ -8,17 +8,13 @@ |_| |_|_____)_____|\_) |_| |_|_|_|_|_____) |_| |_|_____|___/| __/ \___/|_| |_(___/|_____) |_| """ -import os -from datetime import datetime +import datetime +import os from functools import partial -from typing import Dict, List +from typing import Dict, List, Union -from falconpy import ( - OAuth2, - RealTimeResponse, - RealTimeResponseAdmin, -) +from falconpy import OAuth2, RealTimeResponse, RealTimeResponseAdmin from caracara.common.batching import batch_get_data from caracara.common.module import FalconApiModule, ModuleMapper @@ -60,13 +56,13 @@ def batch_session(self) -> RTRBatchSession: return rtr_batch_session @filter_string - def _search_sessions(self, filters: str or FalconFilter = None): + def _search_sessions(self, filters: Union[str, FalconFilter] = None): """ Get RTR Session IDs based on filters. Arguments --------- - filters: FalconFilter or str, optional + filters: Union[FalconFilter, str], optional Filters to apply to the RTR session search. Returns @@ -124,12 +120,10 @@ def delete_queued_session_command(self, session_id: str, cloud_request_id: str): """Delete a specific command within a queued session.""" self.logger.info( "Deleting command with ID %s from queued session %s", - cloud_request_id, session_id, - ) - self.rtr_api.delete_queued_session( - session_id=session_id, - cloud_request_id=cloud_request_id + cloud_request_id, + session_id, ) + self.rtr_api.delete_queued_session(session_id=session_id, cloud_request_id=cloud_request_id) def clear_queued_sessions(self): """ @@ -148,13 +142,13 @@ def clear_queued_sessions(self): self.delete_queued_session(session_id) @filter_string - def describe_put_files(self, filters: str or FalconFilter = None) -> Dict: + def describe_put_files(self, filters: Union[str, FalconFilter] = None) -> Dict: """ Query RTR PUT files and return a list. Falcon (FQL) filters are supported. Arguments --------- - filters: FalconFilter or str, optional + filters: Union[FalconFilter, str], optional Filters to apply to the PUT file search. Returns @@ -205,21 +199,25 @@ def create_put_file(self, file_path: str, name: str = None, description: str = N name = os.path.basename(file_path) if description is None: - timestamp_str = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ') + timestamp_str = datetime.datetime.now(datetime.timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%SZ" + ) description = f"File uploaded via Caracara at {timestamp_str}" self.logger.info( "Uploading %s to Falcon with name %s and description %s", - file_path, name, description, + file_path, + name, + description, ) - with open(file_path, 'rb') as put_file: + with open(file_path, "rb") as put_file: file_contents = put_file.read() self.rtr_admin_api.create_put_files( name=name, description=description, - files=[(name, (name, file_contents, 'application/script'))] + files=[(name, (name, file_contents, "application/script"))], ) def delete_put_file(self, put_file_id: str): @@ -239,13 +237,13 @@ def delete_put_file(self, put_file_id: str): self.rtr_admin_api.delete_put_files(put_file_id) @filter_string - def describe_scripts(self, filters: str or FalconFilter = None) -> Dict: + def describe_scripts(self, filters: Union[str, FalconFilter] = None) -> Dict: """ Query RTR scripts and return a list. Falcon (FQL) filters are supported. Arguments --------- - filters: str or FalconFilter, optional + filters: Union[FalconFilter, str], optional Filters to apply to the script search Returns diff --git a/caracara/modules/sensor_download/sensor_download.py b/caracara/modules/sensor_download/sensor_download.py index 57412d7..0644543 100644 --- a/caracara/modules/sensor_download/sensor_download.py +++ b/caracara/modules/sensor_download/sensor_download.py @@ -1,12 +1,9 @@ """Falcon Sensor Download API.""" -from falconpy import ( - OAuth2, - SensorDownload, -) +from falconpy import OAuth2, SensorDownload -from caracara.common.module import FalconApiModule, ModuleMapper from caracara.common.exceptions import GenericAPIError +from caracara.common.module import FalconApiModule, ModuleMapper class SensorDownloadApiModule(FalconApiModule): @@ -45,8 +42,7 @@ def get_cid(self, include_checksum: bool = False) -> str: ccid = response["body"]["resources"][0] except (KeyError, IndexError) as exc: self.logger.info( - "Failed to retrieve the CCID from the cloud. " - "Check your API credentials." + "Failed to retrieve the CCID from the cloud. Check your API credentials." ) raise GenericAPIError(response["body"]["errors"]) from exc diff --git a/caracara/modules/sensor_update_policies/__init__.py b/caracara/modules/sensor_update_policies/__init__.py index 4b3117f..b9c8927 100644 --- a/caracara/modules/sensor_update_policies/__init__.py +++ b/caracara/modules/sensor_update_policies/__init__.py @@ -1,7 +1,9 @@ """Caracara Sensor Policies API Module.""" + __all__ = [ - 'SensorUpdatePoliciesApiModule', + "SensorUpdatePoliciesApiModule", ] -from caracara.modules.sensor_update_policies.sensor_update_policies \ - import SensorUpdatePoliciesApiModule +from caracara.modules.sensor_update_policies.sensor_update_policies import ( + SensorUpdatePoliciesApiModule, +) diff --git a/caracara/modules/sensor_update_policies/sensor_update_policies.py b/caracara/modules/sensor_update_policies/sensor_update_policies.py index 0196dfa..9e4c1d4 100644 --- a/caracara/modules/sensor_update_policies/sensor_update_policies.py +++ b/caracara/modules/sensor_update_policies/sensor_update_policies.py @@ -3,14 +3,11 @@ This module allows for interaction with the Sensor Update policies, and facilitates the retrieval of maintenance tokens. """ -import datetime +import datetime from typing import Optional -from falconpy import ( - OAuth2, - SensorUpdatePolicies, -) +from falconpy import OAuth2, SensorUpdatePolicies from caracara.common.exceptions import BaseCaracaraError from caracara.common.module import FalconApiModule, ModuleMapper @@ -50,9 +47,9 @@ def get_maintenance_token( str: The maintenance token. """ if audit_message is None: - timestamp = datetime.datetime.now(tz=datetime.timezone.utc,).strftime( - "%Y-%m-%d %H:%M:%S" - ) + timestamp = datetime.datetime.now( + tz=datetime.timezone.utc, + ).strftime("%Y-%m-%d %H:%M:%S") audit_message = f"Generated via Caracara at {timestamp} UTC" @@ -61,9 +58,9 @@ def get_maintenance_token( device_id=device_id, ) - if response['status_code'] == 200: - body = response['body'] - return body['resources'][0]['uninstall_token'] + if response["status_code"] == 200: + body = response["body"] + return body["resources"][0]["uninstall_token"] raise BaseCaracaraError( "The API operation failed to generate a maintenance token." diff --git a/caracara/modules/users/users.py b/caracara/modules/users/users.py index 934f10c..61e7162 100644 --- a/caracara/modules/users/users.py +++ b/caracara/modules/users/users.py @@ -3,10 +3,7 @@ from functools import partial from typing import Dict, List, Optional, Union -from falconpy import ( - OAuth2, - UserManagement, -) +from falconpy import OAuth2, UserManagement from caracara.common.batching import batch_get_data from caracara.common.exceptions import GenericAPIError @@ -38,10 +35,7 @@ def __init__(self, api_authentication: OAuth2, mapper: ModuleMapper): self.user_management_api = UserManagement(auth_object=self.api_authentication) @filter_string - def get_user_uuids( - self, - filters: Optional[Union[FalconFilter, str]] = None - ) -> List[str]: + def get_user_uuids(self, filters: Optional[Union[FalconFilter, str]] = None) -> List[str]: """Get a list of IDs of users (UUIDs) configured in the Falcon tenant.""" self.logger.info("Obtaining a list of all users in the Falcon tenant") diff --git a/examples/common/__init__.py b/examples/common/__init__.py index 1ae717c..1030138 100644 --- a/examples/common/__init__.py +++ b/examples/common/__init__.py @@ -1,8 +1,9 @@ """Caracara Examples: Common Module.""" + __all__ = [ - 'caracara_example', - 'parse_filter_list', - 'pretty_print', + "caracara_example", + "parse_filter_list", + "pretty_print", ] from examples.common.example import caracara_example diff --git a/examples/common/example.py b/examples/common/example.py index aea0a78..deb9fdf 100644 --- a/examples/common/example.py +++ b/examples/common/example.py @@ -1,9 +1,9 @@ """Caracara Examples: Common Backend.""" + # pylint: disable=duplicate-code import logging import os import sys - from functools import wraps from typing import Dict @@ -12,7 +12,6 @@ from caracara import Client from caracara.common.csdialog import csradiolist_dialog - _config_path = os.path.join( os.path.dirname(os.path.dirname(__file__)), "config.yml", @@ -36,7 +35,7 @@ def _select_profile(config: dict) -> str: continue client_id = str(client_id) - profile_text = f"{profile_name} (Client ID: {client_id[0:7]}{"x"*24})" + profile_text = f"{profile_name} (Client ID: {client_id[0:7]}{'x'*24})" profile_pairs.append((profile_name, profile_text)) profile_name = csradiolist_dialog( @@ -58,13 +57,13 @@ def _get_profile() -> Dict: if not os.path.exists(_config_path): raise FileNotFoundError(f"You must create the file {_config_path}") - with open(_config_path, 'r', encoding='utf8') as yaml_config_file: + with open(_config_path, "r", encoding="utf8") as yaml_config_file: config = yaml.safe_load(yaml_config_file) - if 'profiles' not in config: + if "profiles" not in config: raise KeyError("You must create a profiles stanza in the configuration YAML file") - profile_names = list(config['profiles'].keys()) + profile_names = list(config["profiles"].keys()) # Check to see if the user provided us with a profile name as the first argument profile_name = None if len(sys.argv) > 1: @@ -74,7 +73,7 @@ def _get_profile() -> Dict: else: profile_name = _select_profile(config) - profile = config['profiles'][profile_name] + profile = config["profiles"][profile_name] return profile @@ -83,24 +82,24 @@ def _configure_logging(profile: Dict) -> None: log_format = "%(name)s: %(message)s" log_level = logging.INFO - if 'logging' in profile: - logging_data = profile['logging'] - if 'level' in logging_data: - level_str = str.upper(logging_data['level']) + if "logging" in profile: + logging_data = profile["logging"] + if "level" in logging_data: + level_str = str.upper(logging_data["level"]) if hasattr(logging, level_str): log_level = getattr(logging, level_str) else: raise ValueError(f"{level_str} is not a valid logging level") - if 'format' in logging_data: - log_format = logging_data['format'] + if "format" in logging_data: + log_format = logging_data["format"] logging.basicConfig(format=log_format, level=log_level) def _get_example_settings(profile: Dict, example_abs_path: str) -> Dict: """Load example-specific settings from config.yml based on the filename.""" - if 'examples' not in profile: + if "examples" not in profile: return None # Get the base path of the examples module by obtaining the common @@ -108,7 +107,7 @@ def _get_example_settings(profile: Dict, example_abs_path: str) -> Dict: common_path = os.path.commonpath([__file__, example_abs_path]) # Strip away the common path from the path to the example - example_rel_path = example_abs_path.replace(common_path, '') + example_rel_path = example_abs_path.replace(common_path, "") # Remove the leading / or \ if example_rel_path.startswith(os.path.sep): @@ -126,7 +125,7 @@ def _get_example_settings(profile: Dict, example_abs_path: str) -> Dict: exception. It is up to each individual module to check whether the settings are complete. """ - example_settings = profile['examples'] + example_settings = profile["examples"] for path_section in example_dict_path: if path_section not in example_settings: return None @@ -139,21 +138,22 @@ def _get_example_settings(profile: Dict, example_abs_path: str) -> Dict: def caracara_example(example_func): """Caracara Example Decorator.""" + @wraps(example_func) def wrapped(*args, **kwargs): profile = _get_profile() if not profile: raise TypeError("No profile chosen; aborting") - if 'falcon' not in profile: + if "falcon" not in profile: raise KeyError( "You must create a falcon stanza within the profile's " "section of the configuration YAML file" ) - falcon_config: Dict = profile['falcon'] + falcon_config: Dict = profile["falcon"] - if 'client_id' not in falcon_config or 'client_secret' not in falcon_config: + if "client_id" not in falcon_config or "client_secret" not in falcon_config: raise KeyError("You must include, at minimum, a client_id and client_secret") _configure_logging(profile) @@ -162,13 +162,13 @@ def wrapped(*args, **kwargs): example_settings = _get_example_settings( profile, - example_func.__globals__['__file__'], + example_func.__globals__["__file__"], ) # Pass data back to the example via keyword arguments - kwargs['client'] = client - kwargs['logger'] = logging.getLogger(example_func.__name__) - kwargs['settings'] = example_settings + kwargs["client"] = client + kwargs["logger"] = logging.getLogger(example_func.__name__) + kwargs["settings"] = example_settings return example_func(*args, **kwargs) diff --git a/examples/common/prompts.py b/examples/common/prompts.py index b82b65e..d9143e3 100644 --- a/examples/common/prompts.py +++ b/examples/common/prompts.py @@ -3,6 +3,7 @@ This file contains a pretty prompt_toolkit-based completer for devices. """ + from typing import Dict, Iterable from prompt_toolkit import prompt diff --git a/examples/common/timer.py b/examples/common/timer.py index e6968d0..b2349cd 100644 --- a/examples/common/timer.py +++ b/examples/common/timer.py @@ -1,11 +1,10 @@ """⏰ Caracara Example execution timer.""" import time - from math import ceil -class Timer(): +class Timer: """Timer class to track example execution times.""" def __init__(self): diff --git a/examples/common/utils.py b/examples/common/utils.py index 1711f8d..d283c79 100644 --- a/examples/common/utils.py +++ b/examples/common/utils.py @@ -3,17 +3,17 @@ A series of functions to improve example output. """ -import json -from typing import Dict, List +import json +from typing import Dict, List, Union -def prettify_json(data: List or Dict) -> str: +def prettify_json(data: Union[Dict, List]) -> str: """Dums dictionaries and lists as formatted JSON.""" return json.dumps(data, sort_keys=True, indent=4) -def pretty_print(data: List or Dict, rewrite_new_lines: bool = False) -> str: +def pretty_print(data: Union[Dict, List], rewrite_new_lines: bool = False) -> str: """Format dictionaries and lists nicely, and optionally rewrite new line characters.""" pretty_data = prettify_json(data) if rewrite_new_lines: diff --git a/examples/flight_control/describe_child_cids.py b/examples/flight_control/describe_child_cids.py index ef67b57..4f7411a 100755 --- a/examples/flight_control/describe_child_cids.py +++ b/examples/flight_control/describe_child_cids.py @@ -7,14 +7,13 @@ This example will show all Child CIDs within a Parent Falcon Flight Control / MSSP CID. """ from caracara import Client - from examples.common import caracara_example, pretty_print @caracara_example def describe_child_cids(**kwargs): """Dump out every child Falcon tenant to screen when authenticated to a Parent CID.""" - client: Client = kwargs['client'] + client: Client = kwargs["client"] child_cids = client.flight_control.describe_child_cids() print(pretty_print(child_cids)) diff --git a/examples/hosts/find_devices.py b/examples/hosts/find_devices.py index e3cb13e..2bf3614 100755 --- a/examples/hosts/find_devices.py +++ b/examples/hosts/find_devices.py @@ -18,31 +18,29 @@ The example demonstrates how to use the Hosts API. """ import logging - from typing import Dict, List from caracara import Client - from examples.common import ( + NoDevicesFound, + Timer, caracara_example, parse_filter_list, pretty_print, - NoDevicesFound, - Timer, ) @caracara_example def find_devices(**kwargs): """Find devices by hostname.""" - client: Client = kwargs['client'] - logger: logging.Logger = kwargs['logger'] - settings: Dict = kwargs['settings'] + client: Client = kwargs["client"] + logger: logging.Logger = kwargs["logger"] + settings: Dict = kwargs["settings"] timer: Timer = Timer() - filters = client.FalconFilter(dialect='hosts') - if 'filters' in settings: - filter_list: List[Dict] = settings['filters'] + filters = client.FalconFilter(dialect="hosts") + if "filters" in settings: + filter_list: List[Dict] = settings["filters"] parse_filter_list(filter_list, filters) if filters.filters: diff --git a/examples/hosts/find_stale_sensors.py b/examples/hosts/find_stale_sensors.py index cdbcdda..8e99299 100755 --- a/examples/hosts/find_stale_sensors.py +++ b/examples/hosts/find_stale_sensors.py @@ -13,36 +13,30 @@ The example demonstrates how to use the Hosts API and a FalconFilter using a date. """ import logging - from datetime import datetime, timezone from typing import Dict from dateutil import parser as dparser from caracara import Client - -from examples.common import ( - caracara_example, - NoDevicesFound, - Timer, -) +from examples.common import NoDevicesFound, Timer, caracara_example @caracara_example def find_stale_sensors(**kwargs): """Find devices that have not checked in after X number of days.""" - client: Client = kwargs['client'] - logger: logging.Logger = kwargs['logger'] - settings: Dict = kwargs['settings'] + client: Client = kwargs["client"] + logger: logging.Logger = kwargs["logger"] + settings: Dict = kwargs["settings"] timer: Timer = Timer() days = 7 remove = False if settings: - days = int(settings.get('days', days)) + days = int(settings.get("days", days)) remove = bool(settings.get("remove", remove)) - filters = client.FalconFilter(dialect='hosts') + filters = client.FalconFilter(dialect="hosts") filters.create_new_filter("LastSeen", f"-{days}d", "LTE") logger.info("Using the FQL filter: %s", filters.get_fql()) @@ -62,12 +56,12 @@ def find_stale_sensors(**kwargs): time_now = datetime.now(timezone.utc) for device_id, device_data in response_data.items(): - last_seen = dparser.isoparse(device_data['last_seen']) + last_seen = dparser.isoparse(device_data["last_seen"]) last_seen_days = (time_now - last_seen).days logger.info( "[%s] %s was last seen %d days ago", device_id, - device_data['hostname'], + device_data["hostname"], last_seen_days, ) diff --git a/examples/hosts/list_all_devices.py b/examples/hosts/list_all_devices.py index a9897e4..a84fab4 100755 --- a/examples/hosts/list_all_devices.py +++ b/examples/hosts/list_all_devices.py @@ -11,19 +11,14 @@ import logging from caracara import Client - -from examples.common import ( - caracara_example, - NoDevicesFound, - Timer, -) +from examples.common import NoDevicesFound, Timer, caracara_example @caracara_example def list_all_devices(**kwargs): """List All Devices.""" - client: Client = kwargs['client'] - logger: logging.Logger = kwargs['logger'] + client: Client = kwargs["client"] + logger: logging.Logger = kwargs["logger"] timer: Timer = Timer() logger.info("Listing all devices within the tenant") diff --git a/examples/hosts/list_all_group_member_ids.py b/examples/hosts/list_all_group_member_ids.py index c9dc3c8..70b85ed 100755 --- a/examples/hosts/list_all_group_member_ids.py +++ b/examples/hosts/list_all_group_member_ids.py @@ -11,19 +11,14 @@ import logging from caracara import Client - -from examples.common import ( - caracara_example, - NoGroupsFound, - Timer, -) +from examples.common import NoGroupsFound, Timer, caracara_example @caracara_example def list_all_group_member_ids(**kwargs): """List All Host Group Members.""" - client: Client = kwargs['client'] - logger: logging.Logger = kwargs['logger'] + client: Client = kwargs["client"] + logger: logging.Logger = kwargs["logger"] timer: Timer = Timer() logger.info("Listing all host groups and their members within the tenant") @@ -34,17 +29,21 @@ def list_all_group_member_ids(**kwargs): for group_id, group_data in response_data.items(): logger.info( "Group %s (%s) contains %d devices", - group_id, group_data['name'], len(group_data['devices']), + group_id, + group_data["name"], + len(group_data["devices"]), ) - for device in group_data['devices']: - device_id = device['device_id'] + for device in group_data["devices"]: + device_id = device["device_id"] hostname = device.get("hostname", "No Hostname") discovered_devices.add(device_id) logger.info("Device %s (%s)", device_id, hostname) logger.info( "Found %d groups with %d total members in %f seconds", - len(response_data), len(discovered_devices), float(timer), + len(response_data), + len(discovered_devices), + float(timer), ) if not response_data: diff --git a/examples/hosts/list_all_group_members.py b/examples/hosts/list_all_group_members.py index 7b3299b..e4dcf81 100755 --- a/examples/hosts/list_all_group_members.py +++ b/examples/hosts/list_all_group_members.py @@ -8,19 +8,16 @@ The example demonstrates how to use the Hosts API. """ import logging + from caracara import Client -from examples.common import ( - caracara_example, - NoGroupsFound, - Timer, -) +from examples.common import NoGroupsFound, Timer, caracara_example @caracara_example def list_all_group_members(**kwargs): """List All Host Group Members.""" - client: Client = kwargs['client'] - logger: logging.Logger = kwargs['logger'] + client: Client = kwargs["client"] + logger: logging.Logger = kwargs["logger"] timer: Timer = Timer() logger.info("Listing all host groups and their members within the tenant") @@ -31,17 +28,21 @@ def list_all_group_members(**kwargs): for group_id, group_data in response_data.items(): logger.info( "Group %s (%s) contains %d devices", - group_id, group_data['name'], len(group_data['devices']), + group_id, + group_data["name"], + len(group_data["devices"]), ) - for device in group_data['devices']: - device_id = device['device_id'] + for device in group_data["devices"]: + device_id = device["device_id"] hostname = device.get("hostname", "No Hostname") discovered_devices.add(device_id) logger.info("%s (%s)", device_id, hostname) logger.info( "Found %d groups with %d total members in %f seconds", - len(response_data), len(discovered_devices), float(timer), + len(response_data), + len(discovered_devices), + float(timer), ) if not response_data: diff --git a/examples/hosts/list_all_groups.py b/examples/hosts/list_all_groups.py index 6d23fe6..7790191 100755 --- a/examples/hosts/list_all_groups.py +++ b/examples/hosts/list_all_groups.py @@ -11,19 +11,14 @@ import logging from caracara import Client - -from examples.common import ( - caracara_example, - NoGroupsFound, - Timer, -) +from examples.common import NoGroupsFound, Timer, caracara_example @caracara_example def list_all_groups(**kwargs): """List All Host Groups.""" - client: Client = kwargs['client'] - logger: logging.Logger = kwargs['logger'] + client: Client = kwargs["client"] + logger: logging.Logger = kwargs["logger"] timer: Timer = Timer() logger.info("Listing all host groups within the tenant") diff --git a/examples/hosts/list_device_states.py b/examples/hosts/list_device_states.py index ad36a52..8fe55d3 100755 --- a/examples/hosts/list_device_states.py +++ b/examples/hosts/list_device_states.py @@ -11,19 +11,14 @@ import logging from caracara import Client - -from examples.common import ( - caracara_example, - NoDevicesFound, - Timer, -) +from examples.common import NoDevicesFound, Timer, caracara_example @caracara_example def list_device_states(**kwargs): """List All Device states.""" - client: Client = kwargs['client'] - logger: logging.Logger = kwargs['logger'] + client: Client = kwargs["client"] + logger: logging.Logger = kwargs["logger"] timer: Timer = Timer() logger.info("Listing all device states within the tenant") diff --git a/examples/hosts/list_hidden_devices.py b/examples/hosts/list_hidden_devices.py index 5c86e80..06c8fa6 100755 --- a/examples/hosts/list_hidden_devices.py +++ b/examples/hosts/list_hidden_devices.py @@ -11,19 +11,14 @@ import logging from caracara import Client - -from examples.common import ( - caracara_example, - NoDevicesFound, - Timer, -) +from examples.common import NoDevicesFound, Timer, caracara_example @caracara_example def list_hidden_devices(**kwargs): """List All Devices.""" - client: Client = kwargs['client'] - logger: logging.Logger = kwargs['logger'] + client: Client = kwargs["client"] + logger: logging.Logger = kwargs["logger"] timer: Timer = Timer() logger.info("Listing all hidden devices within the tenant") diff --git a/examples/hosts/list_login_history.py b/examples/hosts/list_login_history.py index fc6f42d..d20fe94 100755 --- a/examples/hosts/list_login_history.py +++ b/examples/hosts/list_login_history.py @@ -9,23 +9,17 @@ The example demonstrates how to use the Hosts API. """ import logging - from typing import Dict, List from caracara import Client - -from examples.common import ( - caracara_example, - NoDevicesFound, - Timer, -) +from examples.common import NoDevicesFound, Timer, caracara_example @caracara_example def list_login_history(**kwargs): """List All Devices.""" - client: Client = kwargs['client'] - logger: logging.Logger = kwargs['logger'] + client: Client = kwargs["client"] + logger: logging.Logger = kwargs["logger"] timer: Timer = Timer() logger.info("Listing login history for all devices within the tenant") @@ -34,15 +28,17 @@ def list_login_history(**kwargs): response_data = client.hosts.describe_login_history() for device_id, device_data in response_data.items(): - recents: List[Dict] = device_data.get('recent_logins') + recents: List[Dict] = device_data.get("recent_logins") logins = "No logins found" found = [] if recents: for login in recents: - login_detail = "".join([ - f"{login.get('user_name', 'Username not found')}: ", - f"{login.get('login_time', 'Unknown')}", - ]) + login_detail = "".join( + [ + f"{login.get('user_name', 'Username not found')}: ", + f"{login.get('login_time', 'Unknown')}", + ] + ) if login_detail not in found: found.append(login_detail) diff --git a/examples/hosts/list_network_history.py b/examples/hosts/list_network_history.py index f5a2714..4713546 100755 --- a/examples/hosts/list_network_history.py +++ b/examples/hosts/list_network_history.py @@ -9,23 +9,17 @@ The example demonstrates how to use the Hosts API. """ import logging - from typing import Dict, List from caracara import Client - -from examples.common import ( - caracara_example, - NoDevicesFound, - Timer, -) +from examples.common import NoDevicesFound, Timer, caracara_example @caracara_example def list_network_history(**kwargs): """List All Devices.""" - client: Client = kwargs['client'] - logger: logging.Logger = kwargs['logger'] + client: Client = kwargs["client"] + logger: logging.Logger = kwargs["logger"] timer: Timer = Timer() logger.info("Listing all network address changes within the tenant") @@ -39,11 +33,13 @@ def list_network_history(**kwargs): found = [] if recents: for change in recents: - change_detail = "".join([ - f"{change.get('ip_address', 'IP Unknown')} ", - f"({change.get('mac_address', 'MAC Unknown')}) on ", - f"{change.get('timestamp', 'Unknown')}", - ]) + change_detail = "".join( + [ + f"{change.get('ip_address', 'IP Unknown')} ", + f"({change.get('mac_address', 'MAC Unknown')}) on ", + f"{change.get('timestamp', 'Unknown')}", + ] + ) if change_detail not in found: found.append(change_detail) changes = ", ".join(found) diff --git a/examples/hosts/list_windows_devices.py b/examples/hosts/list_windows_devices.py index 0ee4f0a..b1ccf0e 100755 --- a/examples/hosts/list_windows_devices.py +++ b/examples/hosts/list_windows_devices.py @@ -12,24 +12,19 @@ import logging from caracara import Client - -from examples.common import ( - caracara_example, - NoDevicesFound, - Timer, -) +from examples.common import NoDevicesFound, Timer, caracara_example @caracara_example def list_windows_devices(**kwargs): """List Windows Devices.""" - client: Client = kwargs['client'] - logger: logging.Logger = kwargs['logger'] + client: Client = kwargs["client"] + logger: logging.Logger = kwargs["logger"] timer: Timer = Timer() logger.info("Grabbing all Windows devices within the tenant") - filters = client.FalconFilter(dialect='hosts') + filters = client.FalconFilter(dialect="hosts") filters.create_new_filter("OS", "Windows") logger.info("Using the FQL filter: %s", filters.get_fql()) @@ -42,7 +37,11 @@ def list_windows_devices(**kwargs): hostname = device_data.get("hostname", "Unknown Hostname") logger.info("%s (%s)", device_id, hostname) - logger.info("Found %d devices running Windows in %s seconds", len(response_data), float(timer)) + logger.info( + "Found %d devices running Windows in %s seconds", + len(response_data), + float(timer), + ) if __name__ in ["__main__", "examples.hosts.list_windows_devices"]: diff --git a/examples/hosts/show_agent_versions.py b/examples/hosts/show_agent_versions.py index fce5386..32008b1 100755 --- a/examples/hosts/show_agent_versions.py +++ b/examples/hosts/show_agent_versions.py @@ -18,19 +18,14 @@ from tabulate import tabulate from caracara import Client - -from examples.common import ( - caracara_example, - NoDevicesFound, - Timer, -) +from examples.common import NoDevicesFound, Timer, caracara_example @caracara_example def show_agent_versions(**kwargs): """List all devices and agent versions.""" - client: Client = kwargs['client'] - logger: logging.Logger = kwargs['logger'] + client: Client = kwargs["client"] + logger: logging.Logger = kwargs["logger"] timer: Timer = Timer() logger.info("Grabbing all devices within the tenant") @@ -41,7 +36,7 @@ def show_agent_versions(**kwargs): "hostname": "Hostname", "local_ip": "Internal IP", "external_ip": "External IP", - "agent_version": "Agent Version" + "agent_version": "Agent Version", } devices = [] with client: diff --git a/examples/prevention_policies/create_prevention_policy.py b/examples/prevention_policies/create_prevention_policy.py index 48691f3..59267e8 100755 --- a/examples/prevention_policies/create_prevention_policy.py +++ b/examples/prevention_policies/create_prevention_policy.py @@ -8,14 +8,13 @@ You can use this code sample to customise the policy. """ from caracara import Client - from examples.common import caracara_example, pretty_print @caracara_example def create_prevention_policy(**kwargs): """Create a new Windows prevention policy with everything enabled.""" - client: Client = kwargs['client'] + client: Client = kwargs["client"] prevention_policy = client.prevention_policies.new_policy("Windows") pretty_print(prevention_policy.flat_dump()) diff --git a/examples/prevention_policies/describe_prevention_policies.py b/examples/prevention_policies/describe_prevention_policies.py index f4c5ecc..2f4c7c8 100755 --- a/examples/prevention_policies/describe_prevention_policies.py +++ b/examples/prevention_policies/describe_prevention_policies.py @@ -11,23 +11,21 @@ the response policies API. """ import logging - from typing import List from caracara import Client, Policy - from examples.common import caracara_example, pretty_print @caracara_example def describe_prevention_policies(**kwargs): """List Prevention Policies.""" - client: Client = kwargs['client'] - logger: logging.Logger = kwargs['logger'] + client: Client = kwargs["client"] + logger: logging.Logger = kwargs["logger"] logger.info("Grabbing all Windows prevention policies from the Falcon tenant") - filters = client.FalconFilter(dialect='hosts') + filters = client.FalconFilter(dialect="hosts") filters.create_new_filter("OS", "Windows") policies: List[Policy] = client.prevention_policies.describe_policies(filters=filters) @@ -46,5 +44,5 @@ def describe_prevention_policies(**kwargs): i += 1 -if __name__ == '__main__': +if __name__ == "__main__": describe_prevention_policies() diff --git a/examples/response_policies/create_response_policy.py b/examples/response_policies/create_response_policy.py index ee811ea..96c6ad3 100755 --- a/examples/response_policies/create_response_policy.py +++ b/examples/response_policies/create_response_policy.py @@ -10,15 +10,14 @@ import logging from caracara import Client - from examples.common import caracara_example, pretty_print @caracara_example def create_response_policy(**kwargs): """Create a new Windows response policy with everything enabled.""" - client: Client = kwargs['client'] - logger: logging.Logger = kwargs['logger'] + client: Client = kwargs["client"] + logger: logging.Logger = kwargs["logger"] response_policy = client.response_policies.new_policy("Windows") logger.info(pretty_print(response_policy.flat_dump())) diff --git a/examples/response_policies/describe_response_policies.py b/examples/response_policies/describe_response_policies.py index bf5c89a..8f6c3f8 100755 --- a/examples/response_policies/describe_response_policies.py +++ b/examples/response_policies/describe_response_policies.py @@ -11,23 +11,21 @@ the response policies API. """ import logging - from typing import List from caracara import Client, Policy - from examples.common import caracara_example, pretty_print @caracara_example def describe_response_policies(**kwargs): """List Response Policies Example.""" - client: Client = kwargs['client'] - logger: logging.Logger = kwargs['logger'] + client: Client = kwargs["client"] + logger: logging.Logger = kwargs["logger"] logger.info("Grabbing all Windows response policies from the Falcon tenant") - filters = client.FalconFilter(dialect='response_policies') + filters = client.FalconFilter(dialect="response_policies") filters.create_new_filter("OS", "Windows") policies: List[Policy] = client.response_policies.describe_policies(filters=filters) @@ -43,5 +41,5 @@ def describe_response_policies(**kwargs): i += 1 -if __name__ == '__main__': +if __name__ == "__main__": describe_response_policies() diff --git a/examples/rtr/clear_queued_sessions.py b/examples/rtr/clear_queued_sessions.py index d82da86..c1c81f4 100755 --- a/examples/rtr/clear_queued_sessions.py +++ b/examples/rtr/clear_queued_sessions.py @@ -13,15 +13,14 @@ import logging from caracara import Client - from examples.common import caracara_example @caracara_example def clear_queued_sessions(**kwargs): """Clear all RTR sessions queued up by this API client.""" - client: Client = kwargs['client'] - logger: logging.Logger = kwargs['logger'] + client: Client = kwargs["client"] + logger: logging.Logger = kwargs["logger"] logger.info("Clearing all queued RTR sessions") client.rtr.clear_queued_sessions() diff --git a/examples/rtr/describe_put_files.py b/examples/rtr/describe_put_files.py index adb469f..a65f359 100755 --- a/examples/rtr/describe_put_files.py +++ b/examples/rtr/describe_put_files.py @@ -10,15 +10,14 @@ import logging from caracara import Client - from examples.common import caracara_example, pretty_print @caracara_example def describe_put_files(**kwargs): """Describe all available PUT files and and write the output to the log.""" - client: Client = kwargs['client'] - logger: logging.Logger = kwargs['logger'] + client: Client = kwargs["client"] + logger: logging.Logger = kwargs["logger"] with client: logger.info("Listing available PUT files") @@ -26,5 +25,5 @@ def describe_put_files(**kwargs): logger.info("%s\n%s", put_file_id, pretty_print(put_file_data)) -if __name__ == '__main__': +if __name__ == "__main__": describe_put_files() diff --git a/examples/rtr/describe_queued_sessions.py b/examples/rtr/describe_queued_sessions.py index a725930..a815932 100755 --- a/examples/rtr/describe_queued_sessions.py +++ b/examples/rtr/describe_queued_sessions.py @@ -10,15 +10,14 @@ import logging from caracara import Client - from examples.common import caracara_example, pretty_print @caracara_example def describe_queued_sessions(**kwargs): """Describe all currently queued RTR sessions and write the output to the log.""" - client: Client = kwargs['client'] - logger: logging.Logger = kwargs['logger'] + client: Client = kwargs["client"] + logger: logging.Logger = kwargs["logger"] logger.info("Listing queued RTR sessions") sessions = client.rtr.describe_queued_sessions() diff --git a/examples/rtr/describe_scripts.py b/examples/rtr/describe_scripts.py index 6fe4ee3..4869fcc 100755 --- a/examples/rtr/describe_scripts.py +++ b/examples/rtr/describe_scripts.py @@ -10,15 +10,14 @@ import logging from caracara import Client - from examples.common import caracara_example, pretty_print @caracara_example def describe_scripts(**kwargs): """Describe all available cloud scripts and and write the output to the log.""" - client: Client = kwargs['client'] - logger: logging.Logger = kwargs['logger'] + client: Client = kwargs["client"] + logger: logging.Logger = kwargs["logger"] with client: logger.info("Listing available PUT files") diff --git a/examples/rtr/download_event_log.py b/examples/rtr/download_event_log.py index 8f34367..4a05d4a 100755 --- a/examples/rtr/download_event_log.py +++ b/examples/rtr/download_event_log.py @@ -27,13 +27,11 @@ import logging import os import time - from typing import Dict, List from caracara import Client from caracara.modules.rtr.batch_session import BatchGetCmdRequest, RTRBatchSession from caracara.modules.rtr.get_file import GetFile - from examples.common import caracara_example, parse_filter_list @@ -82,9 +80,9 @@ def download_loop( @caracara_example def download_event_log(**kwargs): # pylint: disable=too-many-locals """Download a specified Windows Event Log from all online systems.""" - client: Client = kwargs['client'] - logger: logging.Logger = kwargs['logger'] - settings: Dict = kwargs['settings'] + client: Client = kwargs["client"] + logger: logging.Logger = kwargs["logger"] + settings: Dict = kwargs["settings"] filename: str = settings.get("filename") if not filename: @@ -102,7 +100,7 @@ def download_event_log(**kwargs): # pylint: disable=too-many-locals logger.info("Downloading the event log %s", filename) - filters = client.FalconFilter(dialect='rtr') + filters = client.FalconFilter(dialect="rtr") filter_list: List[Dict] = settings.get("filters") # This is a custom generic function to load filters from the config file. You can @@ -135,7 +133,8 @@ def download_event_log(**kwargs): # pylint: disable=too-many-locals devices = list(x.devices.keys() for x in batch_get_cmd_reqs) logger.info( "%d batch get requests executed successfully against %d systems", - len(batch_get_cmd_reqs), len(devices), + len(batch_get_cmd_reqs), + len(devices), ) logger.info(batch_get_cmd_req_ids) logger.debug(devices) diff --git a/examples/rtr/queue_command.py b/examples/rtr/queue_command.py index 125fae9..b2db2a9 100755 --- a/examples/rtr/queue_command.py +++ b/examples/rtr/queue_command.py @@ -17,39 +17,39 @@ command: CommandToRun """ import logging - from typing import Dict, List from caracara import Client - from examples.common import ( - caracara_example, - parse_filter_list, MissingArgument, NoDevicesFound, NoSessionsConnected, + caracara_example, + parse_filter_list, ) @caracara_example def queue_command(**kwargs): """Run a single RTR command against all hosts matching the filter, even if offline.""" - client: Client = kwargs['client'] - logger: logging.Logger = kwargs['logger'] - settings: Dict = kwargs['settings'] - - if not settings or 'command' not in settings: - error_message = "".join([ - "You must configure the 'cmd' argument within your " - "YAML file to proceed with this example." - ]) + client: Client = kwargs["client"] + logger: logging.Logger = kwargs["logger"] + settings: Dict = kwargs["settings"] + + if not settings or "command" not in settings: + error_message = "".join( + [ + "You must configure the 'cmd' argument within your " + "YAML file to proceed with this example." + ] + ) logger.critical(error_message) raise MissingArgument("cmd", error_message) - cmd: str = settings['command'] + cmd: str = settings["command"] logger.info("Running the command: %s", cmd) - filters = client.FalconFilter(dialect='rtr') + filters = client.FalconFilter(dialect="rtr") filter_list: List[Dict] = settings.get("filters") parse_filter_list(filter_list, filters) diff --git a/examples/sensor_update_policies/get_maintenance_token.py b/examples/sensor_update_policies/get_maintenance_token.py index 8f8ac30..6c59e4a 100755 --- a/examples/sensor_update_policies/get_maintenance_token.py +++ b/examples/sensor_update_policies/get_maintenance_token.py @@ -9,17 +9,13 @@ from typing import Dict from caracara import Client - -from examples.common import ( - caracara_example, - choose_item, -) +from examples.common import caracara_example, choose_item @caracara_example def get_maintenance_token(**kwargs): """Search for a system and get its maintenance token.""" - client: Client = kwargs['client'] + client: Client = kwargs["client"] print("Getting all devices in the Falcon tenant") devices: Dict[str, Dict] = client.hosts.describe_devices() @@ -28,7 +24,7 @@ def get_maintenance_token(**kwargs): "MAINTENANCE": "Bulk Maintenance Token", } for device_id, device_data in devices.items(): - id_name_mapping[device_id] = device_data['hostname'] + id_name_mapping[device_id] = device_data["hostname"] chosen_id = choose_item(id_name_mapping, prompt_text="Search for a Device") diff --git a/examples/users/add_user.py b/examples/users/add_user.py index 424bbf5..ec9f5cf 100644 --- a/examples/users/add_user.py +++ b/examples/users/add_user.py @@ -9,10 +9,8 @@ """ import logging -from examples.common import caracara_example, pretty_print - from caracara import Client - +from examples.common import caracara_example, pretty_print @caracara_example diff --git a/examples/users/delete_user.py b/examples/users/delete_user.py index a4f5122..343f9cf 100644 --- a/examples/users/delete_user.py +++ b/examples/users/delete_user.py @@ -9,9 +9,8 @@ """ import logging -from examples.common import caracara_example, pretty_print - from caracara import Client +from examples.common import caracara_example, pretty_print @caracara_example diff --git a/examples/users/describe_roles.py b/examples/users/describe_roles.py index 01a6c4e..e76ec44 100755 --- a/examples/users/describe_roles.py +++ b/examples/users/describe_roles.py @@ -10,20 +10,19 @@ import logging from caracara import Client - from examples.common import caracara_example, pretty_print @caracara_example def describe_roles(**kwargs): """List Roles Example.""" - client: Client = kwargs['client'] - logger: logging.Logger = kwargs['logger'] + client: Client = kwargs["client"] + logger: logging.Logger = kwargs["logger"] logger.info("Describing all possible roles in the Falcon tenant") available_role_info = client.users.describe_available_roles() logger.info(pretty_print(available_role_info)) -if __name__ == '__main__': +if __name__ == "__main__": describe_roles() diff --git a/examples/users/describe_users.py b/examples/users/describe_users.py index b54c82a..35fae6d 100755 --- a/examples/users/describe_users.py +++ b/examples/users/describe_users.py @@ -10,15 +10,14 @@ import logging from caracara import Client - from examples.common import caracara_example, pretty_print @caracara_example def describe_users(**kwargs): """List Users Example.""" - client: Client = kwargs['client'] - logger: logging.Logger = kwargs['logger'] + client: Client = kwargs["client"] + logger: logging.Logger = kwargs["logger"] logger.info("Describing all users in the Falcon tenant") @@ -26,5 +25,5 @@ def describe_users(**kwargs): logger.info(pretty_print(users)) -if __name__ == '__main__': +if __name__ == "__main__": describe_users() diff --git a/poetry.lock b/poetry.lock index 2f56deb..af20aba 100644 --- a/poetry.lock +++ b/poetry.lock @@ -16,13 +16,13 @@ typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} [[package]] name = "bandit" -version = "1.7.9" +version = "1.7.10" description = "Security oriented static analyser for python code." optional = false python-versions = ">=3.8" files = [ - {file = "bandit-1.7.9-py3-none-any.whl", hash = "sha256:52077cb339000f337fb25f7e045995c4ad01511e716e5daac37014b9752de8ec"}, - {file = "bandit-1.7.9.tar.gz", hash = "sha256:7c395a436743018f7be0a4cbb0a4ea9b902b6d87264ddecf8cfdc73b4f78ff61"}, + {file = "bandit-1.7.10-py3-none-any.whl", hash = "sha256:665721d7bebbb4485a339c55161ac0eedde27d51e638000d91c8c2d68343ad02"}, + {file = "bandit-1.7.10.tar.gz", hash = "sha256:59ed5caf5d92b6ada4bf65bc6437feea4a9da1093384445fed4d472acc6cff7b"}, ] [package.dependencies] @@ -38,6 +38,52 @@ test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", toml = ["tomli (>=1.1.0)"] yaml = ["PyYAML"] +[[package]] +name = "black" +version = "24.8.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.8" +files = [ + {file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"}, + {file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"}, + {file = "black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42"}, + {file = "black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a"}, + {file = "black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1"}, + {file = "black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af"}, + {file = "black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4"}, + {file = "black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af"}, + {file = "black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368"}, + {file = "black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed"}, + {file = "black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018"}, + {file = "black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2"}, + {file = "black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd"}, + {file = "black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2"}, + {file = "black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e"}, + {file = "black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920"}, + {file = "black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c"}, + {file = "black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e"}, + {file = "black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47"}, + {file = "black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb"}, + {file = "black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed"}, + {file = "black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + [[package]] name = "brotli" version = "1.1.0" @@ -171,100 +217,100 @@ cffi = ">=1.0.0" [[package]] name = "caracara-filters" -version = "0.2.0" +version = "1.0.0" description = "FQL generation engine for Caracara" optional = false -python-versions = ">=3.7.2,<4.0.0" +python-versions = "<4.0.0,>=3.8.2" files = [ - {file = "caracara_filters-0.2.0-py3-none-any.whl", hash = "sha256:853bb9ebf754dc6d707c17b4489dc5c3cf79756071736921b14f60a33cab2b77"}, - {file = "caracara_filters-0.2.0.tar.gz", hash = "sha256:cc0813a7e0cf64b5e9c20cee289610cd25f7a03b2d84988e5fa21333a2485d5e"}, + {file = "caracara_filters-1.0.0-py3-none-any.whl", hash = "sha256:7c9d2ddac483e4ec94b288fe7369bb8633401a9743fa8ea84da9fbeadbed1809"}, + {file = "caracara_filters-1.0.0.tar.gz", hash = "sha256:f31c03ceb884ff1db371e9bcb72158446ca1f2ba2e2de402028d504de111fb44"}, ] [[package]] name = "certifi" -version = "2024.7.4" +version = "2024.8.30" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, - {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, ] [[package]] name = "cffi" -version = "1.17.0" +version = "1.17.1" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" files = [ - {file = "cffi-1.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f9338cc05451f1942d0d8203ec2c346c830f8e86469903d5126c1f0a13a2bcbb"}, - {file = "cffi-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0ce71725cacc9ebf839630772b07eeec220cbb5f03be1399e0457a1464f8e1a"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c815270206f983309915a6844fe994b2fa47e5d05c4c4cef267c3b30e34dbe42"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6bdcd415ba87846fd317bee0774e412e8792832e7805938987e4ede1d13046d"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a98748ed1a1df4ee1d6f927e151ed6c1a09d5ec21684de879c7ea6aa96f58f2"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a048d4f6630113e54bb4b77e315e1ba32a5a31512c31a273807d0027a7e69ab"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24aa705a5f5bd3a8bcfa4d123f03413de5d86e497435693b638cbffb7d5d8a1b"}, - {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:856bf0924d24e7f93b8aee12a3a1095c34085600aa805693fb7f5d1962393206"}, - {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:4304d4416ff032ed50ad6bb87416d802e67139e31c0bde4628f36a47a3164bfa"}, - {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:331ad15c39c9fe9186ceaf87203a9ecf5ae0ba2538c9e898e3a6967e8ad3db6f"}, - {file = "cffi-1.17.0-cp310-cp310-win32.whl", hash = "sha256:669b29a9eca6146465cc574659058ed949748f0809a2582d1f1a324eb91054dc"}, - {file = "cffi-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:48b389b1fd5144603d61d752afd7167dfd205973a43151ae5045b35793232aa2"}, - {file = "cffi-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5d97162c196ce54af6700949ddf9409e9833ef1003b4741c2b39ef46f1d9720"}, - {file = "cffi-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ba5c243f4004c750836f81606a9fcb7841f8874ad8f3bf204ff5e56332b72b9"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb9333f58fc3a2296fb1d54576138d4cf5d496a2cc118422bd77835e6ae0b9cb"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:435a22d00ec7d7ea533db494da8581b05977f9c37338c80bc86314bec2619424"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1df34588123fcc88c872f5acb6f74ae59e9d182a2707097f9e28275ec26a12d"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df8bb0010fdd0a743b7542589223a2816bdde4d94bb5ad67884348fa2c1c67e8"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8b5b9712783415695663bd463990e2f00c6750562e6ad1d28e072a611c5f2a6"}, - {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ffef8fd58a36fb5f1196919638f73dd3ae0db1a878982b27a9a5a176ede4ba91"}, - {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e67d26532bfd8b7f7c05d5a766d6f437b362c1bf203a3a5ce3593a645e870b8"}, - {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45f7cd36186db767d803b1473b3c659d57a23b5fa491ad83c6d40f2af58e4dbb"}, - {file = "cffi-1.17.0-cp311-cp311-win32.whl", hash = "sha256:a9015f5b8af1bb6837a3fcb0cdf3b874fe3385ff6274e8b7925d81ccaec3c5c9"}, - {file = "cffi-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:b50aaac7d05c2c26dfd50c3321199f019ba76bb650e346a6ef3616306eed67b0"}, - {file = "cffi-1.17.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aec510255ce690d240f7cb23d7114f6b351c733a74c279a84def763660a2c3bc"}, - {file = "cffi-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2770bb0d5e3cc0e31e7318db06efcbcdb7b31bcb1a70086d3177692a02256f59"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db9a30ec064129d605d0f1aedc93e00894b9334ec74ba9c6bdd08147434b33eb"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a47eef975d2b8b721775a0fa286f50eab535b9d56c70a6e62842134cf7841195"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3e0992f23bbb0be00a921eae5363329253c3b86287db27092461c887b791e5e"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6107e445faf057c118d5050560695e46d272e5301feffda3c41849641222a828"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb862356ee9391dc5a0b3cbc00f416b48c1b9a52d252d898e5b7696a5f9fe150"}, - {file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c1c13185b90bbd3f8b5963cd8ce7ad4ff441924c31e23c975cb150e27c2bf67a"}, - {file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17c6d6d3260c7f2d94f657e6872591fe8733872a86ed1345bda872cfc8c74885"}, - {file = "cffi-1.17.0-cp312-cp312-win32.whl", hash = "sha256:c3b8bd3133cd50f6b637bb4322822c94c5ce4bf0d724ed5ae70afce62187c492"}, - {file = "cffi-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:dca802c8db0720ce1c49cce1149ff7b06e91ba15fa84b1d59144fef1a1bc7ac2"}, - {file = "cffi-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ce01337d23884b21c03869d2f68c5523d43174d4fc405490eb0091057943118"}, - {file = "cffi-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cab2eba3830bf4f6d91e2d6718e0e1c14a2f5ad1af68a89d24ace0c6b17cced7"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14b9cbc8f7ac98a739558eb86fabc283d4d564dafed50216e7f7ee62d0d25377"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b00e7bcd71caa0282cbe3c90966f738e2db91e64092a877c3ff7f19a1628fdcb"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41f4915e09218744d8bae14759f983e466ab69b178de38066f7579892ff2a555"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4760a68cab57bfaa628938e9c2971137e05ce48e762a9cb53b76c9b569f1204"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:011aff3524d578a9412c8b3cfaa50f2c0bd78e03eb7af7aa5e0df59b158efb2f"}, - {file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a003ac9edc22d99ae1286b0875c460351f4e101f8c9d9d2576e78d7e048f64e0"}, - {file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ef9528915df81b8f4c7612b19b8628214c65c9b7f74db2e34a646a0a2a0da2d4"}, - {file = "cffi-1.17.0-cp313-cp313-win32.whl", hash = "sha256:70d2aa9fb00cf52034feac4b913181a6e10356019b18ef89bc7c12a283bf5f5a"}, - {file = "cffi-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:b7b6ea9e36d32582cda3465f54c4b454f62f23cb083ebc7a94e2ca6ef011c3a7"}, - {file = "cffi-1.17.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:964823b2fc77b55355999ade496c54dde161c621cb1f6eac61dc30ed1b63cd4c"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:516a405f174fd3b88829eabfe4bb296ac602d6a0f68e0d64d5ac9456194a5b7e"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dec6b307ce928e8e112a6bb9921a1cb00a0e14979bf28b98e084a4b8a742bd9b"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4094c7b464cf0a858e75cd14b03509e84789abf7b79f8537e6a72152109c76e"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2404f3de742f47cb62d023f0ba7c5a916c9c653d5b368cc966382ae4e57da401"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa9d43b02a0c681f0bfbc12d476d47b2b2b6a3f9287f11ee42989a268a1833c"}, - {file = "cffi-1.17.0-cp38-cp38-win32.whl", hash = "sha256:0bb15e7acf8ab35ca8b24b90af52c8b391690ef5c4aec3d31f38f0d37d2cc499"}, - {file = "cffi-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:93a7350f6706b31f457c1457d3a3259ff9071a66f312ae64dc024f049055f72c"}, - {file = "cffi-1.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a2ddbac59dc3716bc79f27906c010406155031a1c801410f1bafff17ea304d2"}, - {file = "cffi-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6327b572f5770293fc062a7ec04160e89741e8552bf1c358d1a23eba68166759"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbc183e7bef690c9abe5ea67b7b60fdbca81aa8da43468287dae7b5c046107d4"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bdc0f1f610d067c70aa3737ed06e2726fd9d6f7bfee4a351f4c40b6831f4e82"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d872186c1617d143969defeadac5a904e6e374183e07977eedef9c07c8953bf"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d46ee4764b88b91f16661a8befc6bfb24806d885e27436fdc292ed7e6f6d058"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f76a90c345796c01d85e6332e81cab6d70de83b829cf1d9762d0a3da59c7932"}, - {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e60821d312f99d3e1569202518dddf10ae547e799d75aef3bca3a2d9e8ee693"}, - {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:eb09b82377233b902d4c3fbeeb7ad731cdab579c6c6fda1f763cd779139e47c3"}, - {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:24658baf6224d8f280e827f0a50c46ad819ec8ba380a42448e24459daf809cf4"}, - {file = "cffi-1.17.0-cp39-cp39-win32.whl", hash = "sha256:0fdacad9e0d9fc23e519efd5ea24a70348305e8d7d85ecbb1a5fa66dc834e7fb"}, - {file = "cffi-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:7cbc78dc018596315d4e7841c8c3a7ae31cc4d638c9b627f87d52e8abaaf2d29"}, - {file = "cffi-1.17.0.tar.gz", hash = "sha256:f3157624b7558b914cb039fd1af735e5e8049a87c817cc215109ad1c8779df76"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, ] [package.dependencies] @@ -272,101 +318,116 @@ pycparser = "*" [[package]] name = "charset-normalizer" -version = "3.3.2" +version = "3.4.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7.0" files = [ - {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, - {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, + {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, + {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, ] [[package]] @@ -498,13 +559,13 @@ dev = ["bandit", "coverage", "flake8", "pydocstyle", "pylint", "pytest", "pytest [[package]] name = "dill" -version = "0.3.8" +version = "0.3.9" description = "serialize all of Python" optional = false python-versions = ">=3.8" files = [ - {file = "dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"}, - {file = "dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca"}, + {file = "dill-0.3.9-py3-none-any.whl", hash = "sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a"}, + {file = "dill-0.3.9.tar.gz", hash = "sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c"}, ] [package.extras] @@ -543,15 +604,18 @@ pyflakes = ">=3.2.0,<3.3.0" [[package]] name = "idna" -version = "3.7" +version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" files = [ - {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, - {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, ] +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + [[package]] name = "inflate64" version = "1.0.0" @@ -706,6 +770,17 @@ check = ["check-manifest", "flake8", "flake8-black", "isort (>=5.0.3)", "pygment test = ["coverage[toml] (>=5.2)", "coveralls (>=2.1.1)", "hypothesis", "pyannotate", "pytest", "pytest-cov"] type = ["mypy", "mypy-extensions"] +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + [[package]] name = "packaging" version = "24.1" @@ -717,32 +792,43 @@ files = [ {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + [[package]] name = "pbr" -version = "6.0.0" +version = "6.1.0" description = "Python Build Reasonableness" optional = false python-versions = ">=2.6" files = [ - {file = "pbr-6.0.0-py2.py3-none-any.whl", hash = "sha256:4a7317d5e3b17a3dccb6a8cfe67dab65b20551404c52c8ed41279fa4f0cb4cda"}, - {file = "pbr-6.0.0.tar.gz", hash = "sha256:d1377122a5a00e2f940ee482999518efe16d745d423a670c27773dfbc3c9a7d9"}, + {file = "pbr-6.1.0-py2.py3-none-any.whl", hash = "sha256:a776ae228892d8013649c0aeccbb3d5f99ee15e005a4cbb7e61d55a067b28a2a"}, + {file = "pbr-6.1.0.tar.gz", hash = "sha256:788183e382e3d1d7707db08978239965e8b9e4e5ed42669bf4758186734d5f24"}, ] [[package]] name = "platformdirs" -version = "4.2.2" +version = "4.3.6" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, - {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, ] [package.extras] -docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] -type = ["mypy (>=1.8)"] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] [[package]] name = "pluggy" @@ -761,13 +847,13 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "prompt-toolkit" -version = "3.0.47" +version = "3.0.48" description = "Library for building powerful interactive command lines in Python" optional = false python-versions = ">=3.7.0" files = [ - {file = "prompt_toolkit-3.0.47-py3-none-any.whl", hash = "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10"}, - {file = "prompt_toolkit-3.0.47.tar.gz", hash = "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360"}, + {file = "prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e"}, + {file = "prompt_toolkit-3.0.48.tar.gz", hash = "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90"}, ] [package.dependencies] @@ -804,13 +890,13 @@ test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] [[package]] name = "py7zr" -version = "0.21.1" +version = "0.22.0" description = "Pure python 7-zip library" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "py7zr-0.21.1-py3-none-any.whl", hash = "sha256:57e5be6fafaa417fe93fa9c81f7f01bb579d3cfe1631f535a3e641200ac87dc2"}, - {file = "py7zr-0.21.1.tar.gz", hash = "sha256:dede8ed8b7b32b3586ac476da3a482b69dd433229420bf0f62c495404b72c799"}, + {file = "py7zr-0.22.0-py3-none-any.whl", hash = "sha256:993b951b313500697d71113da2681386589b7b74f12e48ba13cc12beca79d078"}, + {file = "py7zr-0.22.0.tar.gz", hash = "sha256:c6c7aea5913535184003b73938490f9a4d8418598e533f9ca991d3b8e45a139e"}, ] [package.dependencies] @@ -829,7 +915,7 @@ texttable = "*" check = ["black (>=23.1.0)", "check-manifest", "flake8 (<8)", "flake8-black (>=0.3.6)", "flake8-deprecated", "flake8-isort", "isort (>=5.0.3)", "lxml", "mypy (>=0.940)", "mypy-extensions (>=0.4.1)", "pygments", "readme-renderer", "twine", "types-psutil"] debug = ["pytest", "pytest-leaks", "pytest-profiling"] docs = ["docutils", "sphinx (>=5.0)", "sphinx-a4doc", "sphinx-py3doc-enhanced-theme"] -test = ["coverage[toml] (>=5.2)", "coveralls (>=2.1.1)", "py-cpuinfo", "pyannotate", "pytest", "pytest-benchmark", "pytest-cov", "pytest-remotedata", "pytest-timeout"] +test = ["coverage[toml] (>=5.2)", "coveralls (>=2.1.1)", "py-cpuinfo", "pytest", "pytest-benchmark", "pytest-cov", "pytest-remotedata", "pytest-timeout"] test-compat = ["libarchive-c"] [[package]] @@ -910,43 +996,43 @@ files = [ [[package]] name = "pycryptodomex" -version = "3.20.0" +version = "3.21.0" description = "Cryptographic library for Python" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ - {file = "pycryptodomex-3.20.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:645bd4ca6f543685d643dadf6a856cc382b654cc923460e3a10a49c1b3832aeb"}, - {file = "pycryptodomex-3.20.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ff5c9a67f8a4fba4aed887216e32cbc48f2a6fb2673bb10a99e43be463e15913"}, - {file = "pycryptodomex-3.20.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:8ee606964553c1a0bc74057dd8782a37d1c2bc0f01b83193b6f8bb14523b877b"}, - {file = "pycryptodomex-3.20.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7805830e0c56d88f4d491fa5ac640dfc894c5ec570d1ece6ed1546e9df2e98d6"}, - {file = "pycryptodomex-3.20.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:bc3ee1b4d97081260d92ae813a83de4d2653206967c4a0a017580f8b9548ddbc"}, - {file = "pycryptodomex-3.20.0-cp27-cp27m-win32.whl", hash = "sha256:8af1a451ff9e123d0d8bd5d5e60f8e3315c3a64f3cdd6bc853e26090e195cdc8"}, - {file = "pycryptodomex-3.20.0-cp27-cp27m-win_amd64.whl", hash = "sha256:cbe71b6712429650e3883dc81286edb94c328ffcd24849accac0a4dbcc76958a"}, - {file = "pycryptodomex-3.20.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:76bd15bb65c14900d98835fcd10f59e5e0435077431d3a394b60b15864fddd64"}, - {file = "pycryptodomex-3.20.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:653b29b0819605fe0898829c8ad6400a6ccde096146730c2da54eede9b7b8baa"}, - {file = "pycryptodomex-3.20.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62a5ec91388984909bb5398ea49ee61b68ecb579123694bffa172c3b0a107079"}, - {file = "pycryptodomex-3.20.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:108e5f1c1cd70ffce0b68739c75734437c919d2eaec8e85bffc2c8b4d2794305"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:59af01efb011b0e8b686ba7758d59cf4a8263f9ad35911bfe3f416cee4f5c08c"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:82ee7696ed8eb9a82c7037f32ba9b7c59e51dda6f105b39f043b6ef293989cb3"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91852d4480a4537d169c29a9d104dda44094c78f1f5b67bca76c29a91042b623"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca649483d5ed251d06daf25957f802e44e6bb6df2e8f218ae71968ff8f8edc4"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e186342cfcc3aafaad565cbd496060e5a614b441cacc3995ef0091115c1f6c5"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:25cd61e846aaab76d5791d006497134602a9e451e954833018161befc3b5b9ed"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:9c682436c359b5ada67e882fec34689726a09c461efd75b6ea77b2403d5665b7"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:7a7a8f33a1f1fb762ede6cc9cbab8f2a9ba13b196bfaf7bc6f0b39d2ba315a43"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-win32.whl", hash = "sha256:c39778fd0548d78917b61f03c1fa8bfda6cfcf98c767decf360945fe6f97461e"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-win_amd64.whl", hash = "sha256:2a47bcc478741b71273b917232f521fd5704ab4b25d301669879e7273d3586cc"}, - {file = "pycryptodomex-3.20.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:1be97461c439a6af4fe1cf8bf6ca5936d3db252737d2f379cc6b2e394e12a458"}, - {file = "pycryptodomex-3.20.0-pp27-pypy_73-win32.whl", hash = "sha256:19764605feea0df966445d46533729b645033f134baeb3ea26ad518c9fdf212c"}, - {file = "pycryptodomex-3.20.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f2e497413560e03421484189a6b65e33fe800d3bd75590e6d78d4dfdb7accf3b"}, - {file = "pycryptodomex-3.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e48217c7901edd95f9f097feaa0388da215ed14ce2ece803d3f300b4e694abea"}, - {file = "pycryptodomex-3.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d00fe8596e1cc46b44bf3907354e9377aa030ec4cd04afbbf6e899fc1e2a7781"}, - {file = "pycryptodomex-3.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:88afd7a3af7ddddd42c2deda43d53d3dfc016c11327d0915f90ca34ebda91499"}, - {file = "pycryptodomex-3.20.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d3584623e68a5064a04748fb6d76117a21a7cb5eaba20608a41c7d0c61721794"}, - {file = "pycryptodomex-3.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0daad007b685db36d977f9de73f61f8da2a7104e20aca3effd30752fd56f73e1"}, - {file = "pycryptodomex-3.20.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5dcac11031a71348faaed1f403a0debd56bf5404232284cf8c761ff918886ebc"}, - {file = "pycryptodomex-3.20.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:69138068268127cd605e03438312d8f271135a33140e2742b417d027a0539427"}, - {file = "pycryptodomex-3.20.0.tar.gz", hash = "sha256:7a710b79baddd65b806402e14766c721aee8fb83381769c27920f26476276c1e"}, + {file = "pycryptodomex-3.21.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:dbeb84a399373df84a69e0919c1d733b89e049752426041deeb30d68e9867822"}, + {file = "pycryptodomex-3.21.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:a192fb46c95489beba9c3f002ed7d93979423d1b2a53eab8771dbb1339eb3ddd"}, + {file = "pycryptodomex-3.21.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:1233443f19d278c72c4daae749872a4af3787a813e05c3561c73ab0c153c7b0f"}, + {file = "pycryptodomex-3.21.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbb07f88e277162b8bfca7134b34f18b400d84eac7375ce73117f865e3c80d4c"}, + {file = "pycryptodomex-3.21.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:e859e53d983b7fe18cb8f1b0e29d991a5c93be2c8dd25db7db1fe3bd3617f6f9"}, + {file = "pycryptodomex-3.21.0-cp27-cp27m-win32.whl", hash = "sha256:ef046b2e6c425647971b51424f0f88d8a2e0a2a63d3531817968c42078895c00"}, + {file = "pycryptodomex-3.21.0-cp27-cp27m-win_amd64.whl", hash = "sha256:da76ebf6650323eae7236b54b1b1f0e57c16483be6e3c1ebf901d4ada47563b6"}, + {file = "pycryptodomex-3.21.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:c07e64867a54f7e93186a55bec08a18b7302e7bee1b02fd84c6089ec215e723a"}, + {file = "pycryptodomex-3.21.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:56435c7124dd0ce0c8bdd99c52e5d183a0ca7fdcd06c5d5509423843f487dd0b"}, + {file = "pycryptodomex-3.21.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65d275e3f866cf6fe891411be9c1454fb58809ccc5de6d3770654c47197acd65"}, + {file = "pycryptodomex-3.21.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:5241bdb53bcf32a9568770a6584774b1b8109342bd033398e4ff2da052123832"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:34325b84c8b380675fd2320d0649cdcbc9cf1e0d1526edbe8fce43ed858cdc7e"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:103c133d6cd832ae7266feb0a65b69e3a5e4dbbd6f3a3ae3211a557fd653f516"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77ac2ea80bcb4b4e1c6a596734c775a1615d23e31794967416afc14852a639d3"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9aa0cf13a1a1128b3e964dc667e5fe5c6235f7d7cfb0277213f0e2a783837cc2"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:46eb1f0c8d309da63a2064c28de54e5e614ad17b7e2f88df0faef58ce192fc7b"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:cc7e111e66c274b0df5f4efa679eb31e23c7545d702333dfd2df10ab02c2a2ce"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-musllinux_1_2_i686.whl", hash = "sha256:770d630a5c46605ec83393feaa73a9635a60e55b112e1fb0c3cea84c2897aa0a"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:52e23a0a6e61691134aa8c8beba89de420602541afaae70f66e16060fdcd677e"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-win32.whl", hash = "sha256:a3d77919e6ff56d89aada1bd009b727b874d464cb0e2e3f00a49f7d2e709d76e"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-win_amd64.whl", hash = "sha256:b0e9765f93fe4890f39875e6c90c96cb341767833cfa767f41b490b506fa9ec0"}, + {file = "pycryptodomex-3.21.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:feaecdce4e5c0045e7a287de0c4351284391fe170729aa9182f6bd967631b3a8"}, + {file = "pycryptodomex-3.21.0-pp27-pypy_73-win32.whl", hash = "sha256:365aa5a66d52fd1f9e0530ea97f392c48c409c2f01ff8b9a39c73ed6f527d36c"}, + {file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3efddfc50ac0ca143364042324046800c126a1d63816d532f2e19e6f2d8c0c31"}, + {file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0df2608682db8279a9ebbaf05a72f62a321433522ed0e499bc486a6889b96bf3"}, + {file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5823d03e904ea3e53aebd6799d6b8ec63b7675b5d2f4a4bd5e3adcb512d03b37"}, + {file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:27e84eeff24250ffec32722334749ac2a57a5fd60332cd6a0680090e7c42877e"}, + {file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8ef436cdeea794015263853311f84c1ff0341b98fc7908e8a70595a68cefd971"}, + {file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a1058e6dfe827f4209c5cae466e67610bcd0d66f2f037465daa2a29d92d952b"}, + {file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ba09a5b407cbb3bcb325221e346a140605714b5e880741dc9a1e9ecf1688d42"}, + {file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8a9d8342cf22b74a746e3c6c9453cb0cfbb55943410e3a2619bd9164b48dc9d9"}, + {file = "pycryptodomex-3.21.0.tar.gz", hash = "sha256:222d0bd05381dd25c32dd6065c071ebf084212ab79bab4599ba9e6a3e0009e6c"}, ] [[package]] @@ -993,13 +1079,13 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pylint" -version = "3.2.6" +version = "3.2.7" description = "python code static checker" optional = false python-versions = ">=3.8.0" files = [ - {file = "pylint-3.2.6-py3-none-any.whl", hash = "sha256:03c8e3baa1d9fb995b12c1dbe00aa6c4bcef210c2a2634374aedeb22fb4a8f8f"}, - {file = "pylint-3.2.6.tar.gz", hash = "sha256:a5d01678349454806cff6d886fb072294f56a58c4761278c97fb557d708e1eb3"}, + {file = "pylint-3.2.7-py3-none-any.whl", hash = "sha256:02f4aedeac91be69fb3b4bea997ce580a4ac68ce58b89eaefeaf06749df73f4b"}, + {file = "pylint-3.2.7.tar.gz", hash = "sha256:1b7a721b575eaeaa7d39db076b6e7743c993ea44f57979127c517c6c572c803e"}, ] [package.dependencies] @@ -1109,13 +1195,13 @@ test = ["coverage[toml] (>=5.2)", "hypothesis", "pytest (>=6.0)", "pytest-benchm [[package]] name = "pytest" -version = "8.3.2" +version = "8.3.3" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, - {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, + {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, + {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, ] [package.dependencies] @@ -1207,100 +1293,94 @@ files = [ [[package]] name = "pyzstd" -version = "0.16.1" +version = "0.16.2" description = "Python bindings to Zstandard (zstd) compression library." optional = false python-versions = ">=3.5" files = [ - {file = "pyzstd-0.16.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0cff110d121598f9eb638ce15393fece65bb5fac9a9d38c60fc5cb1ac8631465"}, - {file = "pyzstd-0.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:acbf3d01f79be0bd284ab316e33d6a3fceab478a932ce93de7275d7d9547b9be"}, - {file = "pyzstd-0.16.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb1d26304c41cc07a87b1b85f4bf61a0f853368e0c00bb700dc7245971dedd53"}, - {file = "pyzstd-0.16.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c7507175f8d3f48358e28001a19242d3d4df819b6cd4cbc4f0fbe6f9dee9427"}, - {file = "pyzstd-0.16.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd62933e3a11f7dd6c892fa38c67e7ba45de17cae08f1355bf07b31e631a36f3"}, - {file = "pyzstd-0.16.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4725fb00bf06bd674f73f37cb168dd73ca67e68287207fece340e7425f0754d"}, - {file = "pyzstd-0.16.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9865ffbff114ad4411c9794deb1cbe57a03902f82a2671c23929a2628fd70bbc"}, - {file = "pyzstd-0.16.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:65fc3e12ad4d3ddc1f408e31ad2b70e110bbb7f835e4737f0f7b99ed1ff110cd"}, - {file = "pyzstd-0.16.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:397ca9ea892fece84fbbc5847ce46d16ee03501de3bbc6fb1f9b69bb14fe47a3"}, - {file = "pyzstd-0.16.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:83e770056823f8add3be33599219aa962c36f60eff24fa815579bc65bb053499"}, - {file = "pyzstd-0.16.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:f949a5375ca8a546059193400b2e7c70f1a10de58bd87d35bdc31c6230e47ab0"}, - {file = "pyzstd-0.16.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:55e6dd2667a58ca92924f7ced5ac2c53ed26e07c453cfbde50693bf58c4c7b5b"}, - {file = "pyzstd-0.16.1-cp310-cp310-win32.whl", hash = "sha256:c088b57288a8e1818c032ed7e3e3e573b3fe8fad698d02740a1583f55458a73f"}, - {file = "pyzstd-0.16.1-cp310-cp310-win_amd64.whl", hash = "sha256:089f3d04430b1044fccedbd4e88bd5429cd1220cf523b8841ead0127d8eedd9f"}, - {file = "pyzstd-0.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7bb570705a39e2a78619e6134a68be00ccd04398d782827180c0d1df79fc88c1"}, - {file = "pyzstd-0.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5633a0e9ac780a5577fc5dee3d6d05b8edf2f3d646ffe2c71e065d62a1b538c"}, - {file = "pyzstd-0.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61450162fb86504d16c00558976a4864ae12537e362f7346a0a79594ec2eb491"}, - {file = "pyzstd-0.16.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd3d79a74f863ec277ee3297b43f30178aa1a014eba54c286ea48f21248e525e"}, - {file = "pyzstd-0.16.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ddb50c2767ebf411f2b28e698d61d1671c87e943dac81b2a6e89529052c8ad"}, - {file = "pyzstd-0.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf0dec2978f9bc622c4daa48dd286f3f7e6ab196b1e17c46437abb6d4a968201"}, - {file = "pyzstd-0.16.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64ae91c0c19160cc0b95d33a5802e708ab15f11213f8043906d484b6062a80b3"}, - {file = "pyzstd-0.16.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9175bf699ec234189dd5549b4ededc676b66010e2eef5b3170501a17d765cf5"}, - {file = "pyzstd-0.16.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:cdedcddd851139605b0dbc9b9ed5767052f67c02fa98c66b0a0bd4c1bce0ba49"}, - {file = "pyzstd-0.16.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:efeac4bf8a12cc0a1284164e77cca85727f8a5ec20328cef2e5c72f8eabf7630"}, - {file = "pyzstd-0.16.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:b867f620b7402e0befa4b5e7eaa79693be099a52304f31bfc1006cdc915d21c7"}, - {file = "pyzstd-0.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4d9f8aa524f99f593ebf38639e6d066984b0f9ed084d45ee8877761d1ee6aa48"}, - {file = "pyzstd-0.16.1-cp311-cp311-win32.whl", hash = "sha256:a4f2f1bd58361e4994e0fed4223038554bdb61644b2449f50f8c2960a8aeffc4"}, - {file = "pyzstd-0.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:81567ffe7f5ba6d6612399a82191448ba4f7780c96f2643bea36403a49462e0b"}, - {file = "pyzstd-0.16.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:bb26734a5cda4b5e58b33c5fe20aee697fb9ad8dd72999bc71d7df09783f44db"}, - {file = "pyzstd-0.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b73e9d8ae8eca8dd600d54408584b625503761ad6b0e481e47e270a19e968141"}, - {file = "pyzstd-0.16.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b8af1f24361728cb0abeb447204015b2af016bfaf61d55b7c7bc44edc50348b"}, - {file = "pyzstd-0.16.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5faf5894b58f38491ecb458e6f4032ae0bbebea64dfeff86abc6c6176829ac3"}, - {file = "pyzstd-0.16.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:748ea21376016b77f93eb6e5d3fdf158620a27d36d2a05cb319f3e7b8b1943a5"}, - {file = "pyzstd-0.16.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb49c7854c6c56d9d41abdcd970b5fec2681a6a74f390b6f8f8fe9d1ca1f8530"}, - {file = "pyzstd-0.16.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68ea4cbeb5fa722222e8607ed22eab7723dfe8f502cbdaaab0989fc47f2fe4e6"}, - {file = "pyzstd-0.16.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c78ca31b0d83f77ab6ff041808304f51672f925683ffc3a1a866469f1678fc10"}, - {file = "pyzstd-0.16.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:deea37b1618f31fd2618be0aad42bb5bafcdddc24df9fc18c71071314239e3a2"}, - {file = "pyzstd-0.16.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aadbab6d6b79bd37697c3de28d4c2cbac3545ae9622be2f86ae5e426c6e1b192"}, - {file = "pyzstd-0.16.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3b23295a6aedc71e5318b7e490f2ed1ea3fda6b31f2b5957c8da49a5aac7aa81"}, - {file = "pyzstd-0.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f0a685bea6ba4e965d0de77cda3e380efeb144bb4fa0eff362626b4cdec71814"}, - {file = "pyzstd-0.16.1-cp312-cp312-win32.whl", hash = "sha256:ad8686ae57a59432860907e4c62d4b08b98d2330a129928145d797eda118da7b"}, - {file = "pyzstd-0.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:53ae4ac03c286896b2a6741c9069afd80e432526d267f900420d8083f8ab1f78"}, - {file = "pyzstd-0.16.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:96c04f3ef21f8c84672468358001b1f78b18f62a1b6af202e9fe0c71d0cd85f8"}, - {file = "pyzstd-0.16.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f3b74f42ac91dfcd5b3e8dfa691714e23c4bb3931070fdc134dbbaa2c92c51e"}, - {file = "pyzstd-0.16.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cba92b21b12bff45c0393e022ca4e6029aa5d4d3f11d1d9f05ca9a13245d325"}, - {file = "pyzstd-0.16.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:036d76e26300bc03cf05108a019fb0dd0a40ee6ed40128ead1c953fc603fba68"}, - {file = "pyzstd-0.16.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cb00ce5e9a88e27f27db3ff4f4c6080c4158ad848d620b68d48bbc413d99f0ef"}, - {file = "pyzstd-0.16.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f7b5d93b5e7d3b3bd4a0f665b2bfab61a9cc78cb19b4f9d2faa454ae19133e"}, - {file = "pyzstd-0.16.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a426a253413ede9dad34fffde2d533950aa6aab82d0e9c7c7660168e323c43dc"}, - {file = "pyzstd-0.16.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:3fcf498488cf2a866142a35d0c14c021a58c7d96b25bafd13c72676458912011"}, - {file = "pyzstd-0.16.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:2325ff41ff4bea19065894244c4dade5ae6b40df6e9def9dd4bc6e4c81edabf1"}, - {file = "pyzstd-0.16.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:593a4ec2f639a80523c6d8cb6a3f97899a4b3db4eadb768039dbd61fed4fe675"}, - {file = "pyzstd-0.16.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:13ab3b66c660438cf9031543a1cb9a4c7adde6b58b65e05783d32044178e871c"}, - {file = "pyzstd-0.16.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:15a242d03c1516e1325d41a43b05c95abce0306d6f9782408b44f6225fadea9b"}, - {file = "pyzstd-0.16.1-cp38-cp38-win32.whl", hash = "sha256:763e084e0a7273d81d4bd68c4c89d642e3a447e30d1108d3dc0d0ec07a3ad01c"}, - {file = "pyzstd-0.16.1-cp38-cp38-win_amd64.whl", hash = "sha256:8b54ea942847b6e2f842f8b524f0c4dcc199f99b39420e06262cbcf25cb24363"}, - {file = "pyzstd-0.16.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2595819277b46d195565861f2966f58908444c7787da1ec45ea56390650013a6"}, - {file = "pyzstd-0.16.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f42bb898d5335e91d4575758cb11f68308756061d1eff042c7c4daa09cc560ba"}, - {file = "pyzstd-0.16.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffa579210ae03a0aeeff86d492ff26acd358ec1daea8553beaac5f1ba774991d"}, - {file = "pyzstd-0.16.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:234423432d2e66328bdb06121aad3477bb97e200141a863aba0d1a14ff30b0cb"}, - {file = "pyzstd-0.16.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84059dfa917a2704e04776f26d5105bebc5019fc4f13379b44e71e57b575fc28"}, - {file = "pyzstd-0.16.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c870947722ae4c7de8e2d259690041f8b3332b1d75b4c3ca2caf17b170d10be3"}, - {file = "pyzstd-0.16.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3630a47b7d418e65521a45afbea5d77a825e4fb675fdf884eff42e6ce3230f91"}, - {file = "pyzstd-0.16.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:070434349fdd2fd56207a82a146c89a50811c5e0f767ac00d09f513919335f6f"}, - {file = "pyzstd-0.16.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:59d016a105efd11db4305d43712ca2aab5e4f7dc73f42cc6324bc8f1b0ce2402"}, - {file = "pyzstd-0.16.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb2e62ed3d04fed425e009e9948c5e1478665475c5a6ca52d9f02295db7cffb1"}, - {file = "pyzstd-0.16.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:1f00c7d40304329fbebbe9891cd2b144b09844876fe65a8bcfef71d80d417214"}, - {file = "pyzstd-0.16.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:28b33701e0a5bdb7aa96229ef7f680442894a4be3dfb39daf2fbae805778ade7"}, - {file = "pyzstd-0.16.1-cp39-cp39-win32.whl", hash = "sha256:7cdc3c293ab30ea141789a4454a4fd7b7858e005f6d2f61113d239a20d9bafd4"}, - {file = "pyzstd-0.16.1-cp39-cp39-win_amd64.whl", hash = "sha256:f6a7996f56abc23ad96bb73aea363720a1fca91a99822f8267bb5d3c4b7af7dc"}, - {file = "pyzstd-0.16.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cf08a0fa9af8d690a41b9b7db6b8ae174ba2ac42b5463993c2cd3d144a094644"}, - {file = "pyzstd-0.16.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:65683cb63d631b159e02738376987c26106b37a1345105c52067441e6259cf87"}, - {file = "pyzstd-0.16.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc09abfd5e281dba33a1cfdc653ece69fc239ad2c6cebd99506facbcb2669c91"}, - {file = "pyzstd-0.16.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46feda6257df4cde7dda55811851c2096dea7b38dcd601099acb95d7acdc795f"}, - {file = "pyzstd-0.16.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca952ce3388b5f7ee78931733ec41c8939482b466882e41d79a9a8c1387dd398"}, - {file = "pyzstd-0.16.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dc0e4d4c832adcd3c25a5d5b5bf0aa05bc25a279b8e8356eb2b95975b2a67fa0"}, - {file = "pyzstd-0.16.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ef5943a471b0d51cdb4eb05187b4be81cd6c95349e73818c4b959f60a05dfccd"}, - {file = "pyzstd-0.16.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:2df7e255b4aef73d7f8b11301bb6e39cf43e46cf80aa885ff7c1570565cf2398"}, - {file = "pyzstd-0.16.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a456ca431e4968a31c350004eca7957490f51245be8f3b44e49a9f143251312"}, - {file = "pyzstd-0.16.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1751fabc263654b3b4fbfb2729f63d6b3a51bf498bfbb1851ed332cd1b9a02e8"}, - {file = "pyzstd-0.16.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b1ce3eae59fd7093a05b8f073c7dce4795cccbf5987371fda5931b38fa9a567"}, - {file = "pyzstd-0.16.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:bc6326d017c618e7897c2f529dc71100403c0dfdbc523cd6c62f6ba1ed9f23f1"}, - {file = "pyzstd-0.16.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:487efbe3da2b879c5835e0d762bc8ea69e6bd765d31d6de32b20146bc7f5b2cc"}, - {file = "pyzstd-0.16.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:4ae871967fc080a24118135dd8465339cf69c990fdea8755aef8806c5ebfb0e3"}, - {file = "pyzstd-0.16.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6467ba4ccbc1e09793e763c602079bb5b95813dcb2b0d2afffb40130b5927e69"}, - {file = "pyzstd-0.16.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1274d766f8a2655f99bd8f2ebc8f109ccf640734e941ca484ef03e275441e220"}, - {file = "pyzstd-0.16.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd568900f5ce7e2ced7928342b7cbc234c2b5648cff6a84bbf5e713377fce4f5"}, - {file = "pyzstd-0.16.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:123aba9d2bfdc1840b1fadd386c0095130948c10cd5a4f0acc48368d61448c9e"}, - {file = "pyzstd-0.16.1.tar.gz", hash = "sha256:ed50c08233878c155c73ab2622e115cd9e46c0f1c2e2ddd76f2e7ca24933f195"}, + {file = "pyzstd-0.16.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:637376c8f8cbd0afe1cab613f8c75fd502bd1016bf79d10760a2d5a00905fe62"}, + {file = "pyzstd-0.16.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3e7a7118cbcfa90ca2ddbf9890c7cb582052a9a8cf2b7e2c1bbaf544bee0f16a"}, + {file = "pyzstd-0.16.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a74cb1ba05876179525144511eed3bd5a509b0ab2b10632c1215a85db0834dfd"}, + {file = "pyzstd-0.16.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c084dde218ffbf112e507e72cbf626b8f58ce9eb23eec129809e31037984662"}, + {file = "pyzstd-0.16.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4646459ebd3d7a59ddbe9312f020bcf7cdd1f059a2ea07051258f7af87a0b31"}, + {file = "pyzstd-0.16.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14bfc2833cc16d7657fc93259edeeaa793286e5031b86ca5dc861ba49b435fce"}, + {file = "pyzstd-0.16.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f27d488f19e5bf27d1e8aa1ae72c6c0a910f1e1ffbdf3c763d02ab781295dd27"}, + {file = "pyzstd-0.16.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:91e134ca968ff7dcfa8b7d433318f01d309b74ee87e0d2bcadc117c08e1c80db"}, + {file = "pyzstd-0.16.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6b5f64cd3963c58b8f886eb6139bb8d164b42a74f8a1bb95d49b4804f4592d61"}, + {file = "pyzstd-0.16.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0b4a8266871b9e0407f9fd8e8d077c3558cf124d174e6357b523d14f76971009"}, + {file = "pyzstd-0.16.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1bb19f7acac30727354c25125922aa59f44d82e0e6a751df17d0d93ff6a73853"}, + {file = "pyzstd-0.16.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3008325b7368e794d66d4d98f2ee1d867ef5afd09fd388646ae02b25343c420d"}, + {file = "pyzstd-0.16.2-cp310-cp310-win32.whl", hash = "sha256:66f2d5c0bbf5bf32c577aa006197b3525b80b59804450e2c32fbcc2d16e850fd"}, + {file = "pyzstd-0.16.2-cp310-cp310-win_amd64.whl", hash = "sha256:5fe5f5459ebe1161095baa7a86d04ab625b35148f6c425df0347ed6c90a2fd58"}, + {file = "pyzstd-0.16.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1c1bdbe7f01c7f37d5cd07be70e32a84010d7dfd6677920c0de04cf7d245b60d"}, + {file = "pyzstd-0.16.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1882a3ceaaf9adc12212d587d150ec5e58cfa9a765463d803d739abbd3ac0f7a"}, + {file = "pyzstd-0.16.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea46a8b9d60f6a6eba29facba54c0f0d70328586f7ef0da6f57edf7e43db0303"}, + {file = "pyzstd-0.16.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d7865bc06589cdcecdede0deefe3da07809d5b7ad9044c224d7b2a0867256957"}, + {file = "pyzstd-0.16.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:52f938a65b409c02eb825e8c77fc5ea54508b8fc44b5ce226db03011691ae8cc"}, + {file = "pyzstd-0.16.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e97620d3f53a0282947304189deef7ca7f7d0d6dfe15033469dc1c33e779d5e5"}, + {file = "pyzstd-0.16.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7c40e9983d017108670dc8df68ceef14c7c1cf2d19239213274783041d0e64c"}, + {file = "pyzstd-0.16.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7cd4b3b2c6161066e4bde6af1cf78ed3acf5d731884dd13fdf31f1db10830080"}, + {file = "pyzstd-0.16.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:454f31fd84175bb203c8c424f2255a343fa9bd103461a38d1bf50487c3b89508"}, + {file = "pyzstd-0.16.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:5ef754a93743f08fb0386ce3596780bfba829311b49c8f4107af1a4bcc16935d"}, + {file = "pyzstd-0.16.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:be81081db9166e10846934f0e3576a263cbe18d81eca06e6a5c23533f8ce0dc6"}, + {file = "pyzstd-0.16.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:738bcb2fa1e5f1868986f5030955e64de53157fa1141d01f3a4daf07a1aaf644"}, + {file = "pyzstd-0.16.2-cp311-cp311-win32.whl", hash = "sha256:0ea214c9b97046867d1657d55979021028d583704b30c481a9c165191b08d707"}, + {file = "pyzstd-0.16.2-cp311-cp311-win_amd64.whl", hash = "sha256:c17c0fc02f0e75b0c7cd21f8eaf4c6ce4112333b447d93da1773a5f705b2c178"}, + {file = "pyzstd-0.16.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4081fd841a9efe9ded7290ee7502dbf042c4158b90edfadea3b8a072c8ec4e1"}, + {file = "pyzstd-0.16.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fd3fa45d2aeb65367dd702806b2e779d13f1a3fa2d13d5ec777cfd09de6822de"}, + {file = "pyzstd-0.16.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8b5f0d2c07994a5180d8259d51df6227a57098774bb0618423d7eb4a7303467"}, + {file = "pyzstd-0.16.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60c9d25b15c7ae06ed5d516d096a0d8254f9bed4368b370a09cccf191eaab5cb"}, + {file = "pyzstd-0.16.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29acf31ce37254f6cad08deb24b9d9ba954f426fa08f8fae4ab4fdc51a03f4ae"}, + {file = "pyzstd-0.16.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ec77612a17697a9f7cf6634ffcee616eba9b997712fdd896e77fd19ab3a0618"}, + {file = "pyzstd-0.16.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:313ea4974be93be12c9a640ab40f0fc50a023178aae004a8901507b74f190173"}, + {file = "pyzstd-0.16.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e91acdefc8c2c6c3b8d5b1b5fe837dce4e591ecb7c0a2a50186f552e57d11203"}, + {file = "pyzstd-0.16.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:929bd91a403539e72b5b5cb97f725ac4acafe692ccf52f075e20cd9bf6e5493d"}, + {file = "pyzstd-0.16.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:740837a379aa32d110911ebcbbc524f9a9b145355737527543a884bd8777ca4f"}, + {file = "pyzstd-0.16.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:adfc0e80dd157e6d1e0b0112c8ecc4b58a7a23760bd9623d74122ef637cfbdb6"}, + {file = "pyzstd-0.16.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:79b183beae1c080ad3dca39019e49b7785391947f9aab68893ad85d27828c6e7"}, + {file = "pyzstd-0.16.2-cp312-cp312-win32.whl", hash = "sha256:b8d00631a3c466bc313847fab2a01f6b73b3165de0886fb03210e08567ae3a89"}, + {file = "pyzstd-0.16.2-cp312-cp312-win_amd64.whl", hash = "sha256:c0d43764e9a60607f35d8cb3e60df772a678935ab0e02e2804d4147377f4942c"}, + {file = "pyzstd-0.16.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3ae9ae7ad730562810912d7ecaf1fff5eaf4c726f4b4dfe04784ed5f06d7b91f"}, + {file = "pyzstd-0.16.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2ce8d3c213f76a564420f3d0137066ac007ce9fb4e156b989835caef12b367a7"}, + {file = "pyzstd-0.16.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2c14dac23c865e2d78cebd9087e148674b7154f633afd4709b4cd1520b99a61"}, + {file = "pyzstd-0.16.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4527969d66a943e36ef374eda847e918077de032d58b5df84d98ffd717b6fa77"}, + {file = "pyzstd-0.16.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd8256149b88e657e99f31e6d4b114c8ff2935951f1d8bb8e1fe501b224999c0"}, + {file = "pyzstd-0.16.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5bd1f1822d65c9054bf36d35307bf8ed4aa2d2d6827431761a813628ff671b1d"}, + {file = "pyzstd-0.16.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6733f4d373ec9ad2c1976cf06f973a3324c1f9abe236d114d6bb91165a397d"}, + {file = "pyzstd-0.16.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7bec165ab6524663f00b69bfefd13a46a69fed3015754abaf81b103ec73d92c6"}, + {file = "pyzstd-0.16.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e4460fa6949aac6528a1ad0de8871079600b12b3ef4db49316306786a3598321"}, + {file = "pyzstd-0.16.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:75df79ea0315c97d88337953a17daa44023dbf6389f8151903d371513f503e3c"}, + {file = "pyzstd-0.16.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:93e1d45f4a196afb6f18682c79bdd5399277ead105b67f30b35c04c207966071"}, + {file = "pyzstd-0.16.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:075e18b871f38a503b5d23e40a661adfc750bd4bd0bb8b208c1e290f3ceb8fa2"}, + {file = "pyzstd-0.16.2-cp313-cp313-win32.whl", hash = "sha256:9e4295eb299f8d87e3487852bca033d30332033272a801ca8130e934475e07a9"}, + {file = "pyzstd-0.16.2-cp313-cp313-win_amd64.whl", hash = "sha256:18deedc70f858f4cf574e59f305d2a0678e54db2751a33dba9f481f91bc71c28"}, + {file = "pyzstd-0.16.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a9892b707ef52f599098b1e9528df0e7849c5ec01d3e8035fb0e67de4b464839"}, + {file = "pyzstd-0.16.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4fbd647864341f3c174c4a6d7f20e6ea6b4be9d840fb900dc0faf0849561badc"}, + {file = "pyzstd-0.16.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20ac2c15656cc6194c4fed1cb0e8159f9394d4ea1d58be755448743d2ec6c9c4"}, + {file = "pyzstd-0.16.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b239fb9a20c1be3374b9a2bd183ba624fd22ad7a3f67738c0d80cda68b4ae1d3"}, + {file = "pyzstd-0.16.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc52400412cdae2635e0978b8d6bcc0028cc638fdab2fd301f6d157675d26896"}, + {file = "pyzstd-0.16.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b766a6aeb8dbb6c46e622e7a1aebfa9ab03838528273796941005a5ce7257b1"}, + {file = "pyzstd-0.16.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd4b8676052f9d59579242bf3cfe5fd02532b6a9a93ab7737c118ae3b8509dc"}, + {file = "pyzstd-0.16.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1c6c0a677aac7c0e3d2d2605d4d68ffa9893fdeeb2e071040eb7c8750969d463"}, + {file = "pyzstd-0.16.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:15f9c2d612e7e2023d68d321d1b479846751f792af89141931d44e82ae391394"}, + {file = "pyzstd-0.16.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:11740bff847aad23beef4085a1bb767d101895881fe891f0a911aa27d43c372c"}, + {file = "pyzstd-0.16.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:b9067483ebe860e4130a03ee665b3d7be4ec1608b208e645d5e7eb3492379464"}, + {file = "pyzstd-0.16.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:988f0ba19b14c2fe0afefc444ac1edfb2f497b7d7c3212b2f587504cc2ec804e"}, + {file = "pyzstd-0.16.2-cp39-cp39-win32.whl", hash = "sha256:8855acb1c3e3829030b9e9e9973b19e2d70f33efb14ad5c474b4d086864c959c"}, + {file = "pyzstd-0.16.2-cp39-cp39-win_amd64.whl", hash = "sha256:018e88378df5e76f5e1d8cf4416576603b6bc4a103cbc66bb593eaac54c758de"}, + {file = "pyzstd-0.16.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4b631117b97a42ff6dfd0ffc885a92fff462d7c34766b28383c57b996f863338"}, + {file = "pyzstd-0.16.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:56493a3fbe1b651a02102dd0902b0aa2377a732ff3544fb6fb3f114ca18db52f"}, + {file = "pyzstd-0.16.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1eae9bdba4a1e5d3181331f403114ff5b8ce0f4b569f48eba2b9beb2deef1e4"}, + {file = "pyzstd-0.16.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1be6972391c8aeecc7e61feb96ffc8e77a401bcba6ed994e7171330c45a1948"}, + {file = "pyzstd-0.16.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:761439d687e3a5687c2ff5c6a1190e1601362a4a3e8c6c82ff89719d51d73e19"}, + {file = "pyzstd-0.16.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f5fbdb8cf31b60b2dc586fecb9b73e2f172c21a0b320ed275f7b8d8a866d9003"}, + {file = "pyzstd-0.16.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:183f26e34f9becf0f2db38be9c0bfb136753d228bcb47c06c69175901bea7776"}, + {file = "pyzstd-0.16.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:88318b64b5205a67748148d6d244097fa6cf61fcea02ad3435511b9e7155ae16"}, + {file = "pyzstd-0.16.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73142aa2571b6480136a1865ebda8257e09eabbc8bcd54b222202f6fa4febe1e"}, + {file = "pyzstd-0.16.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d3f8877c29a97f1b1bba16f3d3ab01ad10ad3da7bad317aecf36aaf8848b37c"}, + {file = "pyzstd-0.16.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1f25754562473ac7de856b8331ebd5964f5d85601045627a5f0bb0e4e899990"}, + {file = "pyzstd-0.16.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:6ce17e84310080c55c02827ad9bb17893c00a845c8386a328b346f814aabd2c1"}, + {file = "pyzstd-0.16.2.tar.gz", hash = "sha256:179c1a2ea1565abf09c5f2fd72f9ce7c54b2764cf7369e05c0bfd8f1f67f63d2"}, ] [[package]] @@ -1326,39 +1406,23 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "rich" -version = "13.7.1" +version = "13.9.2" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false -python-versions = ">=3.7.0" +python-versions = ">=3.8.0" files = [ - {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, - {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, + {file = "rich-13.9.2-py3-none-any.whl", hash = "sha256:8c82a3d3f8dcfe9e734771313e606b39d8247bb6b826e196f4914b333b743cf1"}, + {file = "rich-13.9.2.tar.gz", hash = "sha256:51a2c62057461aaf7152b4d611168f93a9fc73068f8ded2790f29fe2b5366d0c"}, ] [package.dependencies] markdown-it-py = ">=2.2.0" pygments = ">=2.13.0,<3.0.0" -typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] -[[package]] -name = "setuptools" -version = "69.5.1" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "setuptools-69.5.1-py3-none-any.whl", hash = "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32"}, - {file = "setuptools-69.5.1.tar.gz", hash = "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] - [[package]] name = "six" version = "1.16.0" @@ -1383,17 +1447,17 @@ files = [ [[package]] name = "stevedore" -version = "5.2.0" +version = "5.3.0" description = "Manage dynamic plugins for Python applications" optional = false python-versions = ">=3.8" files = [ - {file = "stevedore-5.2.0-py3-none-any.whl", hash = "sha256:1c15d95766ca0569cad14cb6272d4d31dae66b011a929d7c18219c176ea1b5c9"}, - {file = "stevedore-5.2.0.tar.gz", hash = "sha256:46b93ca40e1114cea93d738a6c1e365396981bb6bb78c27045b7587c9473544d"}, + {file = "stevedore-5.3.0-py3-none-any.whl", hash = "sha256:1efd34ca08f474dad08d9b19e934a22c68bb6fe416926479ba29e5013bcc8f78"}, + {file = "stevedore-5.3.0.tar.gz", hash = "sha256:9a64265f4060312828151c204efbe9b7a9852a0d9228756344dbc7e4023e375a"}, ] [package.dependencies] -pbr = ">=2.0.0,<2.1.0 || >2.1.0" +pbr = ">=2.0.0" [[package]] name = "tabulate" @@ -1433,24 +1497,24 @@ files = [ [[package]] name = "tomli" -version = "2.0.1" +version = "2.0.2" description = "A lil' TOML parser" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, + {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, + {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, ] [[package]] name = "tomlkit" -version = "0.13.0" +version = "0.13.2" description = "Style preserving TOML library" optional = false python-versions = ">=3.8" files = [ - {file = "tomlkit-0.13.0-py3-none-any.whl", hash = "sha256:7075d3042d03b80f603482d69bf0c8f345c2b30e41699fd8883227f89972b264"}, - {file = "tomlkit-0.13.0.tar.gz", hash = "sha256:08ad192699734149f5b97b45f1f18dad7eb1b6d16bc72ad0c2335772650d7b72"}, + {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, + {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, ] [[package]] @@ -1466,13 +1530,13 @@ files = [ [[package]] name = "urllib3" -version = "2.2.2" +version = "2.2.3" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ - {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, - {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, + {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, + {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, ] [package.extras] @@ -1495,4 +1559,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.8.2" -content-hash = "c61a7e0844f6842124930c7ecbb6674e6026438f89301d766314ceadb166ff1d" +content-hash = "a8de05f42ce2a83d4a0280de1cab63a8e752646e2854cdc8047fce01548a9d0c" diff --git a/pyproject.toml b/pyproject.toml index 3455cc4..d945b54 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,30 +1,31 @@ [tool.poetry] name = "caracara" -version = "0.8.0" +version = "0.9.0" description = "The CrowdStrike Falcon Developer Toolkit" authors = [ "CrowdStrike " ] readme = "README.md" [tool.poetry.dependencies] python = "^3.8.2" -py7zr = ">=0.20,<0.22" +py7zr = "^0.22.0" crowdstrike-falconpy = "^1.4.0" -caracara-filters = "^0.2" -setuptools = "^69.0" +caracara-filters = "^1.0.0" -[tool.poetry.dev-dependencies] -bandit = "^1.7.7" -coverage = "^7.4.0" -flake8 = "^7.0.0" +[tool.poetry.group.dev.dependencies] +bandit = "^1.7.9" +coverage = "^7.6" +flake8 = "^7.1.0" pydocstyle = "^6.3.0" -pylint = "^3.0.3" -pytest = "^8.0.1" +pylint = "^3.2.7" +pytest = "^8.3.3" PyYAML = "^6.0.1" toml = "^0.10.2" tabulate = "^0.9.0" click = "^8.1.3" prompt-toolkit = "^3.0.47" python-dateutil = "^2.8.2" +black = "^24.8.0" +isort = "^5.13.2" [build-system] build-backend = "poetry.core.masonry.api" @@ -86,3 +87,6 @@ get-maintenance-token = "examples.sensor_update_policies.get_maintenance_token:g # Users describe-roles = "examples.users.describe_roles:describe_roles" describe-users = "examples.users.describe_users:describe_users" + +[tool.isort] +profile = "black" diff --git a/tests/integration_tests/test_general.py b/tests/integration_tests/test_general.py index 4fb128b..3044bc8 100644 --- a/tests/integration_tests/test_general.py +++ b/tests/integration_tests/test_general.py @@ -1,14 +1,12 @@ """ Caracara general tests """ + import os from falconpy import BaseURL -from caracara import ( - __version__, - Client, -) +from caracara import Client, __version__ AUTH = Client( client_id=os.getenv("FALCON_CLIENT_ID"), @@ -19,7 +17,7 @@ def test_version(): """Assert that the reflective version loading code works""" - assert __version__ == '0.1.2' + assert __version__ == "0.1.2" def cloud_validation_testing(): diff --git a/tests/integration_tests/test_hosts.py b/tests/integration_tests/test_hosts.py index 2873cbc..2baf63f 100644 --- a/tests/integration_tests/test_hosts.py +++ b/tests/integration_tests/test_hosts.py @@ -4,6 +4,7 @@ Hosts: READ, WRITE Host Group: READ, WRITE """ + from .test_general import AUTH HOST_TARGET_FILTER = "falconpy" @@ -32,6 +33,7 @@ def test_describe_network_address_history(): # def test_get_device_id(): # assert bool(AUTH.hosts.get_device_ids(filters="hostname:'falconpy'")) + def test_contain_host(): """Attempts to network contain hosts based on a hostname filter""" assert bool(AUTH.hosts.contain(filters=f"hostname:'{HOST_TARGET_FILTER}'")[0]["id"]) @@ -72,7 +74,7 @@ def test_tag_host(): assert bool( AUTH.hosts.tag( filters=f"hostname:'{HOST_TARGET_FILTER}'", - tags="FalconGroupingTags/unittesttag" + tags="FalconGroupingTags/unittesttag", )[0]["updated"] ) @@ -82,7 +84,7 @@ def test_tag_host_list(): assert bool( AUTH.hosts.tag( filters=f"hostname:'{HOST_TARGET_FILTER}'", - tags=["FalconGroupingTags/unittesttaglist"] + tags=["FalconGroupingTags/unittesttaglist"], )[0]["updated"] ) @@ -92,7 +94,7 @@ def test_tag_host_delimit(): assert bool( AUTH.hosts.tag( filters=f"hostname:'{HOST_TARGET_FILTER}'", - tags="FalconGroupingTags/unittesttagdelimit,FalconGroupingTags/unittesttagdelimit2" + tags="FalconGroupingTags/unittesttagdelimit,FalconGroupingTags/unittesttagdelimit2", )[0]["updated"] ) @@ -106,7 +108,7 @@ def test_untag_host(): "FalconGroupingTags/unittesttag", "FalconGroupingTags/unittesttaglist", "FalconGroupingTags/unittesttagdelimit", - "FalconGroupingTags/unittesttagdelimit2" - ] + "FalconGroupingTags/unittesttagdelimit2", + ], )[0]["updated"] ) diff --git a/tests/unit_tests/test_custom_ioas.py b/tests/unit_tests/test_custom_ioas.py index 2f972d5..639ce9e 100644 --- a/tests/unit_tests/test_custom_ioas.py +++ b/tests/unit_tests/test_custom_ioas.py @@ -1,4 +1,5 @@ """Unit tests for CustomIoaApiModule""" + import copy from typing import List from unittest.mock import MagicMock @@ -8,8 +9,8 @@ from caracara import Client from caracara.modules.custom_ioa import IoaRuleGroup -from caracara.modules.custom_ioa.rules import CustomIoaRule from caracara.modules.custom_ioa.rule_types import RuleType +from caracara.modules.custom_ioa.rules import CustomIoaRule # We have to disable redefined-outer-name, as pytest fixtures break this linting check by design. # pylint: disable=redefined-outer-name @@ -60,6 +61,7 @@ def simple_rule_type(): def create_mock_create_rule_group(assigned_id: str): """Creates a mock function for `create_rule_group` which assigns the given id""" + def mock_create_rule_group(body): new_body = { "customer_id": "test_customer", @@ -80,6 +82,7 @@ def mock_create_rule_group(body): "comment": body["comment"], } return {"body": {"resources": [new_body]}} + return mock_create_rule_group @@ -116,6 +119,7 @@ def mock_create_rule(body, comment=None): "comment": body["comment"] if comment is None else comment, } return {"body": {"resources": [new_body]}} + return mock_create_rule @@ -128,18 +132,24 @@ def create_mock_get_rule_types(rule_types): def create_mock_query_resources(resources): """Creates a generic mock to fetch Style 1 paginated resources""" + def mock_resources(limit, offset): - return {"body": { - "meta": {"pagination": {"total": len(resources)}}, - "resources": resources[offset:offset+limit], - }} + return { + "body": { + "meta": {"pagination": {"total": len(resources)}}, + "resources": resources[offset : offset + limit], + } + } + return mock_resources def create_mock_get_resources(resource_map): """Creates a generic get resources by id given a map""" + def mock_get_resources(ids): return {"body": {"resources": [resource_map[id_] for id_ in ids]}} + return mock_get_resources @@ -154,19 +164,23 @@ def test_create_rule_group_no_rules(client: Client, custom_ioa_api: falconpy.Cus # Mock functions custom_ioa_api.create_rule_group.side_effect = create_mock_create_rule_group( - assigned_id="test_rule_group") + assigned_id="test_rule_group" + ) # Call caracara function new_group = client.custom_ioas.create_rule_group( - group=group, comment="rule group creation test") + group=group, comment="rule group creation test" + ) # Assert falconpy called correctly - custom_ioa_api.create_rule_group.assert_called_once_with(body={ - "name": "test rule group", - "description": "test description", - "platform": "windows", - "comment": "rule group creation test", - }) + custom_ioa_api.create_rule_group.assert_called_once_with( + body={ + "name": "test rule group", + "description": "test description", + "platform": "windows", + "comment": "rule group creation test", + } + ) # Assert returned group is as expected assert group.name == new_group.name assert group.description == new_group.description @@ -175,7 +189,8 @@ def test_create_rule_group_no_rules(client: Client, custom_ioa_api: falconpy.Cus def test_create_rule_group_with_rules( - client: Client, custom_ioa_api: falconpy.CustomIOA, simple_rule_type: RuleType): + client: Client, custom_ioa_api: falconpy.CustomIOA, simple_rule_type: RuleType +): """Tests `CustomIoaApiModule.create_rule_group` on a group with rules""" # Setup group = IoaRuleGroup( @@ -194,25 +209,32 @@ def test_create_rule_group_with_rules( # Mock functions custom_ioa_api.create_rule_group.side_effect = create_mock_create_rule_group( - assigned_id="test_rule_group") + assigned_id="test_rule_group" + ) custom_ioa_api.create_rule.side_effect = create_mock_create_rule( - assigned_id="test_rule", rule_types=[simple_rule_type]) + assigned_id="test_rule", rule_types=[simple_rule_type] + ) custom_ioa_api.query_rule_types.side_effect = create_mock_query_resources( - resources=[simple_rule_type.id_]) + resources=[simple_rule_type.id_] + ) custom_ioa_api.get_rule_types.side_effect = create_mock_get_rule_types( - rule_types=[simple_rule_type]) + rule_types=[simple_rule_type] + ) # Call caracara function new_group = client.custom_ioas.create_rule_group( - group=group, comment="Rule group creation test") + group=group, comment="Rule group creation test" + ) # Assert falconpy called correctly - custom_ioa_api.create_rule_group.assert_called_once_with(body={ - "name": "test rule group name", - "description": "test rule group desc", - "platform": "windows", - "comment": "Rule group creation test", - }) + custom_ioa_api.create_rule_group.assert_called_once_with( + body={ + "name": "test rule group name", + "description": "test rule group desc", + "platform": "windows", + "comment": "Rule group creation test", + } + ) custom_ioa_api.create_rule.assert_called_once_with( body={ "name": "test rule name", @@ -275,9 +297,7 @@ def mock_query_rule_groups_full(offset, limit, filter): custom_ioa_api.query_rule_groups_full.side_effect = mock_query_rule_groups_full # Call caracara - groups = client.custom_ioas.describe_rule_groups( - filters="test_filter" - ) + groups = client.custom_ioas.describe_rule_groups(filters="test_filter") assert len(mock_groups) == len(groups) for mock_group in mock_groups: @@ -286,7 +306,8 @@ def mock_query_rule_groups_full(offset, limit, filter): def test_describe_rule_groups_with_rules( - client: Client, custom_ioa_api: falconpy.CustomIOA, simple_rule_type: RuleType): + client: Client, custom_ioa_api: falconpy.CustomIOA, simple_rule_type: RuleType +): """Tests `CustomIoaApiModule.describe_rule_groups""" # Setup mock_groups = [ @@ -299,30 +320,32 @@ def test_describe_rule_groups_with_rules( "enabled": False, "deleted": False, "rule_ids": ["test_rule_01"], - "rules": [{ - "customer_id": "test_customer", - "instance_id": "test_rule_01", - "name": "test rule", - "description": "test rule desc", - "pattern_id": "41000", - "pattern_severity": "critical", - "disposition_id": list(simple_rule_type.disposition_map.keys())[0], - "action_label": list(simple_rule_type.disposition_map.values())[0], - "ruletype_id": simple_rule_type.id_, - "ruletype_name": simple_rule_type.name, - "field_values": [], - "enabled": True, - "deleted": False, - "instance_version": 1, - "version_ids": [1], - "magic_cookie": 1, - "committed_on": "2022-01-01T12:00:00.000000000Z", - "created_on": "2022-01-01T12:00:00.000000000Z", - "created_by": "caracara@test.com", - "modified_on": "2022-01-01T12:00:00.000000000Z", - "modified_by": "caracara@test.com", - "comment": "test comment 2", - }], + "rules": [ + { + "customer_id": "test_customer", + "instance_id": "test_rule_01", + "name": "test rule", + "description": "test rule desc", + "pattern_id": "41000", + "pattern_severity": "critical", + "disposition_id": list(simple_rule_type.disposition_map.keys())[0], + "action_label": list(simple_rule_type.disposition_map.values())[0], + "ruletype_id": simple_rule_type.id_, + "ruletype_name": simple_rule_type.name, + "field_values": [], + "enabled": True, + "deleted": False, + "instance_version": 1, + "version_ids": [1], + "magic_cookie": 1, + "committed_on": "2022-01-01T12:00:00.000000000Z", + "created_on": "2022-01-01T12:00:00.000000000Z", + "created_by": "caracara@test.com", + "modified_on": "2022-01-01T12:00:00.000000000Z", + "modified_by": "caracara@test.com", + "comment": "test comment 2", + } + ], "version": 1, "committed_on": "2022-01-01T12:00:00.000000000Z", "created_on": "2022-01-01T12:00:00.000000000Z", @@ -342,14 +365,14 @@ def mock_query_rule_groups_full(offset, limit, filter): custom_ioa_api.query_rule_groups_full.side_effect = mock_query_rule_groups_full custom_ioa_api.query_rule_types.side_effect = create_mock_query_resources( - resources=[simple_rule_type.id_]) + resources=[simple_rule_type.id_] + ) custom_ioa_api.get_rule_types.side_effect = create_mock_get_rule_types( - rule_types=[simple_rule_type]) + rule_types=[simple_rule_type] + ) # Call caracara - groups = client.custom_ioas.describe_rule_groups( - filters="test_filter" - ) + groups = client.custom_ioas.describe_rule_groups(filters="test_filter") assert len(mock_groups) == len(groups) for mock_group in mock_groups: @@ -365,38 +388,42 @@ def test_delete_rule_groups_using_ids(client: Client, custom_ioa_api: falconpy.C # Assert custom_ioa_api.delete_rule_groups.assert_called_once_with( - ids=["test_group_01"], comment="test comment") + ids=["test_group_01"], comment="test comment" + ) def test_delete_rule_groups_using_groups(client: Client, custom_ioa_api: falconpy.CustomIOA): """Tests `CustomIoaApiModule.delete_rule_groups` when using rule group objects""" # Setup - group = IoaRuleGroup.from_data_dict({ - "customer_id": "test_customer", - "id": "test_group_01", - "name": "test rule group", - "description": "test rule group desc", - "platform": "windows", - "enabled": False, - "deleted": False, - "rule_ids": [], - "rules": [], - "version": 1, - "committed_on": "2022-01-01T12:00:00.000000000Z", - "created_on": "2022-01-01T12:00:00.000000000Z", - "created_by": "caracara@test.com", - "modified_on": "2022-01-01T12:00:00.000000000Z", - "modified_by": "caracara@test.com", - "comment": "test comment", - }, rule_type_map=[]) + group = IoaRuleGroup.from_data_dict( + { + "customer_id": "test_customer", + "id": "test_group_01", + "name": "test rule group", + "description": "test rule group desc", + "platform": "windows", + "enabled": False, + "deleted": False, + "rule_ids": [], + "rules": [], + "version": 1, + "committed_on": "2022-01-01T12:00:00.000000000Z", + "created_on": "2022-01-01T12:00:00.000000000Z", + "created_by": "caracara@test.com", + "modified_on": "2022-01-01T12:00:00.000000000Z", + "modified_by": "caracara@test.com", + "comment": "test comment", + }, + rule_type_map=[], + ) # Call caracara - client.custom_ioas.delete_rule_groups( - rule_groups=[group], comment="test deletion comment") + client.custom_ioas.delete_rule_groups(rule_groups=[group], comment="test deletion comment") # Assert custom_ioa_api.delete_rule_groups.assert_called_once_with( - ids=["test_group_01"], comment="test deletion comment") + ids=["test_group_01"], comment="test deletion comment" + ) def test_update_rule_groups_no_rules(client: Client, custom_ioa_api: falconpy.CustomIOA): @@ -440,20 +467,23 @@ def mock_update_rule_group(body): new_group = client.custom_ioas.update_rule_group(group, comment="test update comment") # Assert falconpy called correctly - custom_ioa_api.update_rule_group.assert_called_once_with(body={ - "id": "test_group_01", - "name": "test rule group", - "description": "test rule group desc", - "enabled": False, - "rulegroup_version": 1, - "comment": "test update comment", - }) + custom_ioa_api.update_rule_group.assert_called_once_with( + body={ + "id": "test_group_01", + "name": "test rule group", + "description": "test rule group desc", + "enabled": False, + "rulegroup_version": 1, + "comment": "test update comment", + } + ) # Assert new group is as expected assert new_group.version == group.version + 1 def test_update_rule_groups_with_rule_changes( - client: Client, custom_ioa_api: falconpy.CustomIOA, simple_rule_type: RuleType): + client: Client, custom_ioa_api: falconpy.CustomIOA, simple_rule_type: RuleType +): """Tests `CustomIoaApiModule.update_rule_groups` when the group has a rule to update, another rule to create, and another to delete.""" # Setup @@ -525,7 +555,8 @@ def test_update_rule_groups_with_rule_changes( "comment": "test rule group comment", } group = IoaRuleGroup.from_data_dict( # Acts as an already queried group - raw_group, rule_type_map={simple_rule_type.id_: simple_rule_type}) + raw_group, rule_type_map={simple_rule_type.id_: simple_rule_type} + ) group.remove_rule(0) rule = CustomIoaRule( name="test rule 3", @@ -553,8 +584,11 @@ def mock_update_rules(body): assert body["rulegroup_version"] == raw_group["version"] + 1 raw_group["version"] = body["rulegroup_version"] for raw_rule_update in body["rule_updates"]: - matching_rules = [i for (i, raw_rule) in enumerate(raw_group["rules"]) - if raw_rule["instance_id"] == raw_rule_update["instance_id"]] + matching_rules = [ + i + for (i, raw_rule) in enumerate(raw_group["rules"]) + if raw_rule["instance_id"] == raw_rule_update["instance_id"] + ] assert len(matching_rules) == 1 rule_index = matching_rules[0] raw_group["rules"][rule_index]["name"] = raw_rule_update["name"] @@ -601,9 +635,11 @@ def mock_create_rule(body): custom_ioa_api.create_rule.side_effect = mock_create_rule custom_ioa_api.query_rule_types.side_effect = create_mock_query_resources( - resources=[simple_rule_type.id_]) + resources=[simple_rule_type.id_] + ) custom_ioa_api.get_rule_types.side_effect = create_mock_get_rule_types( - rule_types=[simple_rule_type]) + rule_types=[simple_rule_type] + ) # Call caracara new_group = client.custom_ioas.update_rule_group(group, comment="test update comment") @@ -614,39 +650,50 @@ def mock_create_rule(body): # - A rule deletion # - A rule update # - A rule creation - custom_ioa_api.update_rule_group.assert_called_once_with(body={ - "id": "test_group_01", - "name": "test rule group", - "description": "test rule group desc", - "enabled": False, - "rulegroup_version": 1, - "comment": "test update comment", - }) + custom_ioa_api.update_rule_group.assert_called_once_with( + body={ + "id": "test_group_01", + "name": "test rule group", + "description": "test rule group desc", + "enabled": False, + "rulegroup_version": 1, + "comment": "test update comment", + } + ) custom_ioa_api.delete_rules.assert_called_once_with( - rule_group_id="test_group_01", ids=["test_rule_01"], comment="test update comment") - custom_ioa_api.update_rules.assert_called_once_with(body={ - "rulegroup_id": "test_group_01", - "rulegroup_version": 2, - "rule_updates": [{ - "instance_id": "test_rule_02", - "name": "test rule 2", - "description": "test rule 2 desc", + rule_group_id="test_group_01", + ids=["test_rule_01"], + comment="test update comment", + ) + custom_ioa_api.update_rules.assert_called_once_with( + body={ + "rulegroup_id": "test_group_01", + "rulegroup_version": 2, + "rule_updates": [ + { + "instance_id": "test_rule_02", + "name": "test rule 2", + "description": "test rule 2 desc", + "pattern_severity": "critical", + "disposition_id": list(simple_rule_type.disposition_map.keys())[0], + "field_values": [], + "enabled": True, + } + ], + "comment": "test update comment", + } + ) + custom_ioa_api.create_rule.assert_called_once_with( + body={ + "name": "test rule 3", + "description": "test rule 3 desc", "pattern_severity": "critical", "disposition_id": list(simple_rule_type.disposition_map.keys())[0], "field_values": [], - "enabled": True, - }], - "comment": "test update comment", - }) - custom_ioa_api.create_rule.assert_called_once_with(body={ - "name": "test rule 3", - "description": "test rule 3 desc", - "pattern_severity": "critical", - "disposition_id": list(simple_rule_type.disposition_map.keys())[0], - "field_values": [], - "ruletype_id": simple_rule_type.id_, - "rulegroup_id": "test_group_01", - "comment": "test update comment" - }) + "ruletype_id": simple_rule_type.id_, + "rulegroup_id": "test_group_01", + "comment": "test update comment", + } + ) # Assert new group is as expected assert new_group.version == group.version + 1 diff --git a/tests/unit_tests/test_hosts.py b/tests/unit_tests/test_hosts.py index db87141..476eba6 100644 --- a/tests/unit_tests/test_hosts.py +++ b/tests/unit_tests/test_hosts.py @@ -1,8 +1,9 @@ """Unit tests for HostsApiModule""" + from unittest.mock import patch -import pytest import falconpy +import pytest from caracara import Client from caracara.common.constants import OnlineState @@ -65,7 +66,7 @@ def mock_query_devices_by_filter_scroll(*, filter, limit, offset): return { "body": { - "resources": visible_ids[offset:offset+limit], + "resources": visible_ids[offset : offset + limit], "meta": { "pagination": { "total": len(visible_ids), @@ -84,7 +85,7 @@ def mock_query_hidden_devices(*, filter, limit, offset): return { "body": { - "resources": hidden_ids[offset:offset+limit], + "resources": hidden_ids[offset : offset + limit], "meta": { "pagination": { "total": len(hidden_ids), @@ -97,23 +98,20 @@ def mock_query_hidden_devices(*, filter, limit, offset): def mock_get_device_details(ids, *, parameters=None): """Mock method for falconpy.Hosts.get_device_details""" return { - "body": { - "resources": [mock_devices[id_] for id_ in ids] - }, + "body": {"resources": [mock_devices[id_] for id_ in ids]}, } def mock_query_online_state(ids, *, parameters=None): """Mock method for falconpy.Hosts.get_device_details""" return { - "body": { - "resources": [mock_device_online_states[id_] for id_ in ids] - }, + "body": {"resources": [mock_device_online_states[id_] for id_ in ids]}, } def hosts_test(): """Decorator that contains common functionality between all hosts tests.""" + def decorator(func): @patch("caracara.modules.hosts.hosts.HostGroup", autospec=falconpy.HostGroup) @patch("caracara.modules.hosts.hosts.Hosts", autospec=falconpy.Hosts) @@ -134,7 +132,9 @@ def new_func(mock_oauth2, mock_hosts, mock_hostgroup): mock_hosts=mock_hosts, mock_hostgroup=mock_hostgroup, ) + return new_func + return decorator @@ -142,11 +142,13 @@ def new_func(mock_oauth2, mock_hosts, mock_hostgroup): def test_describe_devices(auth: Client, **_): """Unit test for HostsApiModule.describe_devices""" # Mock FalconPy methods - auth.hosts.hosts_api.configure_mock(**{ - "query_devices_by_filter_scroll.side_effect": mock_query_devices_by_filter_scroll, - "get_device_details.side_effect": mock_get_device_details, - "get_online_state.side_effect": mock_query_online_state, - }) + auth.hosts.hosts_api.configure_mock( + **{ + "query_devices_by_filter_scroll.side_effect": mock_query_devices_by_filter_scroll, + "get_device_details.side_effect": mock_get_device_details, + "get_online_state.side_effect": mock_query_online_state, + } + ) visible_devices = dict( (id_, dev) for id_, dev in mock_devices.items() if dev.get("host_hidden_status") != "hidden" @@ -159,16 +161,20 @@ def test_describe_devices(auth: Client, **_): def test_describe_devices__online_only(auth: Client, **_): """Unit test for HostsApiModule.describe_devices""" # Mock FalconPy methods - auth.hosts.hosts_api.configure_mock(**{ - "query_devices_by_filter_scroll.side_effect": mock_query_devices_by_filter_scroll, - "get_device_details.side_effect": mock_get_device_details, - "get_online_state.side_effect": mock_query_online_state, - }) + auth.hosts.hosts_api.configure_mock( + **{ + "query_devices_by_filter_scroll.side_effect": mock_query_devices_by_filter_scroll, + "get_device_details.side_effect": mock_get_device_details, + "get_online_state.side_effect": mock_query_online_state, + } + ) - online_visible_devices = dict(filter( - lambda item: item[0] in list(set(visible_ids) & set(online_ids)), - mock_devices.items(), - )) + online_visible_devices = dict( + filter( + lambda item: item[0] in list(set(visible_ids) & set(online_ids)), + mock_devices.items(), + ) + ) assert auth.hosts.describe_devices(online_state="online") == online_visible_devices @@ -177,16 +183,20 @@ def test_describe_devices__online_only(auth: Client, **_): def test_describe_devices__enum_online_state(auth: Client, **_): """Unit test for HostsApiModule.describe_devices""" # Mock FalconPy methods - auth.hosts.hosts_api.configure_mock(**{ - "query_devices_by_filter_scroll.side_effect": mock_query_devices_by_filter_scroll, - "get_device_details.side_effect": mock_get_device_details, - "get_online_state.side_effect": mock_query_online_state, - }) + auth.hosts.hosts_api.configure_mock( + **{ + "query_devices_by_filter_scroll.side_effect": mock_query_devices_by_filter_scroll, + "get_device_details.side_effect": mock_get_device_details, + "get_online_state.side_effect": mock_query_online_state, + } + ) - offline_visible_devices = dict(filter( - lambda item: item[0] in list(set(visible_ids) & set(offline_ids)), - mock_devices.items(), - )) + offline_visible_devices = dict( + filter( + lambda item: item[0] in list(set(visible_ids) & set(offline_ids)), + mock_devices.items(), + ) + ) assert auth.hosts.describe_devices(online_state=OnlineState.OFFLINE) == offline_visible_devices @@ -195,11 +205,13 @@ def test_describe_devices__enum_online_state(auth: Client, **_): def test_describe_devices__invalid_online_state(auth: Client, **_): """Unit test for HostsApiModule.describe_devices""" # Mock FalconPy methods - auth.hosts.hosts_api.configure_mock(**{ - "query_devices_by_filter_scroll.side_effect": mock_query_devices_by_filter_scroll, - "get_device_details.side_effect": mock_get_device_details, - "get_online_state.side_effect": mock_query_online_state, - }) + auth.hosts.hosts_api.configure_mock( + **{ + "query_devices_by_filter_scroll.side_effect": mock_query_devices_by_filter_scroll, + "get_device_details.side_effect": mock_get_device_details, + "get_online_state.side_effect": mock_query_online_state, + } + ) with pytest.raises(InvalidOnlineState): auth.hosts.describe_devices(online_state="notastate") @@ -209,10 +221,12 @@ def test_describe_devices__invalid_online_state(auth: Client, **_): def test_describe_hidden_devices(auth: Client, **_): """Unit test for HostsApiModule.describe_hidden_devices""" # Mock FalconPy methods - auth.hosts.hosts_api.configure_mock(**{ - "query_hidden_devices.side_effect": mock_query_hidden_devices, - "get_device_details.side_effect": mock_get_device_details, - }) + auth.hosts.hosts_api.configure_mock( + **{ + "query_hidden_devices.side_effect": mock_query_hidden_devices, + "get_device_details.side_effect": mock_get_device_details, + } + ) hidden_devices = dict( (i, dev) for i, dev in mock_devices.items() if dev.get("host_hidden_status") == "hidden" @@ -324,10 +338,12 @@ def mock_perform_action(*, ids, action_name): } } - auth.hosts.hosts_api.configure_mock(**{ - "query_devices_by_filter_scroll.side_effect": mock_query_devices_by_filter_scroll, - "perform_action.side_effect": mock_perform_action, - }) + auth.hosts.hosts_api.configure_mock( + **{ + "query_devices_by_filter_scroll.side_effect": mock_query_devices_by_filter_scroll, + "perform_action.side_effect": mock_perform_action, + } + ) assert auth.hosts.contain() == resources auth.hosts.hosts_api.perform_action.assert_called_once_with( @@ -348,10 +364,12 @@ def mock_perform_action(*, ids, action_name): } } - auth.hosts.hosts_api.configure_mock(**{ - "query_devices_by_filter_scroll.side_effect": mock_query_devices_by_filter_scroll, - "perform_action.side_effect": mock_perform_action, - }) + auth.hosts.hosts_api.configure_mock( + **{ + "query_devices_by_filter_scroll.side_effect": mock_query_devices_by_filter_scroll, + "perform_action.side_effect": mock_perform_action, + } + ) assert auth.hosts.release() == resources auth.hosts.hosts_api.perform_action.assert_called_once_with( @@ -372,10 +390,12 @@ def mock_perform_action(*, ids, action_name): } } - auth.hosts.hosts_api.configure_mock(**{ - "query_devices_by_filter_scroll.side_effect": mock_query_devices_by_filter_scroll, - "perform_action.side_effect": mock_perform_action, - }) + auth.hosts.hosts_api.configure_mock( + **{ + "query_devices_by_filter_scroll.side_effect": mock_query_devices_by_filter_scroll, + "perform_action.side_effect": mock_perform_action, + } + ) assert auth.hosts.hide() == resources auth.hosts.hosts_api.perform_action.assert_called_once_with( @@ -396,10 +416,12 @@ def mock_perform_action(*, ids, action_name): } } - auth.hosts.hosts_api.configure_mock(**{ - "query_hidden_devices.side_effect": mock_query_hidden_devices, - "perform_action.side_effect": mock_perform_action, - }) + auth.hosts.hosts_api.configure_mock( + **{ + "query_hidden_devices.side_effect": mock_query_hidden_devices, + "perform_action.side_effect": mock_perform_action, + } + ) assert auth.hosts.unhide() == resources auth.hosts.hosts_api.perform_action.assert_called_once_with( @@ -421,10 +443,12 @@ def mock_update_device_tags(*, action_name, ids, tags): } } - auth.hosts.hosts_api.configure_mock(**{ - "query_devices_by_filter_scroll.side_effect": mock_query_devices_by_filter_scroll, - "update_device_tags.side_effect": mock_update_device_tags, - }) + auth.hosts.hosts_api.configure_mock( + **{ + "query_devices_by_filter_scroll.side_effect": mock_query_devices_by_filter_scroll, + "update_device_tags.side_effect": mock_update_device_tags, + } + ) assert auth.hosts.tag(tags) == resources auth.hosts.hosts_api.update_device_tags.assert_called_once_with( @@ -447,10 +471,12 @@ def mock_update_device_tags(*, action_name, ids, tags): } } - auth.hosts.hosts_api.configure_mock(**{ - "query_devices_by_filter_scroll.side_effect": mock_query_devices_by_filter_scroll, - "update_device_tags.side_effect": mock_update_device_tags, - }) + auth.hosts.hosts_api.configure_mock( + **{ + "query_devices_by_filter_scroll.side_effect": mock_query_devices_by_filter_scroll, + "update_device_tags.side_effect": mock_update_device_tags, + } + ) assert auth.hosts.untag(tags) == resources auth.hosts.hosts_api.update_device_tags.assert_called_once_with( @@ -463,9 +489,11 @@ def mock_update_device_tags(*, action_name, ids, tags): @hosts_test() def test_get_device_ids(auth: Client, **_): """Unit test for HostsApiModule.get_device_ids""" - auth.hosts.hosts_api.configure_mock(**{ - "query_devices_by_filter_scroll.side_effect": mock_query_devices_by_filter_scroll, - }) + auth.hosts.hosts_api.configure_mock( + **{ + "query_devices_by_filter_scroll.side_effect": mock_query_devices_by_filter_scroll, + } + ) assert auth.hosts.get_device_ids() == visible_ids @@ -473,34 +501,42 @@ def test_get_device_ids(auth: Client, **_): @hosts_test() def test_get_device_ids__online_only(auth: Client, **_): """Unit test for HostsApiModule.get_device_ids""" - auth.hosts.hosts_api.configure_mock(**{ - "query_devices_by_filter_scroll.side_effect": mock_query_devices_by_filter_scroll, - "get_online_state.side_effect": mock_query_online_state, - }) + auth.hosts.hosts_api.configure_mock( + **{ + "query_devices_by_filter_scroll.side_effect": mock_query_devices_by_filter_scroll, + "get_online_state.side_effect": mock_query_online_state, + } + ) - assert set(auth.hosts.get_device_ids(online_state="online")) \ - == set(online_ids) & set(visible_ids) + assert set(auth.hosts.get_device_ids(online_state="online")) == set(online_ids) & set( + visible_ids + ) @hosts_test() def test_get_device_ids__enum_online_state(auth: Client, **_): """Unit test for HostsApiModule.get_device_ids""" - auth.hosts.hosts_api.configure_mock(**{ - "query_devices_by_filter_scroll.side_effect": mock_query_devices_by_filter_scroll, - "get_online_state.side_effect": mock_query_online_state, - }) + auth.hosts.hosts_api.configure_mock( + **{ + "query_devices_by_filter_scroll.side_effect": mock_query_devices_by_filter_scroll, + "get_online_state.side_effect": mock_query_online_state, + } + ) - assert set(auth.hosts.get_device_ids(online_state=OnlineState.UNKNOWN)) \ - == set(unknown_ids) & set(visible_ids) + assert set(auth.hosts.get_device_ids(online_state=OnlineState.UNKNOWN)) == set( + unknown_ids + ) & set(visible_ids) @hosts_test() def test_get_device_ids__invalid_online_state(auth: Client, **_): """Unit test for HostsApiModule.get_device_ids""" - auth.hosts.hosts_api.configure_mock(**{ - "query_devices_by_filter_scroll.side_effect": mock_query_devices_by_filter_scroll, - "get_online_state.side_effect": mock_query_online_state, - }) + auth.hosts.hosts_api.configure_mock( + **{ + "query_devices_by_filter_scroll.side_effect": mock_query_devices_by_filter_scroll, + "get_online_state.side_effect": mock_query_online_state, + } + ) with pytest.raises(InvalidOnlineState): auth.hosts.get_device_ids(online_state="notastate") @@ -509,9 +545,11 @@ def test_get_device_ids__invalid_online_state(auth: Client, **_): @hosts_test() def test_get_hidden_ids(auth: Client, **_): """Unit test for HostsApiModule.get_hidden_ids""" - auth.hosts.hosts_api.configure_mock(**{ - "query_hidden_devices.side_effect": mock_query_hidden_devices, - }) + auth.hosts.hosts_api.configure_mock( + **{ + "query_hidden_devices.side_effect": mock_query_hidden_devices, + } + ) assert auth.hosts.get_hidden_ids() == hidden_ids @@ -519,35 +557,47 @@ def test_get_hidden_ids(auth: Client, **_): @hosts_test() def test_filter_device_ids__online_only(auth: Client, **_): """Unit test for HostsApiModule.filter_device_ids_by_online_state""" - auth.hosts.hosts_api.configure_mock(**{ - "get_online_state.side_effect": mock_query_online_state, - }) + auth.hosts.hosts_api.configure_mock( + **{ + "get_online_state.side_effect": mock_query_online_state, + } + ) - assert auth.hosts.filter_device_ids_by_online_state( - list(mock_device_online_states.keys()), - "online", - ) == online_ids + assert ( + auth.hosts.filter_device_ids_by_online_state( + list(mock_device_online_states.keys()), + "online", + ) + == online_ids + ) @hosts_test() def test_filter_device_ids__enum_online_state(auth: Client, **_): """Unit test for HostsApiModule.filter_device_ids_by_online_state""" - auth.hosts.hosts_api.configure_mock(**{ - "get_online_state.side_effect": mock_query_online_state, - }) + auth.hosts.hosts_api.configure_mock( + **{ + "get_online_state.side_effect": mock_query_online_state, + } + ) - assert auth.hosts.filter_device_ids_by_online_state( - list(mock_device_online_states.keys()), - OnlineState.ONLINE, - ) == online_ids + assert ( + auth.hosts.filter_device_ids_by_online_state( + list(mock_device_online_states.keys()), + OnlineState.ONLINE, + ) + == online_ids + ) @hosts_test() def test_filter_device_ids__invalid_online_state(auth: Client, **_): """Unit test for HostsApiModule.filter_device_ids_by_online_state""" - auth.hosts.hosts_api.configure_mock(**{ - "get_online_state.side_effect": mock_query_online_state, - }) + auth.hosts.hosts_api.configure_mock( + **{ + "get_online_state.side_effect": mock_query_online_state, + } + ) with pytest.raises(InvalidOnlineState): auth.hosts.filter_device_ids_by_online_state( diff --git a/tests/unit_tests/test_prevention_policies.py b/tests/unit_tests/test_prevention_policies.py index ff9fb97..7c7e8f4 100644 --- a/tests/unit_tests/test_prevention_policies.py +++ b/tests/unit_tests/test_prevention_policies.py @@ -1,6 +1,6 @@ """Unit tests for PreventionPoliciesApiModule""" -import copy +import copy from unittest.mock import patch import falconpy @@ -11,6 +11,7 @@ def prevpol_test(): """Decorator that contains common functionality between all prevention policy tests.""" + def decorator(func): @patch( "caracara.modules.prevention_policies.prevention_policies.PreventionPolicies", @@ -32,7 +33,9 @@ def test_new_func(mock_oauth2, mock_prevpol): mock_oauth2=mock_oauth2, mock_prevpol=mock_prevpol, ) + return test_new_func + return decorator @@ -68,7 +71,7 @@ def mock_query_combined_policies(filter, sort, offset, limit): # pylint: disabl return { "body": { - "resources": mock_policies[offset:offset+limit], + "resources": mock_policies[offset : offset + limit], "meta": { "pagination": { "total": len(mock_policies), @@ -82,23 +85,30 @@ def mock_query_combined_policies(filter, sort, offset, limit): # pylint: disabl def test_describe_policies_raw(auth: Client, **_): """Unit test for PreventionPoliciesApiModule.describe_policies_raw.""" - auth.prevention_policies.prevention_policies_api.configure_mock(**{ - "query_combined_policies.side_effect": mock_query_combined_policies, - }) + auth.prevention_policies.prevention_policies_api.configure_mock( + **{ + "query_combined_policies.side_effect": mock_query_combined_policies, + } + ) - assert auth.prevention_policies.describe_policies_raw( - filters=test_filters, - sort=test_sort, - ) == mock_policies + assert ( + auth.prevention_policies.describe_policies_raw( + filters=test_filters, + sort=test_sort, + ) + == mock_policies + ) @prevpol_test() def test_describe_policies(auth, **_): """Unit test for PreventionPoliciesApiModule.describe_policies""" - auth.prevention_policies.prevention_policies_api.configure_mock(**{ - "query_combined_policies.side_effect": mock_query_combined_policies, - }) + auth.prevention_policies.prevention_policies_api.configure_mock( + **{ + "query_combined_policies.side_effect": mock_query_combined_policies, + } + ) results = auth.prevention_policies.describe_policies( filters=test_filters, @@ -128,14 +138,18 @@ def mock_create_policies(body): body["resources"][0]["cid"] = mock_cid return {"body": body} - auth.prevention_policies.prevention_policies_api.configure_mock(**{ - "create_policies.side_effect": mock_create_policies, - }) + auth.prevention_policies.prevention_policies_api.configure_mock( + **{ + "create_policies.side_effect": mock_create_policies, + } + ) - res = auth.prevention_policies.push_policy(Policy( - data_dict=mock_policy, - style="prevention", - )) + res = auth.prevention_policies.push_policy( + Policy( + data_dict=mock_policy, + style="prevention", + ) + ) assert res.cid == mock_cid diff --git a/tests/unit_tests/test_response_policies.py b/tests/unit_tests/test_response_policies.py index 1caaa93..bd322f7 100644 --- a/tests/unit_tests/test_response_policies.py +++ b/tests/unit_tests/test_response_policies.py @@ -1,13 +1,17 @@ """Unit tests for ResponsePoliciesApiModule""" + import copy from unittest.mock import patch + import falconpy + from caracara import Client, Policy from caracara.common.sorting import SORT_ASC def respol_test(): """Decorator that contains common functionality between all prevention policy tests.""" + def decorator(func): @patch( "caracara.modules.response_policies.response_policies.ResponsePolicies", @@ -28,7 +32,9 @@ def test_new_func(mock_oauth2, mock_prevpol): mock_oauth2=mock_oauth2, mock_prevpol=mock_prevpol, ) + return test_new_func + return decorator @@ -63,12 +69,8 @@ def mock_query_combined_policies(filter, sort, offset, limit): # pylint: disabl return { "body": { - "resources": mock_policies[offset:offset+limit], - "meta": { - "pagination": { - "total": len(mock_policies) - } - } + "resources": mock_policies[offset : offset + limit], + "meta": {"pagination": {"total": len(mock_policies)}}, } } @@ -77,27 +79,26 @@ def mock_query_combined_policies(filter, sort, offset, limit): # pylint: disabl def test_describe_policies_raw(auth, **_): """Unit test for ResponsePoliciesApiModule.describe_policies_raw""" - auth.response_policies.response_policies_api.configure_mock(**{ - "query_combined_policies.side_effect": mock_query_combined_policies - }) + auth.response_policies.response_policies_api.configure_mock( + **{"query_combined_policies.side_effect": mock_query_combined_policies} + ) - assert auth.response_policies.describe_policies_raw( - filters=test_filters, sort=test_sort - ) == mock_policies + assert ( + auth.response_policies.describe_policies_raw(filters=test_filters, sort=test_sort) + == mock_policies + ) @respol_test() def test_describe_policies(auth, **_): """Unit test for ResponsePoliciesApiModule.describe_policies""" - auth.response_policies.response_policies_api.configure_mock(**{ - "query_combined_policies.side_effect": mock_query_combined_policies - }) - - results = auth.response_policies.describe_policies( - filters=test_filters, sort=test_sort + auth.response_policies.response_policies_api.configure_mock( + **{"query_combined_policies.side_effect": mock_query_combined_policies} ) + results = auth.response_policies.describe_policies(filters=test_filters, sort=test_sort) + result_dumps = [pol.dump() for pol in results] assert result_dumps == mock_policies @@ -120,9 +121,9 @@ def mock_create_policies(body): body["resources"][0]["cid"] = mock_cid return {"body": body} - auth.response_policies.response_policies_api.configure_mock(**{ - "create_policies.side_effect": mock_create_policies - }) + auth.response_policies.response_policies_api.configure_mock( + **{"create_policies.side_effect": mock_create_policies} + ) res = auth.response_policies.push_policy(Policy(data_dict=mock_policy, style="response")) @@ -150,13 +151,15 @@ def mock_query_combined_policies_(filter): # pylint: disable=redefined-builtin "pagination": { "total": 1, } - } + }, } } - auth.response_policies.response_policies_api.configure_mock(**{ - "query_combined_policies": mock_query_combined_policies_, - }) + auth.response_policies.response_policies_api.configure_mock( + **{ + "query_combined_policies": mock_query_combined_policies_, + } + ) updated_policy = auth.response_policies.add_policy_to_group(policy_id, group_id) @@ -180,15 +183,11 @@ def test_modify_policy(auth, **_): policy = Policy(data_dict=raw_policy, style="response") def mock_update_policies(body): # pylint: disable=unused-argument - return { - "body": { - "resources": [raw_updated_policy] - } - } + return {"body": {"resources": [raw_updated_policy]}} - auth.response_policies.response_policies_api.configure_mock(**{ - "update_policies.side_effect": mock_update_policies - }) + auth.response_policies.response_policies_api.configure_mock( + **{"update_policies.side_effect": mock_update_policies} + ) updated_policy = auth.response_policies.modify_policy(policy) From cb037eee0bc54e1bb8bc8ab730513d6149a74032 Mon Sep 17 00:00:00 2001 From: Chris Hammond Date: Wed, 11 Sep 2024 20:28:20 +0100 Subject: [PATCH 4/6] Resolves #187 and some other general RTR snags. Also bumps dependencies. --- caracara/modules/rtr/batch_session.py | 117 +++++++++++++++++++------- 1 file changed, 86 insertions(+), 31 deletions(-) diff --git a/caracara/modules/rtr/batch_session.py b/caracara/modules/rtr/batch_session.py index c7a1923..0b31b9e 100644 --- a/caracara/modules/rtr/batch_session.py +++ b/caracara/modules/rtr/batch_session.py @@ -4,9 +4,10 @@ import logging from dataclasses import dataclass from datetime import datetime, timedelta +from itertools import repeat from functools import partial, wraps from threading import current_thread -from typing import Dict, List +from typing import Dict, List, Tuple, Optional from falconpy import RealTimeResponse, RealTimeResponseAdmin @@ -44,12 +45,13 @@ class InnerRTRBatchSession: # pylint: disable=too-few-public-methods a list of InnerRTRBatchSession objects. """ - batch_id: str = None - devices: Dict = None - expiry: datetime = None - logger: logging.Logger = None - - def __init__(self, batch_id: str, devices: Dict, expiry: datetime, logger: logging.Logger): + def __init__( + self, + batch_id: str, + devices: Dict, + expiry: datetime, + logger: logging.Logger + ): """Configure an inner batch of RTR sessions.""" self.batch_id = batch_id self.devices = devices @@ -74,7 +76,7 @@ def generic_rtr_worker( func: partial, logger: logging.Logger, device_ids: List[str] = None, -): +) -> Tuple[str, Dict]: """ Execute a partial function against an RTR batch session. @@ -96,7 +98,7 @@ def generic_rtr_worker( func.keywords["optional_hosts"] = device_ids_in_batch response = func(batch_id=session.batch_id)["body"] logger.debug("%s | %s", thread_name, response) - return response + return thread_name, response class RTRBatchSession: @@ -121,14 +123,14 @@ class RTRBatchSession: @_batch_session_required def device_ids(self) -> List[str]: """Return a list of device IDs from all inner batch sessions.""" - return [x.devices.keys() for x in self.batch_sessions] + return [x.devices for x in self.batch_sessions] def connect( self, device_ids: List[str], queueing: bool = False, timeout: int = default_timeout, - ): + ) -> bool: """ Establish a connection to one or more hosts. @@ -142,7 +144,10 @@ def connect( batches.append(device_ids[i : i + MAX_BATCH_SESSION_HOSTS]) self.logger.info("Divided up devices into %d batches", len(batches)) - def worker(batch_device_ids: List[str], batch_func: partial): + def worker( + batch_device_ids: List[str], + batch_func: partial, + ) -> Tuple[str, Optional[InnerRTRBatchSession]]: thread_name = current_thread().name self.logger.info( "%s | Batch worker started with a list of %d devices", @@ -150,16 +155,34 @@ def worker(batch_device_ids: List[str], batch_func: partial): len(batch_device_ids), ) response = batch_func(host_ids=batch_device_ids)["body"] + self.logger.debug("%s | %s", thread_name, str(response)) resources = response["resources"] - self.logger.info("%s | Connected to %s systems", thread_name, len(resources)) + + # Identify devices that failed to connect and/or returned an error + # Resolves GitHub issue #187 + if not resources: + self.logger.info("%s | Resource list is empty", thread_name) + return thread_name, None + + successful_devices = { + device_id: device_data + for device_id, device_data in resources.items() + if not device_data.get("errors") + } + + if not successful_devices: + self.logger.info("%s | Successful device list is empty", thread_name) + return thread_name, None + + self.logger.info("%s | Connected to %s systems", thread_name, len(successful_devices)) self.logger.debug("%s | %s", thread_name, response) batch_data = InnerRTRBatchSession( - batch_id=response["batch_id"], - devices=resources, + batch_id=response['batch_id'], + devices=successful_devices, expiry=datetime.now() + timedelta(seconds=SESSION_EXPIRY), logger=self.logger, ) - return batch_data + return thread_name, batch_data batch_func = partial( self.api.batch_init_sessions, @@ -171,17 +194,27 @@ def worker(batch_device_ids: List[str], batch_func: partial): with concurrent.futures.ThreadPoolExecutor( max_workers=MAX_BATCH_SESSION_THREADS ) as executor: - completed = executor.map(worker, batches, [batch_func]) + completed = executor.map(worker, batches, repeat(batch_func)) self.batch_sessions = [] - for complete in completed: - self.logger.info("Completed a batch of RTR connections") - self.batch_sessions.append(complete) + for thread_name, thread_data in completed: + self.logger.info("%s | Completed a batch of RTR connections", thread_name) + if thread_data is None: + self.logger.info("%s | Batch contained no successful connections", thread_name) + else: + self.logger.info( + "%s | Batch contained %d successful connections", + thread_name, + len(thread_data.devices), + ) + self.batch_sessions.append(thread_data) device_count = sum(len(d.devices) for d in self.batch_sessions) self.logger.info("Connected to %d devices", device_count) self.logger.debug(self.batch_sessions) + return len(self.batch_sessions) > 0 + @_batch_session_required def disconnect(self): """Disconnect the RTR batch session.""" @@ -218,15 +251,29 @@ def get( completed: List[Dict] = executor.map( partial_worker, self.batch_sessions, - [partial_get_func], + repeat(partial_get_func), ) batch_get_cmd_reqs: List[BatchGetCmdRequest] = [] - for complete in completed: - self.logger.info("Executed commands on a batch of %d hosts", len(complete)) + for thread_name, response in completed: + try: + devices = response['combined']['resources'] + except KeyError: + self.logger.warning( + "%s | Batch contained no successful get command executions", + thread_name, + ) + continue + + self.logger.info( + "%s | Executed get command on a batch of %d devices", + thread_name, + len(devices), + ) + batch_get_cmd_req = BatchGetCmdRequest( - batch_get_cmd_req_id=complete["batch_get_cmd_req_id"], - devices=complete["combined"]["resources"], + batch_get_cmd_req_id=response["batch_get_cmd_req_id"], + devices=devices, ) batch_get_cmd_reqs.append(batch_get_cmd_req) @@ -278,7 +325,7 @@ def worker(batch_get_cmd_req: BatchGetCmdRequest, func: partial) -> List[GetFile timeout_duration=f"{timeout}s", ) with concurrent.futures.ThreadPoolExecutor(max_workers=threads) as executor: - completed = executor.map(worker, batch_get_cmd_reqs, [partial_func]) + completed = executor.map(worker, batch_get_cmd_reqs, repeat(partial_func)) all_get_files: List[GetFile] = [] @@ -345,7 +392,7 @@ def worker(session: InnerRTRBatchSession, func: partial): with concurrent.futures.ThreadPoolExecutor( max_workers=MAX_BATCH_SESSION_THREADS ) as executor: - completed = executor.map(worker, self.batch_sessions, [batch_func]) + completed = executor.map(worker, self.batch_sessions, repeat(batch_func)) for complete in completed: self.logger.info("Refreshed session %s", complete) @@ -399,13 +446,21 @@ def run_generic_command( completed: List[Dict] = executor.map( partial_worker, self.batch_sessions, - [partial_cmd_func], + repeat(partial_cmd_func), ) all_responses: Dict = {} - for complete in completed: - self.logger.info("Executed commands on a batch of %d hosts", len(complete)) - all_responses.update(complete["combined"]["resources"]) + for thread_name, response in completed: + try: + devices = response['combined']['resources'] + except KeyError: + self.logger.warning( + "%s | Batch contained no successful command executions", + thread_name, + ) + continue + self.logger.info("%s | Executed commands on a batch of %d hosts", thread_name, len(devices)) + all_responses.update(devices) return all_responses From 8f067debca50e6c98a2770f76c4a064c1c4d4834 Mon Sep 17 00:00:00 2001 From: Chris Hammond Date: Wed, 11 Sep 2024 20:43:43 +0100 Subject: [PATCH 5/6] Resolves linting issues --- caracara/modules/rtr/batch_session.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/caracara/modules/rtr/batch_session.py b/caracara/modules/rtr/batch_session.py index 0b31b9e..7630b12 100644 --- a/caracara/modules/rtr/batch_session.py +++ b/caracara/modules/rtr/batch_session.py @@ -398,7 +398,7 @@ def worker(session: InnerRTRBatchSession, func: partial): self.logger.info("Refreshed session %s", complete) @_batch_session_required - def run_generic_command( + def run_generic_command( # pylint: disable=too-many-locals self, command_string: str, device_ids: List[str] = None, @@ -459,7 +459,11 @@ def run_generic_command( thread_name, ) continue - self.logger.info("%s | Executed commands on a batch of %d hosts", thread_name, len(devices)) + self.logger.info( + "%s | Executed commands on a batch of %d hosts", + thread_name, + len(devices) + ) all_responses.update(devices) return all_responses From 016047e425770ffdf96f6d9811128637481cadf5 Mon Sep 17 00:00:00 2001 From: Chris Hammond Date: Wed, 16 Oct 2024 11:58:07 -0400 Subject: [PATCH 6/6] Resolves linting issues after merge --- caracara/modules/rtr/batch_session.py | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/caracara/modules/rtr/batch_session.py b/caracara/modules/rtr/batch_session.py index 7630b12..af192bb 100644 --- a/caracara/modules/rtr/batch_session.py +++ b/caracara/modules/rtr/batch_session.py @@ -4,10 +4,10 @@ import logging from dataclasses import dataclass from datetime import datetime, timedelta -from itertools import repeat from functools import partial, wraps +from itertools import repeat from threading import current_thread -from typing import Dict, List, Tuple, Optional +from typing import Dict, List, Optional, Tuple from falconpy import RealTimeResponse, RealTimeResponseAdmin @@ -45,13 +45,7 @@ class InnerRTRBatchSession: # pylint: disable=too-few-public-methods a list of InnerRTRBatchSession objects. """ - def __init__( - self, - batch_id: str, - devices: Dict, - expiry: datetime, - logger: logging.Logger - ): + def __init__(self, batch_id: str, devices: Dict, expiry: datetime, logger: logging.Logger): """Configure an inner batch of RTR sessions.""" self.batch_id = batch_id self.devices = devices @@ -177,7 +171,7 @@ def worker( self.logger.info("%s | Connected to %s systems", thread_name, len(successful_devices)) self.logger.debug("%s | %s", thread_name, response) batch_data = InnerRTRBatchSession( - batch_id=response['batch_id'], + batch_id=response["batch_id"], devices=successful_devices, expiry=datetime.now() + timedelta(seconds=SESSION_EXPIRY), logger=self.logger, @@ -257,7 +251,7 @@ def get( batch_get_cmd_reqs: List[BatchGetCmdRequest] = [] for thread_name, response in completed: try: - devices = response['combined']['resources'] + devices = response["combined"]["resources"] except KeyError: self.logger.warning( "%s | Batch contained no successful get command executions", @@ -452,7 +446,7 @@ def run_generic_command( # pylint: disable=too-many-locals all_responses: Dict = {} for thread_name, response in completed: try: - devices = response['combined']['resources'] + devices = response["combined"]["resources"] except KeyError: self.logger.warning( "%s | Batch contained no successful command executions", @@ -460,9 +454,7 @@ def run_generic_command( # pylint: disable=too-many-locals ) continue self.logger.info( - "%s | Executed commands on a batch of %d hosts", - thread_name, - len(devices) + "%s | Executed commands on a batch of %d hosts", thread_name, len(devices) ) all_responses.update(devices)