diff --git a/.vscode/settings.json b/.vscode/settings.json index 1c3e3b9..4e1f4a1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,17 @@ { - "python.testing.pytestArgs": [ - "python_dycasbin" - ], - "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true + "python.analysis.autoImportCompletions": true, + "python.testing.pytestArgs": [ + "." + ], + "python.defaultInterpreterPath": ".venv/bin/python", + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit" + } + }, + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true } \ No newline at end of file diff --git a/README.md b/README.md index 7c77b3e..09f20a3 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,27 @@ -DynamoDB adopter for casbin +# DynamoDB adopter for casbin -Installation ------------- -Run `pip install python-dycasbin` +## Installation + +### From Source + +```bash +uv build +``` + +### From pypi + +```bash +# with uv +uv add python-dycasbin +``` + +```bash +# with pip +pip install python-dycasbin +``` + +## Usage -Usage ------ ```python import casbin from python_dycasbin import adapter @@ -22,8 +38,3 @@ if e.enforce(sub, obj, act): else: print("Deny") ``` - -Running tests ---------------- -* Install [pytest](https://pypi.org/project/pytest/) and [pytest-mock](https://pypi.org/project/pytest-mock/) -* Run `pytest` from project root \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..912d04c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,79 @@ +[project] +name = "python-dycasbin" +version = "0.3.2" +description = "DynamoDB adopter for casbin" +readme = "README.md" +authors = [ + { name = "Abdul Qadeer", email = "abqdr.is@gmail.com" }, + { name = "Dan Shirts", email = "dan@ecobattery.com" } +] +requires-python = ">=3.11" +dependencies = [ + "boto3>=1.40.0,<2", + "cachetools>=6.2.0,<7", + "casbin==1.14.0,<2", +] + +[dependency-groups] +dev = [ + "pytest>=8.4.0", + "boto3-stubs>=1.40.0,<2", + "types-cachetools", + "pytest-cov>=7.0.0", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.mypy] +ignore_missing_imports = true + +[tool.pytest.ini_options] +testpaths = "tests" + +[tool.ruff] +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv", + ".vscode", + ".pytest_cache", + ".build", + ".mypy_cache", + ".github" +] + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "C", # flake8-comprehensions + "B", # flake8-bugbear +] +ignore = [ "E203", "E266", "E501", "W191", "C901"] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +line-ending = "auto" diff --git a/python_dycasbin/adapter.py b/python_dycasbin/adapter.py index 0748c90..1a5fc5e 100644 --- a/python_dycasbin/adapter.py +++ b/python_dycasbin/adapter.py @@ -1,180 +1,312 @@ import hashlib +from typing import Any, Iterable import boto3 -from casbin import persist +from cachetools import TTLCache, cached +from casbin import Model, persist class Adapter(persist.Adapter): - """the interface for Casbin adapters.""" - - def __init__(self, table_name='casbin_rule', **kwargs): + """DynamoDB adopter for casbin + + Args: + table_name: Dynamodb table name + provision_policy_table: Provision a policy table if it doesnt exist + table_definition: (Optional) Dynamodb table defintion. Please review the code to ensure you create a table with the proper structure. + also see (https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb/client/create_table.html) + table_provisioned_read_capacity: (Optional) Table read capacity units + table_provisioned_write_capacity: (Optional) Table write capacity units + table_billing_mode: (Optional) Table billing mode + kwargs: Additional kwargs are passed to dynamodb client + """ + + def __init__( + self, + table_name: str = "casbin_rule", + *, + table_create_table: bool = True, + table_definition: dict | None = None, + table_provisioned_read_capacity: int = 10, + table_provisioned_write_capacity: int = 10, + table_billing_mode: str = "PAY_PER_REQUEST", + aws_endpoint_url: str | None = None, + aws_region_name: str | None = None, + aws_access_key_id: str | None = None, + aws_secret_access_key: str | None = None, + aws_session_token: str | None = None, + aws_use_ssl: bool | None = None, + aws_verify: bool | None = None, + aws_account_id: str | None = None, + ) -> None: """create connection and dynamodb table""" + self.WRITE_BATCH_SIZE = 25 # dynamodb batch size self.table_name = table_name - self.dynamodb = boto3.client('dynamodb', **kwargs) - self.dynamodb_resource = boto3.resource('dynamodb', **kwargs) - - try: - - self.dynamodb.create_table( - TableName=self.table_name, + self.aws_endpoint_url = aws_endpoint_url + self.aws_region_name = aws_region_name + self.aws_access_key_id = aws_access_key_id + self.aws_secret_access_key = aws_secret_access_key + self.aws_session_token = aws_session_token + self.aws_account_id = aws_account_id + self.aws_use_ssl = aws_use_ssl + self.aws_verify = aws_verify + self.aws_account_id = aws_account_id + + if table_create_table: + self._provision_table( + table_name, + table_definition, + table_provisioned_read_capacity, + table_provisioned_write_capacity, + table_billing_mode, + ) - AttributeDefinitions=[ - { - 'AttributeName': 'id', - 'AttributeType': 'S' - } + @cached(cache=TTLCache(maxsize=1, ttl=300)) + def _get_db_handler(self): + """Cache the dynamodb handler""" + dynamodb = boto3.client( + "dynamodb", + region_name=self.aws_region_name, + use_ssl=self.aws_use_ssl, + verify=self.aws_verify, + endpoint_url=self.aws_endpoint_url, + aws_access_key_id=self.aws_access_key_id, + aws_secret_access_key=self.aws_secret_access_key, + aws_session_token=self.aws_session_token, + aws_account_id=self.aws_account_id, + ) + return dynamodb + + def _provision_table( + self, + table_name: str, + table_definition: dict | None, + provisioned_read_capacity: int, + provisioned_write_capacity: int, + table_billing_mode: str, + ) -> None: + """Provision the dynamodb table""" + table_attribute_dfinitions = [ + {"AttributeName": "id", "AttributeType": "S"}, + {"AttributeName": "v0", "AttributeType": "S"}, + {"AttributeName": "v1", "AttributeType": "S"}, + ] + + table_key_schema = [ + {"AttributeName": "id", "KeyType": "HASH"}, + ] + + table_global_secondary_indexes = [ + { + "IndexName": "v0-v1-index", + "KeySchema": [ + {"AttributeName": "v0", "KeyType": "HASH"}, + {"AttributeName": "v1", "KeyType": "RANGE"}, ], - KeySchema=[ - { - 'AttributeName': 'id', - 'KeyType': 'HASH' - }, + "Projection": { + "ProjectionType": "ALL", + }, + }, + { + "IndexName": "v1-v0-index", + "KeySchema": [ + {"AttributeName": "v1", "KeyType": "HASH"}, + {"AttributeName": "v0", "KeyType": "RANGE"}, ], - ProvisionedThroughput={ - 'ReadCapacityUnits': 10, - 'WriteCapacityUnits': 10 - } - ) - except self.dynamodb.exceptions.ResourceInUseException: + "Projection": { + "ProjectionType": "ALL", + }, + }, + ] + + table_provisioned_throughput = { + "ReadCapacityUnits": provisioned_read_capacity, + "WriteCapacityUnits": provisioned_write_capacity, + } + + dynamodb = self._get_db_handler() + + try: + if table_definition is not None: + dynamodb.create_table(**table_definition) + else: + dynamodb.create_table( + TableName=table_name, + AttributeDefinitions=table_attribute_dfinitions, + KeySchema=table_key_schema, + ProvisionedThroughput=table_provisioned_throughput, + GlobalSecondaryIndexes=table_global_secondary_indexes, + BillingMode=table_billing_mode, + ) + except dynamodb.exceptions.ResourceInUseException: pass - def update_policy(self, sec, ptype, old_rule, new_rule): + def _write_batch(self, batch: list) -> None: + """Batch multiple writes to improve performance.""" + dynamodb = self._get_db_handler() + request_items = {self.table_name: batch} + + while request_items: + response = dynamodb.batch_write_item(RequestItems=request_items) + request_items = response.get("UnprocessedItems", {}) + + def update_policy( + self, sec: str, ptype: str, old_rule: Iterable, new_rule: Iterable + ) -> bool: self.add_policy(sec, ptype, new_rule) self.remove_policy(sec, ptype, old_rule) return True - def get_filtered_item(self, ptype, rules): - exp_attr = {} - exp_attr[':ptype'] = {'S': ptype} - filter_exp = [] - filter_exp.append('ptype = :ptype') + def get_filtered_item(self, ptype: str, rules: Iterable) -> dict: + dynamodb = self._get_db_handler() + exp_attr = {":ptype": {"S": ptype}} + filter_exp_list = [] + filter_exp_list.append("ptype = :ptype") for i, rule in enumerate(rules): - exp_attr[':v{}'.format(i)] = {'S': rule} - filter_exp.append('v{} = :v{}'.format(i, i)) + exp_attr[":v{}".format(i)] = {"S": rule} + filter_exp_list.append("v{} = :v{}".format(i, i)) - filter_exp = ' and '.join(filter_exp) + filter_exp = " and ".join(filter_exp_list) - response = self.dynamodb.scan( + response = dynamodb.scan( ExpressionAttributeValues=exp_attr, FilterExpression=filter_exp, TableName=self.table_name, ) - data = response['Items'] + data = response.get("Items", {}) - while 'LastEvaluatedKey' in response: - response = self.dynamodb.scan( - ExclusiveStartKey=response['LastEvaluatedKey']) - data.extend(response['Items']) + while "LastEvaluatedKey" in response: + response = dynamodb.scan(ExclusiveStartKey=response["LastEvaluatedKey"]) + data.extend(response["Items"]) return data - def load_policy(self, model): - """load all policies from database""" - response = self.dynamodb.scan(TableName=self.table_name) - - for i in response['Items']: + def load_policy_lines(self, response: dict, model: Model) -> None: + for i in response["Items"]: persist.load_policy_line(self.get_line_from_item(i), model) - while 'LastEvaluatedKey' in response: - response = self.dynamodb.scan( - TableName=self.table_name, - ExclusiveStartKey=response['LastEvaluatedKey']) + def load_policy(self, model: Model): + """load all policies from database""" + dynamodb = self._get_db_handler() + response = dynamodb.scan(TableName=self.table_name) + self.load_policy_lines(response, model) - for i in response['Items']: - persist.load_policy_line(self.get_line_from_item(i), model) + def load_filtered_policy_by_sub(self, model: Model, sub: str) -> None: + dynamodb = self._get_db_handler() + response = dynamodb.query( + TableName=self.table_name, + IndexName="v0-v1-index", + Select="ALL_ATTRIBUTES", + KeyConditionExpression="v0 = :v0", + ExpressionAttributeValues={":v0": {"S": sub}}, + ) + self.load_policy_lines(response, model) - # To forcefully break the loop when testing - if "LastEvaluatedKey" in response and response["LastEvaluatedKey"] == "from_pytest": - break + def load_filtered_policy_by_obj(self, model: Model, obj: str) -> None: + dynamodb = self._get_db_handler() + response = dynamodb.query( + TableName=self.table_name, + IndexName="v1-v0-index", + Select="ALL_ATTRIBUTES", + KeyConditionExpression="v1 = :v1", + ExpressionAttributeValues={":v1": {"S": obj}}, + ) + self.load_policy_lines(response, model) - def get_line_from_item(self, item): + def get_line_from_item(self, item: dict[str, Any]) -> str: """make casbin policy string from dynamodb item""" - line = item['ptype']['S'] + line = item["ptype"]["S"] i = 0 while i < len(item) - 2: - line = '{}, {}'.format(line, item['v{}'.format(i)]['S']) + line = "{}, {}".format(line, item["v{}".format(i)]["S"]) i = i + 1 return line - def get_md5(self, line): + def get_md5(self, line: Iterable): """convert policy line to MD5 hash to be used as "id" """ m = hashlib.md5() - m.update(str(line).encode('utf-8')) + m.update(str(line).encode("utf-8")) return m.hexdigest() - def convert_to_item(self, ptype, rule): + def convert_to_item(self, ptype: str, rule: Iterable): """change casbin policy string to dynamodb item""" - line = {} - line['ptype'] = {} - line['ptype']['S'] = ptype + line = {"ptype": {"S": ptype}} for i, v in enumerate(rule): - line['v{}'.format(i)] = {} - line['v{}'.format(i)]['S'] = v + line["v{}".format(i)] = {} + line["v{}".format(i)]["S"] = v - line['id'] = {} - line['id']['S'] = self.get_md5(line) + line["id"] = {"S": self.get_md5(line)} return line - def _save_policy_line(self, ptype, rule): - """save a policy line in the dynamodb""" - line = self.convert_to_item(ptype, rule) - self.dynamodb.put_item(TableName=self.table_name, Item=line) + def save_policy(self, model: Model) -> bool: + """Save all policy rules to DynamoDB.""" + write_requests = [] - def save_policy(self, model): - """saves all policy rules to the storage.""" for sec in ["p", "g"]: - if sec not in model.model.keys(): + if sec not in model.model: continue + for ptype, ast in model.model[sec].items(): for rule in ast.policy: - self._save_policy_line(ptype, rule) - return True + item = self.convert_to_item(ptype, rule) + write_requests.append({"PutRequest": {"Item": item}}) + + if len(write_requests) == self.WRITE_BATCH_SIZE: + self._write_batch(write_requests) + write_requests = [] + + if write_requests: + self._write_batch(write_requests) - def add_policy(self, sec, ptype, rule): - """adds a policy rule to the storage.""" - self._save_policy_line(ptype, rule) + return True - def remove_policy(self, sec, ptype, rule): - """removes a policy rule from the storage.""" + def add_policy(self, _: str, ptype: str, rule: Iterable) -> None: + """adds a single policy rule to the storage.""" + dynamodb = self._get_db_handler() line = self.convert_to_item(ptype, rule) + dynamodb.put_item(TableName=self.table_name, Item=line) - _id = line['id']['S'] + def remove_policy(self, _: str, ptype: str, rule: Iterable) -> bool: + """removes a single policy rule from the storage.""" + dynamodb = self._get_db_handler() + line = self.convert_to_item(ptype, rule) - self.dynamodb.delete_item( - Key={ - 'id': { - 'S': _id, - } - }, + dynamodb.delete_item( + Key={"id": {"S": line["id"]["S"]}}, TableName=self.table_name, ) return True - def remove_filtered_policy(self, sec, ptype, field_index, *field_values): - """removes policy rules that match the filter from the storage. - This is part of the Auto-Save feature. - """ + def remove_filtered_policy( + self, _: str, ptype: str, field_index: int, *field_values: Iterable + ) -> bool: + """Removes policy rules that match the filter from the storage.""" + if not (0 <= field_index <= 5): return False if not (1 <= field_index + len(field_values) <= 6): return False matched_rules = self.get_filtered_item(ptype, list(field_values)) - table = self.dynamodb_resource.Table(self.table_name) - - with table.batch_writer() as batch: - for each in matched_rules: - batch.delete_item( - Key={ - 'id': each['id']['S'], - } - ) + if not matched_rules: + return True + + batch = [] + + for rule in matched_rules: + batch.append({"DeleteRequest": {"Key": {"id": rule["id"]}}}) + + if len(batch) == self.WRITE_BATCH_SIZE: + self._write_batch(batch) + batch = [] + + if batch: + self._write_batch(batch) return True diff --git a/python_dycasbin/e2e/test_management.py b/python_dycasbin/e2e/test_management.py deleted file mode 100644 index 37d8ef3..0000000 --- a/python_dycasbin/e2e/test_management.py +++ /dev/null @@ -1,290 +0,0 @@ -import casbin -import os -from python_dycasbin import adapter -import boto3 - - -os.environ['AWS_ACCESS_KEY_ID'] = 'key' -os.environ['AWS_SECRET_ACCESS_KEY'] = 'anything' -os.environ['AWS_DEFAULT_REGION'] = 'us-east-1' - -client = boto3.client('dynamodb', endpoint_url="http://localhost:8000") -table_name = 'casbin_rule' - -adapter = adapter.Adapter(endpoint_url="http://localhost:8000") -e = casbin.Enforcer("python_dycasbin/e2e/model.conf", adapter) - - -def test_add_policy_get_policy(): - - policy = ['ham', 'data4', 'write'] - e.add_policy(*policy) - - policies = e.get_policy() - - assert policies == [policy] - - -def test_enforcer(): - - result_true = e.enforce('ham', 'data4', 'write') - - result_false = e.enforce('ham', 'data5', 'write') - - assert result_true == True - assert result_false == False - - -def test_enforce_ex(): - result_true = e.enforce_ex('ham', 'data4', 'write') - result_false = e.enforce_ex('ham', 'data5', 'write') - - assert result_true == (True, ['ham', 'data4', 'write']) - assert result_false == (False, []) - - -def test_get_all_subjects(): - - e.add_policy('alice', 'data5', 'read') - - result = e.get_all_subjects() - - assert result == ['ham', 'alice'] - - -def test_get_all_named_subjects(): - - result = e.get_all_named_subjects('p') - - assert result == ['ham', 'alice'] - - -def test_get_all_objects(): - - e.add_policy('alice', 'data5', 'read') - - result = e.get_all_objects() - - assert result == ['data4', 'data5'] - - -def test_get_all_named_objects(): - - result = e.get_all_named_objects('p') - - assert result == ['data4', 'data5'] - - e.get_all_objects - - -def test_get_all_actions(): - result = e.get_all_actions() - - assert result == ['write', 'read'] - - -def test_get_all_named_actions(): - result = e.get_all_named_actions('p') - - assert result == ['write', 'read'] - - -def test_add_role_for_user(): - result = e.add_role_for_user('bob', 'admin') - - assert result == True - - -def test_get_all_roles(): - result = e.get_all_roles() - - assert result == ['admin'] - - -def test_get_all_named_roles(): - result = e.get_all_named_roles('g') - - assert result == ['admin'] - - -def test_get_filtered_policy(): - result = e.get_filtered_policy(0, 'alice') - assert result == [['alice', 'data5', 'read']] - - -def test_get_named_policy(): - result = e.get_named_policy('p') - assert result == [['ham', 'data4', 'write'], ['alice', 'data5', 'read']] - - -def test_get_filtered_named_policy(): - result = e.get_filtered_named_policy('p', 0, 'ham') - assert result == [['ham', 'data4', 'write']] - - -def test_get_grouping_policy(): - result = e.get_grouping_policy() - assert result == [['bob', 'admin']] - - -def test_get_filtered_grouping_policy(): - result = e.get_filtered_grouping_policy(0, 'bob') - assert result == [['bob', 'admin']] - - -def test_get_named_grouping_policy(): - result = e.get_named_grouping_policy('g') - assert result == [['bob', 'admin']] - - -def test_get_named_grouping_policy(): - result = e.get_filtered_named_grouping_policy('g', 0, 'bob') - assert result == [['bob', 'admin']] - - -def test_has_policy(): - result = e.has_policy('ham', 'data4', 'write') - assert result == True - result = e.has_policy('ham', 'data4', 'read') - assert result == False - - -def test_has_named_policy(): - result = e.has_named_policy('p', 'ham', 'data4', 'write') - assert result == True - result = e.has_policy('p', 'ham', 'data4', 'read') - assert result == False - - -def test_add_named_policy(): - name_policy = e.add_named_grouping_policy('g', 'ham', 'data_g', 'read') - assert name_policy == True - - -def test_remove_policy(): - result = e.remove_policy('ham', 'data4', 'write') - assert result == True - result = e.get_policy() - assert result == [['alice', 'data5', 'read']] - - -def test_remove_policies(): - # Not implemented - result = e.remove_policies('ham') - assert result == False - - -def test_remove_filtered_policy(): - result = e.remove_filtered_policy(0, 'alice', 'data5', 'read') - assert result == True - result = e.get_policy() - assert result == [] - - -def test_remove_named_policy(): - r = e.add_named_policy('p', 'david', 'files', 'read') - result = e.remove_named_policy('p', 'david', 'files', 'read') - assert result == True - result = e.get_policy() - assert result == [] - - -def test_remove_policies(): - # Not implemented - result = e.remove_named_policies('p', 'ham') - assert result == False - - -def test_remove_filtered_named_policy(): - r = e.add_named_policy('p', 'david', 'images', 'write') - result = e.remove_filtered_named_policy('p', 0, 'david', 'images', 'write') - assert result == True - result = e.get_policy() - assert result == [] - - -def test_has_grouping_policy(): - result = e.has_grouping_policy('bob', 'admin') - assert result == True - result = e.has_grouping_policy('bob', 'admin2') - assert result == False - - -def test_has_named_grouping_policy(): - result = e.has_named_grouping_policy('g', 'bob', 'admin') - assert result == True - result = e.has_named_grouping_policy('g', 'bob', 'admin2') - assert result == False - - -def test_add_grouping_policy(): - result = e.add_grouping_policy('group1', 'data2_admin') - assert result == True - result = e.has_grouping_policy('group1', 'data2_admin') - assert result == True - - -def test_add_grouping_policies(): - # Not implemented - result = e.add_grouping_policies([ - ['ham', 'data4_admin'], - ['jack', 'data5_admin'] - ]) - assert result == False - - -def test_remove_grouping_policy(): - # Not implemented - result = e.remove_grouping_policy('group1', 'data2_admin') - assert result == True - result = e.has_grouping_policy('group1', 'data2_admin') - assert result == False - - -def test_remove_grouping_policies(): - # Not implemented - e.add_grouping_policy('group1', 'data2_admin') - result = e.remove_grouping_policies([ - ['group1', 'data2_admin'] - ]) - assert result == False - - -def test_remove_filtered_grouping_policy(): - result = e.remove_filtered_grouping_policy(0, 'group1') - assert result == [] - - -def test_remove_named_grouping_policy(): - pass - - -def test_remove_named_grouping_policies(): - result = e.remove_named_grouping_policies('g', [ - ['ham', 'data4_admin'], - ['jack', 'data5_admin'] - ]) - - assert result == False - - -def test_remove_named_grouping_policy(): - e.add_grouping_policy('group1', 'data2_admin') - result = e.remove_filtered_named_grouping_policy('g', 0, 'group1') - - assert result == [['group1', 'data2_admin']] - - -def test_update_policy(): - result = e.add_policy('sam', 'files', 'read') - result = e.get_policy() - assert result == [['sam', 'files', 'read']] - result = e.update_policy(['sam', 'files', 'read'], ['sam', 'files', 'write']) - assert result == True - - -def test_update_policies(): - # not implemented - result = e.update_policies(['sam', 'files', 'read'], [ - 'sam', 'files', 'write']) - assert result == False diff --git a/python_dycasbin/e2e/test_rbac.py b/python_dycasbin/e2e/test_rbac.py deleted file mode 100644 index f636fda..0000000 --- a/python_dycasbin/e2e/test_rbac.py +++ /dev/null @@ -1,143 +0,0 @@ -import casbin -import os -from python_dycasbin import adapter -import boto3 - - -os.environ['AWS_ACCESS_KEY_ID'] = 'key' -os.environ['AWS_SECRET_ACCESS_KEY'] = 'anything' -os.environ['AWS_DEFAULT_REGION'] = 'us-east-1' - -client = boto3.client('dynamodb', endpoint_url="http://localhost:8000") -table_name = 'casbin_rule' - -adapter = adapter.Adapter(endpoint_url="http://localhost:8000") -e = casbin.Enforcer("python_dycasbin/e2e/model.conf", adapter) -# e = casbin.Enforcer("python_dycasbin/e2e/model.conf", "python_dycasbin/e2e/policy.csv" ) - - -def test_add_and_get_roles_for_user(): - e.add_role_for_user('ham', 'data_admin') - result = e.get_roles_for_user('ham') - assert result == ['data_admin'] - - -def test_get_users_for_role(): - result = e.get_users_for_role('data_admin') - assert result == ['ham'] - - -def test_has_role_for_user(): - result = e.has_role_for_user('ham', 'data_admin') - assert result == True - result = e.has_role_for_user('sam', 'data_admin') - assert result == False - - -def test_delete_role_for_user(): - result = e.delete_role_for_user('ham', 'data_admin') - assert result == True - - -def test_delete_roles_for_user(): - e.add_role_for_user('ham', 'data_admin') - result = e.delete_roles_for_user('ham') - assert result == [['ham', 'data_admin']] - result = e.has_role_for_user('ham', 'data_admin') - assert result == False - - -def test_delete_user(): - e.add_role_for_user('ham', 'data_admin') - result = e.has_role_for_user('ham', 'data_admin') - result = e.delete_user('ham') - assert result == [['ham', 'data_admin']] - result = e.has_role_for_user('ham', 'data_admin') - assert result == False - - -def test_delete_role(): - e.add_role_for_user('sam', 'data_admin') - result = e.has_role_for_user('sam', 'data_admin') - result = e.delete_role('data_admin') - assert result == [['sam', 'data_admin']] - result = e.has_role_for_user('sam', 'data_admin') - assert result == False - - -def test_add_permission_for_user(): - e.add_permission_for_user('bob', 'read') - result = e.has_permission_for_user('bob', 'read') - assert result == True - - -def test_delete_permission(): - e.add_permission_for_user('bob', 'read') - result = e.delete_permission('read') - assert result == True - result = e.has_permission_for_user('bob', 'read') - assert result == False - - -def test_add_permission_for_user(): - e.add_permission_for_user('alice', 'write') - result = e.delete_permission_for_user('alice', 'write') - assert result == True - result = e.has_permission_for_user('alice', 'write') - assert result == False - - -def test_delete_permissions_for_user(): - e.add_permission_for_user('alice', 'write') - e.add_permission_for_user('alice', 'read') - result = e.delete_permissions_for_user('alice') - assert result == True - result = e.has_permission_for_user('alice', 'write') - assert result == False - result = e.has_permission_for_user('alice', 'read') - assert result == False - - -def test_delete_permissions_for_user(): - e.add_permission_for_user('alice', 'write') - e.add_permission_for_user('alice', 'read') - result = e.get_permissions_for_user('alice') - assert result == [['alice', 'write'], ['alice', 'read']] - - -def test_get_users_for_role_in_domain(): - e.add_role_for_user_in_domain('ham', 'data_admin', 'domain1') - e.add_role_for_user_in_domain('sam', 'data2_admin', 'domain1') - e.add_role_for_user_in_domain('alice', 'data2_admin', 'domain1') - e.add_role_for_user_in_domain('sam', 'data2_admin', 'domain2') - result = e.get_users_for_role_in_domain('data2_admin', 'domain1') - result.sort() - assert result == ['alice', 'sam'] - - -def test_get_roles_for_user_in_domain(): - e.add_role_for_user_in_domain('alice', 'data3_admin', 'domain1') - e.add_role_for_user_in_domain('alice', 'data2_admin', 'domain1') - result = e.get_roles_for_user_in_domain('alice', 'domain1') - result.sort() - assert result == ['data2_admin', 'data3_admin'] - - -def test_get_permissions_for_user_in_domain(): - e.add_role_for_user_in_domain('alice', 'data3_admin', 'domain1') - e.add_role_for_user_in_domain('alice', 'data2_admin', 'domain1') - result = e.get_permissions_for_user_in_domain('alice', 'domain1') - assert result == [] - - -def test_get_permissions_for_user_in_domain(): - e.delete_roles_for_user_in_domain('alice', 'data3_admin', 'domain1') - result = e.get_roles_for_user_in_domain('alice', 'domain1') - result.sort() - assert result == ['data2_admin'] - - -def test_delete_table(): - client.delete_table( - TableName=table_name - ) diff --git a/python_dycasbin/test_adapter.py b/python_dycasbin/test_adapter.py deleted file mode 100644 index c031a9e..0000000 --- a/python_dycasbin/test_adapter.py +++ /dev/null @@ -1,79 +0,0 @@ -import boto3 -from python_dycasbin import adapter -from casbin.model.model import Model -import casbin -import python_dycasbin - - -table_name = 'casbin_rule' -endpoint_url = 'http://localhost:8000' -policy_line = 'mock,policy,line' - - -def test_init(mocker): - mocker.patch("boto3.client") - - obj = adapter.Adapter(endpoint_url=endpoint_url) - - assert obj.table_name == table_name - - boto3.client.assert_called_with( - 'dynamodb', endpoint_url=endpoint_url) - - boto3.client.return_value.create_table.assert_called_with( - AttributeDefinitions=[ - {'AttributeName': 'id', 'AttributeType': 'S'}], - KeySchema=[ - {'AttributeName': 'id', 'KeyType': 'HASH'}], - ProvisionedThroughput={ - 'ReadCapacityUnits': 10, 'WriteCapacityUnits': 10}, - TableName='casbin_rule' - ) - - -def test_load_policy(mocker, monkeypatch): - model = Model() - mocker.patch("boto3.client") - mocker.patch("casbin.persist.load_policy_line") - monkeypatch.setattr(adapter.Adapter, "get_line_from_item", - mock_get_line_from_item) - - boto3.client.return_value.scan.return_value = {"Items": [{}]} - - obj = adapter.Adapter() - obj.load_policy(model) - - boto3.client.return_value.scan.assert_called_with(TableName=table_name) - casbin.persist.load_policy_line.assert_called_with(policy_line, model) - - -def test_load_polic_with_LastEvaluatedKey(mocker, monkeypatch): - last_evaluated_key = "from_pytest" - model = Model() - mocker.patch("boto3.client") - mocker.patch("casbin.persist.load_policy_line") - monkeypatch.setattr(adapter.Adapter, "get_line_from_item", - mock_get_line_from_item) - - boto3.client.return_value.scan.return_value = { - "Items": [{}], "LastEvaluatedKey": last_evaluated_key} - - obj = adapter.Adapter() - obj.load_policy(model) - - boto3.client.return_value.scan.assert_called_with( - TableName=table_name, ExclusiveStartKey=last_evaluated_key) - casbin.persist.load_policy_line.assert_called_with(policy_line, model) - - -def test_get_line_from_item(mocker): - mocker.patch("boto3.client") - - obj = adapter.Adapter() - result = obj.get_line_from_item( - {"id": "rand_id", "ptype": {"S": "p"}, "v0": {"S": "user1"}}) - assert result == 'p, user1' - - -def mock_get_line_from_item(itme, model): - return policy_line diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 27180a9..0000000 --- a/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -casbin==1.14.0 -pytest-mock==2.0.0 -pytest==6.2.5 -boto3==1.12.26 \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index 1cd0089..0000000 --- a/setup.py +++ /dev/null @@ -1,22 +0,0 @@ -import setuptools -with open("README.md", "r") as fh: - long_description = fh.read() -setuptools.setup( - name='python_dycasbin', - version='0.3.1', - author="Abdul Qadeer", - author_email="abqdr.is@gmail.com", - description="DynamoDB adopter for casbin", - long_description=long_description, - long_description_content_type="text/markdown", - url="https://github.com/abqadeer/python-dynacasbin", - keywords=["casbin" "dynamodb", "aws", "boto3", "casbin-adapter", - "rbac", "access control", "abac", "acl", "permission"], - install_requires=['casbin>=0.8.4', "boto3>=1.4.0"], - packages=setuptools.find_packages(), - classifiers=[ - "Programming Language :: Python :: 3", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - ], -) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/e2e/__init__.py b/tests/e2e/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python_dycasbin/e2e/model.conf b/tests/e2e/rbac_model.conf similarity index 71% rename from python_dycasbin/e2e/model.conf rename to tests/e2e/rbac_model.conf index bf27a49..71159e3 100644 --- a/python_dycasbin/e2e/model.conf +++ b/tests/e2e/rbac_model.conf @@ -4,11 +4,11 @@ r = sub, obj, act [policy_definition] p = sub, obj, act +[role_definition] +g = _, _ + [policy_effect] e = some(where (p.eft == allow)) [matchers] -m = r.sub == p.sub && r.obj == p.obj && r.act == p.act - -[role_definition] -g = _, _ \ No newline at end of file +m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act \ No newline at end of file diff --git a/tests/e2e/rbac_with_domains_model.conf b/tests/e2e/rbac_with_domains_model.conf new file mode 100644 index 0000000..57c3721 --- /dev/null +++ b/tests/e2e/rbac_with_domains_model.conf @@ -0,0 +1,14 @@ +[request_definition] +r = sub, dom, obj, act + +[policy_definition] +p = sub, dom, obj, act + +[role_definition] +g = _, _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.act \ No newline at end of file diff --git a/tests/e2e/test_management.py b/tests/e2e/test_management.py new file mode 100644 index 0000000..31b3013 --- /dev/null +++ b/tests/e2e/test_management.py @@ -0,0 +1,295 @@ +import unittest + +import boto3 +import casbin + +from python_dycasbin import adapter + + +class TestManagement(unittest.TestCase): + def setUp(self): + self.table_name = "casbin_rule" + self.aws_endpoint_url = "http://localhost:8000" + self.aws_region_name = "us-east-1" + self.aws_access_key_id = "anything" + self.aws_secret_access_key = "anything" + self.aws_use_ssl = False + self.aws_verify = False + + test_adapter = adapter.Adapter( + table_name=self.table_name, + aws_endpoint_url=self.aws_endpoint_url, + aws_region_name=self.aws_region_name, + aws_access_key_id=self.aws_access_key_id, + aws_secret_access_key=self.aws_secret_access_key, + aws_use_ssl=self.aws_use_ssl, + aws_verify=self.aws_verify, + ) + + self.e = casbin.Enforcer("tests/e2e/rbac_model.conf", test_adapter) + + def test_add_policy_get_policy(self): + policy = ["1suuj7Lzff", "data4", "write"] + self.e.add_policy(*policy) + self.e.add_policy("1suuj7Lzff", "data4", "write") + policies = self.e.get_policy() + self.assertIn(policy, policies) + + def test_enforcer(self): + self.e.add_policy("vH5A0DLZAl", "data4", "write") + result_true = self.e.enforce("vH5A0DLZAl", "data4", "write") + self.assertTrue(result_true) + result_false = self.e.enforce("vH5A0DLZAl", "data5", "write") + self.assertFalse(result_false) + + def test_enforce_ex(self): + result_true = self.e.enforce_ex("OwR8cXikqw", "data4", "write") + self.assertTrue(result_true) + result_false = self.e.enforce_ex("OwR8cXikqw", "data5", "write") + self.assertFalse(result_false[0]) + + def test_get_all_subjects(self): + self.e.add_policy("bBV8kAeTuk", "data5", "read") + result = self.e.get_all_subjects() + self.assertIn("bBV8kAeTuk", result) + + def test_get_all_named_subjects(self): + self.e.add_policy("qw1NGNQOq0", "data5", "read") + self.e.add_policy("gAZgBkYwyb", "data5", "read") + self.e.add_policy("y9enHfJMGG", "data5", "read") + result = self.e.get_all_named_subjects("p") + self.assertIn("qw1NGNQOq0", result) + self.assertIn("gAZgBkYwyb", result) + self.assertIn("y9enHfJMGG", result) + + def test_get_all_objects(self): + self.e.add_policy("MROaMgeena", "data5", "read") + result = self.e.get_all_objects() + self.assertIn("data5", result) + + def test_get_all_named_objects(self): + self.e.add_policy("FPPcCTJhfg", "new_obj1", "read") + result = self.e.get_all_named_objects("p") + self.assertIn("new_obj1", result) + + def test_get_all_actions(self): + self.e.add_policy("5Ux56gXVjp", "data1", "X7zaTl7kwr") + self.e.add_policy("5Ux56gXVjp", "data1", "tQIxcrfHvW") + result = self.e.get_all_actions() + self.assertIn("X7zaTl7kwr", result) + self.assertIn("tQIxcrfHvW", result) + self.assertNotIn("IuTFEeQxIg", result) + + def test_get_all_named_actions(self): + self.e.add_policy("riyBrhVml6", "data1", "UYQNQCiG8f") + self.e.add_policy("riyBrhVml6", "data1", "ZJBlXoY03y") + result = self.e.get_all_named_actions("p") + self.assertIn("UYQNQCiG8f", result) + self.assertIn("ZJBlXoY03y", result) + self.assertNotIn("IuTFEeQxIg", result) + + def test_add_role_for_user(self): + result = self.e.add_role_for_user("nm8aE7o5HR", "RmqbX5ELvs") + self.assertTrue(result) + + def test_get_all_roles(self): + self.e.add_role_for_user("dghSEaQzbS", "CBtqJR6EJw") + result = self.e.get_all_roles() + self.assertIn("CBtqJR6EJw", result) + + def test_get_all_named_roles(self): + self.e.add_role_for_user("mMLyjr3ozn", "WGet7EpgS0") + result = self.e.get_all_named_roles("g") + self.assertIn("WGet7EpgS0", result) + + def test_get_filtered_policy(self): + self.e.add_policy("cb1znyqD7T", "data5", "read") + result = self.e.get_filtered_policy(0, "cb1znyqD7T") + self.assertIn(["cb1znyqD7T", "data5", "read"], result) + + def test_get_named_policy(self): + self.e.add_policy("aPmDWx33SH", "data4", "write") + self.e.add_policy("mfHM74BXBg", "data5", "read") + result = self.e.get_named_policy("p") + self.assertIn(["aPmDWx33SH", "data4", "write"], result) + self.assertNotIn(["aPmDWx33SH", "data4", "read"], result) + self.assertIn(["mfHM74BXBg", "data5", "read"], result) + self.assertNotIn(["mfHM74BXBg", "data4", "read"], result) + + def test_get_filtered_named_policy(self): + self.e.add_policy("bavADh0Awa", "data4", "write") + result = self.e.get_filtered_named_policy("p", 0, "bavADh0Awa") + self.assertIn(["bavADh0Awa", "data4", "write"], result) + self.assertNotIn(["bavADh0Awa", "data4", "read"], result) + + def test_get_grouping_policy(self): + self.e.add_grouping_policy("dghSEaQzbS", "admin") + result = self.e.get_grouping_policy() + self.assertIn(["dghSEaQzbS", "admin"], result) + + def test_get_filtered_grouping_policy(self): + self.e.add_grouping_policy("zPhvX3tU77", "admin") + result = self.e.get_filtered_grouping_policy(0, "zPhvX3tU77") + self.assertIn(["zPhvX3tU77", "admin"], result) + + def test_get_named_grouping_policy_case1(self): + self.e.add_grouping_policy("S8v6yInuhY", "admin") + result = self.e.get_named_grouping_policy("g") + self.assertIn(["S8v6yInuhY", "admin"], result) + + def test_get_named_grouping_policy_case2(self): + self.e.add_grouping_policy("7zyYG5fqM9", "admin") + result = self.e.get_filtered_named_grouping_policy("g", 0, "7zyYG5fqM9") + self.assertIn(["7zyYG5fqM9", "admin"], result) + + def test_has_policy(self): + self.e.add_policy("xgt0MQOMen", "data4", "write") + result = self.e.has_policy("xgt0MQOMen", "data4", "write") + self.assertTrue(result) + result = self.e.has_policy("xgt0MQOMen", "data4", "read") + self.assertFalse(result) + + def test_has_named_policy(self): + self.e.add_policy("xUhT7Y6JI7", "data4", "write") + result = self.e.has_named_policy("p", "xUhT7Y6JI7", "data4", "write") + self.assertTrue(result) + result = self.e.has_policy("p", "xUhT7Y6JI7", "data4", "read") + self.assertFalse(result) + + def test_add_named_policy(self): + result = self.e.add_named_grouping_policy("g", "ilYTMKV6X7", "data_g", "read") + self.assertTrue(result) + + def test_remove_policy(self): + self.e.add_policy("urnyBULgF4", "data5", "write") + self.e.add_policy("urnyBULgF4", "data5", "read") + result = self.e.remove_policy("urnyBULgF4", "data5", "write") + self.assertTrue(result) + result = self.e.get_policy() + self.assertNotIn(["urnyBULgF4", "data5", "write"], result) + self.assertIn(["urnyBULgF4", "data5", "read"], result) + + def test_remove_policies_case1(self): + # Not implemented + result = self.e.remove_policies("K2IAjeepBh") + self.assertFalse(result) + + def test_remove_filtered_policy(self): + self.e.add_policy("e2pMnwUGc1", "data5", "read") + result = self.e.remove_filtered_policy(0, "e2pMnwUGc1", "data5", "read") + self.assertTrue(result) + result = self.e.get_policy() + self.assertListEqual([], result) + + def test_remove_named_policy(self): + self.e.add_named_policy("p", "P9vmnUdxqh", "files", "read") + result = self.e.remove_named_policy("p", "P9vmnUdxqh", "files", "read") + self.assertTrue(result) + result = self.e.get_policy() + self.assertListEqual([], result) + + def test_remove_policies_case2(self): + # Not implemented + result = self.e.remove_named_policies("p", "ccnurObEST") + self.assertFalse(result) + + def test_remove_filtered_named_policy(self): + self.e.add_named_policy("p", "6XRDpwNsvw", "images", "write") + result = self.e.remove_filtered_named_policy( + "p", 0, "6XRDpwNsvw", "images", "write" + ) + self.assertTrue(result) + result = self.e.get_policy() + self.assertListEqual([], result) + + def test_has_grouping_policy(self): + self.e.add_grouping_policy("FXhT9RBzAo", "admin") + result = self.e.has_grouping_policy("FXhT9RBzAo", "admin") + self.assertTrue(result) + result = self.e.has_grouping_policy("FXhT9RBzAo", "admin2") + self.assertFalse(result) + + def test_has_named_grouping_policy(self): + self.e.add_grouping_policy("elnox0bgjh", "admin") + result = self.e.has_named_grouping_policy("g", "elnox0bgjh", "admin") + self.assertTrue(result) + result = self.e.has_named_grouping_policy("g", "elnox0bgjh", "admin2") + self.assertFalse(result) + + def test_add_grouping_policy(self): + result = self.e.add_grouping_policy("sHyJP3Yxj8", "data2_admin") + self.assertTrue(result) + result = self.e.has_grouping_policy("sHyJP3Yxj8", "data2_admin") + self.assertTrue(result) + + def test_add_grouping_policies(self): + # Not implemented + result = self.e.add_grouping_policies( + [["jIJv0pqSRr", "data4_admin"], ["eybi6hf6m0", "data5_admin"]] + ) + self.assertFalse(result) + + def test_remove_grouping_policy(self): + # Not implemented + self.e.add_grouping_policy("oLsENrAEom", "data2_admin") + result = self.e.remove_grouping_policy("oLsENrAEom", "data2_admin") + self.assertTrue(result) + result = self.e.has_grouping_policy("oLsENrAEom", "data2_admin") + self.assertFalse(result) + + def test_remove_grouping_policies(self): + # Not implemented + self.e.add_grouping_policy("uFRsrJ2gJe", "data2_admin") + result = self.e.remove_grouping_policies([["uFRsrJ2gJe", "data2_admin"]]) + self.assertFalse(result) + + def test_remove_filtered_grouping_policy(self): + result = self.e.remove_filtered_grouping_policy(0, "JsT6BOYJtf") + self.assertListEqual([], result) + + def test_remove_named_grouping_policies(self): + self.e.add_grouping_policy("IL8Cvh6sqL", "data4_admin") + self.e.add_grouping_policy("vVm78hI07v", "data5_admin") + result = self.e.remove_named_grouping_policies( + "g", [["IL8Cvh6sqL", "data4_admin"], ["vVm78hI07v", "data5_admin"]] + ) + self.assertFalse(result) + + def test_remove_named_grouping_policy_case2(self): + self.e.add_grouping_policy("z1oZ0MWTqW", "data2_admin") + result = self.e.remove_filtered_named_grouping_policy("g", 0, "z1oZ0MWTqW") + self.assertIn(["z1oZ0MWTqW", "data2_admin"], result) + + def test_update_policy(self): + result = self.e.add_policy("oXrsGpXPB6", "files", "read") + result = self.e.get_policy() + self.assertIn(["oXrsGpXPB6", "files", "read"], result) + result = self.e.update_policy( + ["oXrsGpXPB6", "files", "read"], ["oXrsGpXPB6", "files", "write"] + ) + self.assertTrue(result) + result = self.e.get_policy() + self.assertIn(["oXrsGpXPB6", "files", "write"], result) + + def test_update_policies(self): + # not implemented + result = self.e.update_policies( + ["wr0CBpB99O", "files", "read"], ["wr0CBpB99O", "files", "write"] + ) + self.assertFalse(result) + + def tearDown(self): + client = boto3.client( + "dynamodb", + endpoint_url=self.aws_endpoint_url, + region_name=self.aws_region_name, + aws_access_key_id=self.aws_access_key_id, + aws_secret_access_key=self.aws_secret_access_key, + use_ssl=self.aws_use_ssl, + verify=self.aws_verify, + ) + client.delete_table(TableName=self.table_name) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/e2e/test_rbac.py b/tests/e2e/test_rbac.py new file mode 100644 index 0000000..4bb1193 --- /dev/null +++ b/tests/e2e/test_rbac.py @@ -0,0 +1,147 @@ +import unittest + +import boto3 +import casbin + +from python_dycasbin import adapter + + +class TestRBAC(unittest.TestCase): + def setUp(self): + self.table_name = "casbin_rule" + self.aws_endpoint_url = "http://localhost:8000" + self.aws_region_name = "us-east-1" + self.aws_access_key_id = "anything" + self.aws_secret_access_key = "anything" + self.aws_use_ssl = False + self.aws_verify = False + + test_adapter = adapter.Adapter( + table_name=self.table_name, + aws_endpoint_url=self.aws_endpoint_url, + aws_region_name=self.aws_region_name, + aws_access_key_id=self.aws_access_key_id, + aws_secret_access_key=self.aws_secret_access_key, + aws_use_ssl=self.aws_use_ssl, + aws_verify=self.aws_verify, + ) + + self.e = casbin.Enforcer("tests/e2e/rbac_model.conf", test_adapter) + + def test_add_and_get_roles_for_user(self): + self.e.add_role_for_user("test_add_and_get_roles_for_user", "data_admin") + result = self.e.get_roles_for_user("test_add_and_get_roles_for_user") + self.assertListEqual(["data_admin"], result) + + def test_get_users_for_role(self): + self.e.add_role_for_user("test_get_users_for_role", "data_admin") + result = self.e.get_users_for_role("data_admin") + self.assertIn("test_get_users_for_role", result) + + def test_has_role_for_user(self): + self.e.add_role_for_user("test_has_role_for_user1", "data_admin") + result = self.e.has_role_for_user("test_has_role_for_user1", "data_admin") + self.assertTrue(result) + result = self.e.has_role_for_user("test_has_role_for_user2", "data_admin") + self.assertFalse(result) + + def test_delete_role_for_user(self): + self.e.add_role_for_user("test_delete_role_for_user", "data_admin") + result = self.e.delete_role_for_user("test_delete_role_for_user", "data_admin") + self.assertTrue(result) + + def test_delete_roles_for_user(self): + self.e.add_role_for_user("test_delete_roles_for_user", "data_admin") + result = self.e.delete_roles_for_user("test_delete_roles_for_user") + self.assertIsNot(False, result) + self.assertIn(["test_delete_roles_for_user", "data_admin"], result) # type: ignore + result = self.e.has_role_for_user("test_delete_roles_for_user", "data_admin") + self.assertFalse(result) + + def test_delete_user(self): + self.e.add_role_for_user("test_delete_user", "data_admin") + result = self.e.has_role_for_user("test_delete_user", "data_admin") + self.assertTrue(result) + result = self.e.delete_user("test_delete_user") + self.assertTrue(result) + result = self.e.has_role_for_user("test_delete_user", "data_admin") + self.assertFalse(result) + + def test_delete_role(self): + self.e.add_role_for_user("test_delete_role", "delete_role") + result = self.e.has_role_for_user("test_delete_role", "delete_role") + self.assertTrue(result) + result = self.e.delete_role("delete_role") + self.assertTrue(result) + result = self.e.has_role_for_user("test_delete_role", "delete_role") + self.assertFalse(result) + + def test_add_permission_for_user_case1(self): + self.e.add_permission_for_user("test_add_permission_for_user_case1", "read") + result = self.e.has_permission_for_user( + "test_add_permission_for_user_case1", "read" + ) + self.assertTrue(result) + + def test_delete_permission(self): + self.e.add_permission_for_user("test_delete_permission", "read") + result = self.e.delete_permission("read") + self.assertTrue(result) + result = self.e.has_permission_for_user("test_delete_permission", "read") + self.assertFalse(result) + + def test_add_permission_for_user_case2(self): + self.e.add_permission_for_user("test_add_permission_for_user_case2", "write") + result = self.e.has_permission_for_user( + "test_add_permission_for_user_case2", "write" + ) + self.assertTrue(result) + result = self.e.has_permission_for_user( + "test_add_permission_for_user_case2", "read" + ) + self.assertFalse(result) + + def test_delete_permissions_for_user_case1(self): + self.e.add_permission_for_user( + "test_delete_permissions_for_user_case1", "write" + ) + self.e.add_permission_for_user("test_delete_permissions_for_user_case1", "read") + result = self.e.delete_permissions_for_user( + "test_delete_permissions_for_user_case1" + ) + self.assertTrue(result) + result = self.e.has_permission_for_user( + "test_delete_permissions_for_user_case1", "write" + ) + self.assertFalse(result) + result = self.e.has_permission_for_user( + "test_delete_permissions_for_user_case1", "read" + ) + self.assertFalse(result) + + def test_delete_permissions_for_user_case2(self): + self.e.add_permission_for_user( + "test_delete_permissions_for_user_case2", "write" + ) + self.e.add_permission_for_user("test_delete_permissions_for_user_case2", "read") + result = self.e.get_permissions_for_user( + "test_delete_permissions_for_user_case2" + ) + self.assertIn(["test_delete_permissions_for_user_case2", "write"], result) + self.assertIn(["test_delete_permissions_for_user_case2", "read"], result) + + def tearDown(self): + client = boto3.client( + "dynamodb", + endpoint_url=self.aws_endpoint_url, + region_name=self.aws_region_name, + aws_access_key_id=self.aws_access_key_id, + aws_secret_access_key=self.aws_secret_access_key, + use_ssl=self.aws_use_ssl, + verify=self.aws_verify, + ) + client.delete_table(TableName=self.table_name) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/e2e/test_rbac_domains.py b/tests/e2e/test_rbac_domains.py new file mode 100644 index 0000000..0329b1c --- /dev/null +++ b/tests/e2e/test_rbac_domains.py @@ -0,0 +1,111 @@ +import unittest + +import boto3 +import casbin + +from python_dycasbin import adapter + + +class TestRBACWithDomains(unittest.TestCase): + def setUp(self): + self.table_name = "casbin_rule" + self.aws_endpoint_url = "http://localhost:8000" + self.aws_region_name = "us-east-1" + self.aws_access_key_id = "anything" + self.aws_secret_access_key = "anything" + self.aws_use_ssl = False + self.aws_verify = False + + test_adapter = adapter.Adapter( + table_name=self.table_name, + aws_endpoint_url=self.aws_endpoint_url, + aws_region_name=self.aws_region_name, + aws_access_key_id=self.aws_access_key_id, + aws_secret_access_key=self.aws_secret_access_key, + aws_use_ssl=self.aws_use_ssl, + aws_verify=self.aws_verify, + ) + + self.e = casbin.Enforcer("tests/e2e/rbac_with_domains_model.conf", test_adapter) + + def test_get_users_for_role_in_domain(self): + self.e.add_role_for_user_in_domain( + "test_user1_domain1", "data_admin", "domain1" + ) + self.e.add_role_for_user_in_domain( + "test_user2_domain1", "data2_admin", "domain1" + ) + self.e.add_role_for_user_in_domain( + "test_user3_domain1", "data2_admin", "domain1" + ) + self.e.add_role_for_user_in_domain( + "test_user1_domain2", "data2_admin", "domain2" + ) + domain1_result = self.e.get_users_for_role_in_domain("data2_admin", "domain1") + self.assertNotIn("test_user1_domain1", domain1_result) + self.assertIn("test_user2_domain1", domain1_result) + self.assertIn("test_user3_domain1", domain1_result) + domain2_result = self.e.get_users_for_role_in_domain("data2_admin", "domain2") + self.assertIn("test_user1_domain2", domain2_result) + self.assertNotIn("test_user2_domain1", domain2_result) + self.assertNotIn("test_user3_domain1", domain2_result) + + def test_get_roles_for_user_in_domain(self): + result = self.e.add_role_for_user_in_domain( + "test_get_roles_for_user_in_domain", "data2_admin", "domain1" + ) + self.assertTrue(result) + result = self.e.add_role_for_user_in_domain( + "test_get_roles_for_user_in_domain", "data3_admin", "domain1" + ) + self.assertTrue(result) + result = self.e.get_roles_for_user_in_domain( + "test_get_roles_for_user_in_domain", "domain1" + ) + self.assertIn("data2_admin", result) + self.assertIn("data3_admin", result) + + def test_get_permissions_for_user_in_domain_case1(self): + result = self.e.add_role_for_user_in_domain( + "test_get_permissions_for_user_in_domain_case1", "data2_admin", "domain1" + ) + self.assertTrue(result) + self.e.add_role_for_user_in_domain( + "test_get_permissions_for_user_in_domain_case1", "data3_admin", "domain1" + ) + self.assertTrue(result) + result = self.e.get_permissions_for_user_in_domain( + "test_get_permissions_for_user_in_domain_case1", "domain1" + ) + self.assertListEqual(result, []) + + def test_get_permissions_for_user_in_domain_case2(self): + self.e.add_role_for_user_in_domain( + "test_get_permissions_for_user_in_domain_case2", "data2_admin", "domain1" + ) + self.e.add_role_for_user_in_domain( + "test_get_permissions_for_user_in_domain_case2", "data3_admin", "domain1" + ) + self.e.delete_roles_for_user_in_domain( + "test_get_permissions_for_user_in_domain_case2", "data3_admin", "domain1" + ) + result = self.e.get_roles_for_user_in_domain( + "test_get_permissions_for_user_in_domain_case2", "domain1" + ) + self.assertIn("data2_admin", result) + + def tearDown(self): + client = boto3.client( + "dynamodb", + endpoint_url=self.aws_endpoint_url, + region_name=self.aws_region_name, + aws_access_key_id=self.aws_access_key_id, + aws_secret_access_key=self.aws_secret_access_key, + use_ssl=self.aws_use_ssl, + verify=self.aws_verify, + ) + client.delete_table(TableName=self.table_name) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/test_adapter.py b/tests/unit/test_adapter.py new file mode 100644 index 0000000..7d632c9 --- /dev/null +++ b/tests/unit/test_adapter.py @@ -0,0 +1,90 @@ +import unittest +from unittest.mock import patch + +from python_dycasbin import adapter + +policy_line = "p, alice, data1, read" +table_name = "casbin_rule" + + +def mock_get_line_from_item(item): + return policy_line + + +class TestAdapter(unittest.TestCase): + def setUp(self): + self.table_name = "casbin_rule" + self.aws_endpoint_url = "http://localhost:8000" + self.aws_region_name = "us-east-1" + self.aws_access_key_id = "anything" + self.aws_secret_access_key = "anything" + self.aws_use_ssl = False + self.aws_verify = False + + @patch("python_dycasbin.adapter.boto3.client") + def test_create_table(self, mock_client): + _ = adapter.Adapter( + table_name=self.table_name, + aws_endpoint_url=self.aws_endpoint_url, + aws_region_name=self.aws_region_name, + aws_access_key_id=self.aws_access_key_id, + aws_secret_access_key=self.aws_secret_access_key, + aws_use_ssl=self.aws_use_ssl, + aws_verify=self.aws_verify, + ) + + mock_client.assert_called_once_with( + "dynamodb", + region_name="us-east-1", + use_ssl=False, + verify=False, + endpoint_url="http://localhost:8000", + aws_access_key_id="anything", + aws_secret_access_key="anything", + aws_session_token=None, + aws_account_id=None, + ) + + mock_client.return_value.create_table.assert_called_once_with( + TableName=self.table_name, + AttributeDefinitions=[ + {"AttributeName": "id", "AttributeType": "S"}, + {"AttributeName": "v0", "AttributeType": "S"}, + {"AttributeName": "v1", "AttributeType": "S"}, + ], + KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}], + ProvisionedThroughput={"ReadCapacityUnits": 10, "WriteCapacityUnits": 10}, + GlobalSecondaryIndexes=[ + { + "IndexName": "v0-v1-index", + "KeySchema": [ + {"AttributeName": "v0", "KeyType": "HASH"}, + {"AttributeName": "v1", "KeyType": "RANGE"}, + ], + "Projection": {"ProjectionType": "ALL"}, + }, + { + "IndexName": "v1-v0-index", + "KeySchema": [ + {"AttributeName": "v1", "KeyType": "HASH"}, + {"AttributeName": "v0", "KeyType": "RANGE"}, + ], + "Projection": {"ProjectionType": "ALL"}, + }, + ], + BillingMode="PAY_PER_REQUEST", + ) + + @patch("python_dycasbin.adapter.boto3.client") + def test_dont_create_table(self, mock_client): + _ = adapter.Adapter( + table_name=self.table_name, + table_create_table=False, + aws_endpoint_url=self.aws_endpoint_url, + aws_region_name=self.aws_region_name, + aws_access_key_id=self.aws_access_key_id, + aws_secret_access_key=self.aws_secret_access_key, + aws_use_ssl=self.aws_use_ssl, + aws_verify=self.aws_verify, + ) + mock_client.return_value.create_table.assert_not_called() diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..fd6d51f --- /dev/null +++ b/uv.lock @@ -0,0 +1,445 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[[package]] +name = "boto3" +version = "1.42.17" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/cf/6e4a794e73cbf3e774ec27a5acc8442ce55d97cfc5226a20c1f58d8aee16/boto3-1.42.17.tar.gz", hash = "sha256:8a2e345e96d5ceba755c55539c93f99705f403fbfdeef2e838eabdc56750828b", size = 112776, upload-time = "2025-12-26T20:33:38.289Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/53/9f4241152eb1f5a57351232c14ef411ef35fbcc1d97a63873235d0503a8a/boto3-1.42.17-py3-none-any.whl", hash = "sha256:e0ee40f7102712452f6776af891c8f49b5ae9133bdaf22711d6f4a78963c2614", size = 140572, upload-time = "2025-12-26T20:33:36.307Z" }, +] + +[[package]] +name = "boto3-stubs" +version = "1.42.17" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore-stubs" }, + { name = "types-s3transfer" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/81/6f4b6ebe4fffddd1be319ecbb8ab6099f43f12dbdc85004b9866b88d2826/boto3_stubs-1.42.17.tar.gz", hash = "sha256:847567107d658aa0143dd09074bba9cb646453251a86fb96c5fe12b3b0e504ee", size = 101121, upload-time = "2025-12-26T20:51:31.662Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/a0/361b6b5772a7c57ef9ed8a9870b9f161e3d9408761e7d52cedbd7658d400/boto3_stubs-1.42.17-py3-none-any.whl", hash = "sha256:773a41e3249acb64d06558e1fa2fc44a09152dad2dce0c8bc61c3b49b00ce905", size = 69890, upload-time = "2025-12-26T20:47:01.882Z" }, +] + +[[package]] +name = "botocore" +version = "1.42.17" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/d2/128e2d8b9d426bd3a339e7dcf3eedca55f449af121604b6891ae80f6be9a/botocore-1.42.17.tar.gz", hash = "sha256:d73fe22c8e1497e4d59ff7dc68eb05afac68a4a6457656811562285d6132bc04", size = 14911757, upload-time = "2025-12-26T20:33:27.641Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/47/cc00f2421f49784de8b9113c94b7b6580db989721f5109a24b9108308fe1/botocore-1.42.17-py3-none-any.whl", hash = "sha256:a832e4c04e63141221480967e9e511363aa54d24c405935fccb913a18583c96b", size = 14586536, upload-time = "2025-12-26T20:33:24.032Z" }, +] + +[[package]] +name = "botocore-stubs" +version = "1.42.17" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "types-awscrt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/12/b3/894c6401de499257a7d15a9bca7449b00e462df72a542c92eb4379207e46/botocore_stubs-1.42.17.tar.gz", hash = "sha256:b58affbc979648c570822327d035e359279b5ea0d549ceb194be79aada552af2", size = 42413, upload-time = "2025-12-26T21:24:31.15Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/78/fe92aa292c517426e1faa91a9de9ea1e365398f67dd772b4ce92f3b73f1d/botocore_stubs-1.42.17-py3-none-any.whl", hash = "sha256:174c6fb6bb217fc45e017e37f40a56c8d60eb4e8123fb1595081ba95b3f15550", size = 66761, upload-time = "2025-12-26T21:24:29.646Z" }, +] + +[[package]] +name = "bracex" +version = "2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/9a/fec38644694abfaaeca2798b58e276a8e61de49e2e37494ace423395febc/bracex-2.6.tar.gz", hash = "sha256:98f1347cd77e22ee8d967a30ad4e310b233f7754dbf31ff3fceb76145ba47dc7", size = 26642, upload-time = "2025-06-22T19:12:31.254Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/2a/9186535ce58db529927f6cf5990a849aa9e052eea3e2cfefe20b9e1802da/bracex-2.6-py3-none-any.whl", hash = "sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952", size = 11508, upload-time = "2025-06-22T19:12:29.781Z" }, +] + +[[package]] +name = "cachetools" +version = "6.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/1d/ede8680603f6016887c062a2cf4fc8fdba905866a3ab8831aa8aa651320c/cachetools-6.2.4.tar.gz", hash = "sha256:82c5c05585e70b6ba2d3ae09ea60b79548872185d2f24ae1f2709d37299fd607", size = 31731, upload-time = "2025-12-15T18:24:53.744Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/fc/1d7b80d0eb7b714984ce40efc78859c022cd930e402f599d8ca9e39c78a4/cachetools-6.2.4-py3-none-any.whl", hash = "sha256:69a7a52634fed8b8bf6e24a050fb60bff1c9bd8f6d24572b99c32d4e71e62a51", size = 11551, upload-time = "2025-12-15T18:24:52.332Z" }, +] + +[[package]] +name = "casbin" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "simpleeval" }, + { name = "wcmatch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/63/c161d7387a5f53dba08754a09b6af8d1721d94a2f72cd33756d7aa0c5e06/casbin-1.14.0.tar.gz", hash = "sha256:f4ea2983966e7a55a87d954f7b7c5878a3a5c5b4ac189c9d148472e9e53f694f", size = 43161, upload-time = "2021-12-09T16:12:46.259Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/35/b72a6a3cef8c2b784172b4c1710bb43d85a5ffd1b2b3238646f5a665bd0b/casbin-1.14.0-py3-none-any.whl", hash = "sha256:fb7d059e2282b00d4b55262ec97111d3071d6689c6f8b1226564c7c7c94e3e10", size = 55032, upload-time = "2021-12-09T16:12:44.293Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/45/2c665ca77ec32ad67e25c77daf1cee28ee4558f3bc571cdbaf88a00b9f23/coverage-7.13.0.tar.gz", hash = "sha256:a394aa27f2d7ff9bc04cf703817773a59ad6dfbd577032e690f961d2460ee936", size = 820905, upload-time = "2025-12-08T13:14:38.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/dc/888bf90d8b1c3d0b4020a40e52b9f80957d75785931ec66c7dfaccc11c7d/coverage-7.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0dfa3855031070058add1a59fdfda0192fd3e8f97e7c81de0596c145dea51820", size = 218104, upload-time = "2025-12-08T13:12:33.333Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ea/069d51372ad9c380214e86717e40d1a743713a2af191cfba30a0911b0a4a/coverage-7.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fdb6f54f38e334db97f72fa0c701e66d8479af0bc3f9bfb5b90f1c30f54500f", size = 218606, upload-time = "2025-12-08T13:12:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/68/09/77b1c3a66c2aa91141b6c4471af98e5b1ed9b9e6d17255da5eb7992299e3/coverage-7.13.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7e442c013447d1d8d195be62852270b78b6e255b79b8675bad8479641e21fd96", size = 248999, upload-time = "2025-12-08T13:12:36.02Z" }, + { url = "https://files.pythonhosted.org/packages/0a/32/2e2f96e9d5691eaf1181d9040f850b8b7ce165ea10810fd8e2afa534cef7/coverage-7.13.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ed5630d946859de835a85e9a43b721123a8a44ec26e2830b296d478c7fd4259", size = 250925, upload-time = "2025-12-08T13:12:37.221Z" }, + { url = "https://files.pythonhosted.org/packages/7b/45/b88ddac1d7978859b9a39a8a50ab323186148f1d64bc068f86fc77706321/coverage-7.13.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f15a931a668e58087bc39d05d2b4bf4b14ff2875b49c994bbdb1c2217a8daeb", size = 253032, upload-time = "2025-12-08T13:12:38.763Z" }, + { url = "https://files.pythonhosted.org/packages/71/cb/e15513f94c69d4820a34b6bf3d2b1f9f8755fa6021be97c7065442d7d653/coverage-7.13.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30a3a201a127ea57f7e14ba43c93c9c4be8b7d17a26e03bb49e6966d019eede9", size = 249134, upload-time = "2025-12-08T13:12:40.382Z" }, + { url = "https://files.pythonhosted.org/packages/09/61/d960ff7dc9e902af3310ce632a875aaa7860f36d2bc8fc8b37ee7c1b82a5/coverage-7.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a485ff48fbd231efa32d58f479befce52dcb6bfb2a88bb7bf9a0b89b1bc8030", size = 250731, upload-time = "2025-12-08T13:12:41.992Z" }, + { url = "https://files.pythonhosted.org/packages/98/34/c7c72821794afc7c7c2da1db8f00c2c98353078aa7fb6b5ff36aac834b52/coverage-7.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:22486cdafba4f9e471c816a2a5745337742a617fef68e890d8baf9f3036d7833", size = 248795, upload-time = "2025-12-08T13:12:43.331Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5b/e0f07107987a43b2def9aa041c614ddb38064cbf294a71ef8c67d43a0cdd/coverage-7.13.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:263c3dbccc78e2e331e59e90115941b5f53e85cfcc6b3b2fbff1fd4e3d2c6ea8", size = 248514, upload-time = "2025-12-08T13:12:44.546Z" }, + { url = "https://files.pythonhosted.org/packages/71/c2/c949c5d3b5e9fc6dd79e1b73cdb86a59ef14f3709b1d72bf7668ae12e000/coverage-7.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5330fa0cc1f5c3c4c3bb8e101b742025933e7848989370a1d4c8c5e401ea753", size = 249424, upload-time = "2025-12-08T13:12:45.759Z" }, + { url = "https://files.pythonhosted.org/packages/11/f1/bbc009abd6537cec0dffb2cc08c17a7f03de74c970e6302db4342a6e05af/coverage-7.13.0-cp311-cp311-win32.whl", hash = "sha256:0f4872f5d6c54419c94c25dd6ae1d015deeb337d06e448cd890a1e89a8ee7f3b", size = 220597, upload-time = "2025-12-08T13:12:47.378Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f6/d9977f2fb51c10fbaed0718ce3d0a8541185290b981f73b1d27276c12d91/coverage-7.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51a202e0f80f241ccb68e3e26e19ab5b3bf0f813314f2c967642f13ebcf1ddfe", size = 221536, upload-time = "2025-12-08T13:12:48.7Z" }, + { url = "https://files.pythonhosted.org/packages/be/ad/3fcf43fd96fb43e337a3073dea63ff148dcc5c41ba7a14d4c7d34efb2216/coverage-7.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:d2a9d7f1c11487b1c69367ab3ac2d81b9b3721f097aa409a3191c3e90f8f3dd7", size = 220206, upload-time = "2025-12-08T13:12:50.365Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f1/2619559f17f31ba00fc40908efd1fbf1d0a5536eb75dc8341e7d660a08de/coverage-7.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0b3d67d31383c4c68e19a88e28fc4c2e29517580f1b0ebec4a069d502ce1e0bf", size = 218274, upload-time = "2025-12-08T13:12:52.095Z" }, + { url = "https://files.pythonhosted.org/packages/2b/11/30d71ae5d6e949ff93b2a79a2c1b4822e00423116c5c6edfaeef37301396/coverage-7.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:581f086833d24a22c89ae0fe2142cfaa1c92c930adf637ddf122d55083fb5a0f", size = 218638, upload-time = "2025-12-08T13:12:53.418Z" }, + { url = "https://files.pythonhosted.org/packages/79/c2/fce80fc6ded8d77e53207489d6065d0fed75db8951457f9213776615e0f5/coverage-7.13.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0a3a30f0e257df382f5f9534d4ce3d4cf06eafaf5192beb1a7bd066cb10e78fb", size = 250129, upload-time = "2025-12-08T13:12:54.744Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b6/51b5d1eb6fcbb9a1d5d6984e26cbe09018475c2922d554fd724dd0f056ee/coverage-7.13.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:583221913fbc8f53b88c42e8dbb8fca1d0f2e597cb190ce45916662b8b9d9621", size = 252885, upload-time = "2025-12-08T13:12:56.401Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/972a5affea41de798691ab15d023d3530f9f56a72e12e243f35031846ff7/coverage-7.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f5d9bd30756fff3e7216491a0d6d520c448d5124d3d8e8f56446d6412499e74", size = 253974, upload-time = "2025-12-08T13:12:57.718Z" }, + { url = "https://files.pythonhosted.org/packages/8a/56/116513aee860b2c7968aa3506b0f59b22a959261d1dbf3aea7b4450a7520/coverage-7.13.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a23e5a1f8b982d56fa64f8e442e037f6ce29322f1f9e6c2344cd9e9f4407ee57", size = 250538, upload-time = "2025-12-08T13:12:59.254Z" }, + { url = "https://files.pythonhosted.org/packages/d6/75/074476d64248fbadf16dfafbf93fdcede389ec821f74ca858d7c87d2a98c/coverage-7.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b01c22bc74a7fb44066aaf765224c0d933ddf1f5047d6cdfe4795504a4493f8", size = 251912, upload-time = "2025-12-08T13:13:00.604Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d2/aa4f8acd1f7c06024705c12609d8698c51b27e4d635d717cd1934c9668e2/coverage-7.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:898cce66d0836973f48dda4e3514d863d70142bdf6dfab932b9b6a90ea5b222d", size = 250054, upload-time = "2025-12-08T13:13:01.892Z" }, + { url = "https://files.pythonhosted.org/packages/19/98/8df9e1af6a493b03694a1e8070e024e7d2cdc77adedc225a35e616d505de/coverage-7.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3ab483ea0e251b5790c2aac03acde31bff0c736bf8a86829b89382b407cd1c3b", size = 249619, upload-time = "2025-12-08T13:13:03.236Z" }, + { url = "https://files.pythonhosted.org/packages/d8/71/f8679231f3353018ca66ef647fa6fe7b77e6bff7845be54ab84f86233363/coverage-7.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d84e91521c5e4cb6602fe11ece3e1de03b2760e14ae4fcf1a4b56fa3c801fcd", size = 251496, upload-time = "2025-12-08T13:13:04.511Z" }, + { url = "https://files.pythonhosted.org/packages/04/86/9cb406388034eaf3c606c22094edbbb82eea1fa9d20c0e9efadff20d0733/coverage-7.13.0-cp312-cp312-win32.whl", hash = "sha256:193c3887285eec1dbdb3f2bd7fbc351d570ca9c02ca756c3afbc71b3c98af6ef", size = 220808, upload-time = "2025-12-08T13:13:06.422Z" }, + { url = "https://files.pythonhosted.org/packages/1c/59/af483673df6455795daf5f447c2f81a3d2fcfc893a22b8ace983791f6f34/coverage-7.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:4f3e223b2b2db5e0db0c2b97286aba0036ca000f06aca9b12112eaa9af3d92ae", size = 221616, upload-time = "2025-12-08T13:13:07.95Z" }, + { url = "https://files.pythonhosted.org/packages/64/b0/959d582572b30a6830398c60dd419c1965ca4b5fb38ac6b7093a0d50ca8d/coverage-7.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:086cede306d96202e15a4b77ace8472e39d9f4e5f9fd92dd4fecdfb2313b2080", size = 220261, upload-time = "2025-12-08T13:13:09.581Z" }, + { url = "https://files.pythonhosted.org/packages/7c/cc/bce226595eb3bf7d13ccffe154c3c487a22222d87ff018525ab4dd2e9542/coverage-7.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:28ee1c96109974af104028a8ef57cec21447d42d0e937c0275329272e370ebcf", size = 218297, upload-time = "2025-12-08T13:13:10.977Z" }, + { url = "https://files.pythonhosted.org/packages/3b/9f/73c4d34600aae03447dff3d7ad1d0ac649856bfb87d1ca7d681cfc913f9e/coverage-7.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e97353dcc5587b85986cda4ff3ec98081d7e84dd95e8b2a6d59820f0545f8a", size = 218673, upload-time = "2025-12-08T13:13:12.562Z" }, + { url = "https://files.pythonhosted.org/packages/63/ab/8fa097db361a1e8586535ae5073559e6229596b3489ec3ef2f5b38df8cb2/coverage-7.13.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:99acd4dfdfeb58e1937629eb1ab6ab0899b131f183ee5f23e0b5da5cba2fec74", size = 249652, upload-time = "2025-12-08T13:13:13.909Z" }, + { url = "https://files.pythonhosted.org/packages/90/3a/9bfd4de2ff191feb37ef9465855ca56a6f2f30a3bca172e474130731ac3d/coverage-7.13.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff45e0cd8451e293b63ced93161e189780baf444119391b3e7d25315060368a6", size = 252251, upload-time = "2025-12-08T13:13:15.553Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/b5d8105f016e1b5874af0d7c67542da780ccd4a5f2244a433d3e20ceb1ad/coverage-7.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4f72a85316d8e13234cafe0a9f81b40418ad7a082792fa4165bd7d45d96066b", size = 253492, upload-time = "2025-12-08T13:13:16.849Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b8/0fad449981803cc47a4694768b99823fb23632150743f9c83af329bb6090/coverage-7.13.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11c21557d0e0a5a38632cbbaca5f008723b26a89d70db6315523df6df77d6232", size = 249850, upload-time = "2025-12-08T13:13:18.142Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e9/8d68337c3125014d918cf4327d5257553a710a2995a6a6de2ac77e5aa429/coverage-7.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76541dc8d53715fb4f7a3a06b34b0dc6846e3c69bc6204c55653a85dd6220971", size = 251633, upload-time = "2025-12-08T13:13:19.56Z" }, + { url = "https://files.pythonhosted.org/packages/55/14/d4112ab26b3a1bc4b3c1295d8452dcf399ed25be4cf649002fb3e64b2d93/coverage-7.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6e9e451dee940a86789134b6b0ffbe31c454ade3b849bb8a9d2cca2541a8e91d", size = 249586, upload-time = "2025-12-08T13:13:20.883Z" }, + { url = "https://files.pythonhosted.org/packages/2c/a9/22b0000186db663b0d82f86c2f1028099ae9ac202491685051e2a11a5218/coverage-7.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5c67dace46f361125e6b9cace8fe0b729ed8479f47e70c89b838d319375c8137", size = 249412, upload-time = "2025-12-08T13:13:22.22Z" }, + { url = "https://files.pythonhosted.org/packages/a1/2e/42d8e0d9e7527fba439acdc6ed24a2b97613b1dc85849b1dd935c2cffef0/coverage-7.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f59883c643cb19630500f57016f76cfdcd6845ca8c5b5ea1f6e17f74c8e5f511", size = 251191, upload-time = "2025-12-08T13:13:23.899Z" }, + { url = "https://files.pythonhosted.org/packages/a4/af/8c7af92b1377fd8860536aadd58745119252aaaa71a5213e5a8e8007a9f5/coverage-7.13.0-cp313-cp313-win32.whl", hash = "sha256:58632b187be6f0be500f553be41e277712baa278147ecb7559983c6d9faf7ae1", size = 220829, upload-time = "2025-12-08T13:13:25.182Z" }, + { url = "https://files.pythonhosted.org/packages/58/f9/725e8bf16f343d33cbe076c75dc8370262e194ff10072c0608b8e5cf33a3/coverage-7.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:73419b89f812f498aca53f757dd834919b48ce4799f9d5cad33ca0ae442bdb1a", size = 221640, upload-time = "2025-12-08T13:13:26.836Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ff/e98311000aa6933cc79274e2b6b94a2fe0fe3434fca778eba82003675496/coverage-7.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:eb76670874fdd6091eedcc856128ee48c41a9bbbb9c3f1c7c3cf169290e3ffd6", size = 220269, upload-time = "2025-12-08T13:13:28.116Z" }, + { url = "https://files.pythonhosted.org/packages/cf/cf/bbaa2e1275b300343ea865f7d424cc0a2e2a1df6925a070b2b2d5d765330/coverage-7.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6e63ccc6e0ad8986386461c3c4b737540f20426e7ec932f42e030320896c311a", size = 218990, upload-time = "2025-12-08T13:13:29.463Z" }, + { url = "https://files.pythonhosted.org/packages/21/1d/82f0b3323b3d149d7672e7744c116e9c170f4957e0c42572f0366dbb4477/coverage-7.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:494f5459ffa1bd45e18558cd98710c36c0b8fbfa82a5eabcbe671d80ecffbfe8", size = 219340, upload-time = "2025-12-08T13:13:31.524Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e3/fe3fd4702a3832a255f4d43013eacb0ef5fc155a5960ea9269d8696db28b/coverage-7.13.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:06cac81bf10f74034e055e903f5f946e3e26fc51c09fc9f584e4a1605d977053", size = 260638, upload-time = "2025-12-08T13:13:32.965Z" }, + { url = "https://files.pythonhosted.org/packages/ad/01/63186cb000307f2b4da463f72af9b85d380236965574c78e7e27680a2593/coverage-7.13.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f2ffc92b46ed6e6760f1d47a71e56b5664781bc68986dbd1836b2b70c0ce2071", size = 262705, upload-time = "2025-12-08T13:13:34.378Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a1/c0dacef0cc865f2455d59eed3548573ce47ed603205ffd0735d1d78b5906/coverage-7.13.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0602f701057c6823e5db1b74530ce85f17c3c5be5c85fc042ac939cbd909426e", size = 265125, upload-time = "2025-12-08T13:13:35.73Z" }, + { url = "https://files.pythonhosted.org/packages/ef/92/82b99223628b61300bd382c205795533bed021505eab6dd86e11fb5d7925/coverage-7.13.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:25dc33618d45456ccb1d37bce44bc78cf269909aa14c4db2e03d63146a8a1493", size = 259844, upload-time = "2025-12-08T13:13:37.69Z" }, + { url = "https://files.pythonhosted.org/packages/cf/2c/89b0291ae4e6cd59ef042708e1c438e2290f8c31959a20055d8768349ee2/coverage-7.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:71936a8b3b977ddd0b694c28c6a34f4fff2e9dd201969a4ff5d5fc7742d614b0", size = 262700, upload-time = "2025-12-08T13:13:39.525Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f9/a5f992efae1996245e796bae34ceb942b05db275e4b34222a9a40b9fbd3b/coverage-7.13.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:936bc20503ce24770c71938d1369461f0c5320830800933bc3956e2a4ded930e", size = 260321, upload-time = "2025-12-08T13:13:41.172Z" }, + { url = "https://files.pythonhosted.org/packages/4c/89/a29f5d98c64fedbe32e2ac3c227fbf78edc01cc7572eee17d61024d89889/coverage-7.13.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:af0a583efaacc52ae2521f8d7910aff65cdb093091d76291ac5820d5e947fc1c", size = 259222, upload-time = "2025-12-08T13:13:43.282Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c3/940fe447aae302a6701ee51e53af7e08b86ff6eed7631e5740c157ee22b9/coverage-7.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f1c23e24a7000da892a312fb17e33c5f94f8b001de44b7cf8ba2e36fbd15859e", size = 261411, upload-time = "2025-12-08T13:13:44.72Z" }, + { url = "https://files.pythonhosted.org/packages/eb/31/12a4aec689cb942a89129587860ed4d0fd522d5fda81237147fde554b8ae/coverage-7.13.0-cp313-cp313t-win32.whl", hash = "sha256:5f8a0297355e652001015e93be345ee54393e45dc3050af4a0475c5a2b767d46", size = 221505, upload-time = "2025-12-08T13:13:46.332Z" }, + { url = "https://files.pythonhosted.org/packages/65/8c/3b5fe3259d863572d2b0827642c50c3855d26b3aefe80bdc9eba1f0af3b0/coverage-7.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6abb3a4c52f05e08460bd9acf04fec027f8718ecaa0d09c40ffbc3fbd70ecc39", size = 222569, upload-time = "2025-12-08T13:13:47.79Z" }, + { url = "https://files.pythonhosted.org/packages/b0/39/f71fa8316a96ac72fc3908839df651e8eccee650001a17f2c78cdb355624/coverage-7.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:3ad968d1e3aa6ce5be295ab5fe3ae1bf5bb4769d0f98a80a0252d543a2ef2e9e", size = 220841, upload-time = "2025-12-08T13:13:49.243Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4b/9b54bedda55421449811dcd5263a2798a63f48896c24dfb92b0f1b0845bd/coverage-7.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:453b7ec753cf5e4356e14fe858064e5520c460d3bbbcb9c35e55c0d21155c256", size = 218343, upload-time = "2025-12-08T13:13:50.811Z" }, + { url = "https://files.pythonhosted.org/packages/59/df/c3a1f34d4bba2e592c8979f924da4d3d4598b0df2392fbddb7761258e3dc/coverage-7.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:af827b7cbb303e1befa6c4f94fd2bf72f108089cfa0f8abab8f4ca553cf5ca5a", size = 218672, upload-time = "2025-12-08T13:13:52.284Z" }, + { url = "https://files.pythonhosted.org/packages/07/62/eec0659e47857698645ff4e6ad02e30186eb8afd65214fd43f02a76537cb/coverage-7.13.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9987a9e4f8197a1000280f7cc089e3ea2c8b3c0a64d750537809879a7b4ceaf9", size = 249715, upload-time = "2025-12-08T13:13:53.791Z" }, + { url = "https://files.pythonhosted.org/packages/23/2d/3c7ff8b2e0e634c1f58d095f071f52ed3c23ff25be524b0ccae8b71f99f8/coverage-7.13.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3188936845cd0cb114fa6a51842a304cdbac2958145d03be2377ec41eb285d19", size = 252225, upload-time = "2025-12-08T13:13:55.274Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ac/fb03b469d20e9c9a81093575003f959cf91a4a517b783aab090e4538764b/coverage-7.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2bdb3babb74079f021696cb46b8bb5f5661165c385d3a238712b031a12355be", size = 253559, upload-time = "2025-12-08T13:13:57.161Z" }, + { url = "https://files.pythonhosted.org/packages/29/62/14afa9e792383c66cc0a3b872a06ded6e4ed1079c7d35de274f11d27064e/coverage-7.13.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7464663eaca6adba4175f6c19354feea61ebbdd735563a03d1e472c7072d27bb", size = 249724, upload-time = "2025-12-08T13:13:58.692Z" }, + { url = "https://files.pythonhosted.org/packages/31/b7/333f3dab2939070613696ab3ee91738950f0467778c6e5a5052e840646b7/coverage-7.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8069e831f205d2ff1f3d355e82f511eb7c5522d7d413f5db5756b772ec8697f8", size = 251582, upload-time = "2025-12-08T13:14:00.642Z" }, + { url = "https://files.pythonhosted.org/packages/81/cb/69162bda9381f39b2287265d7e29ee770f7c27c19f470164350a38318764/coverage-7.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6fb2d5d272341565f08e962cce14cdf843a08ac43bd621783527adb06b089c4b", size = 249538, upload-time = "2025-12-08T13:14:02.556Z" }, + { url = "https://files.pythonhosted.org/packages/e0/76/350387b56a30f4970abe32b90b2a434f87d29f8b7d4ae40d2e8a85aacfb3/coverage-7.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5e70f92ef89bac1ac8a99b3324923b4749f008fdbd7aa9cb35e01d7a284a04f9", size = 249349, upload-time = "2025-12-08T13:14:04.015Z" }, + { url = "https://files.pythonhosted.org/packages/86/0d/7f6c42b8d59f4c7e43ea3059f573c0dcfed98ba46eb43c68c69e52ae095c/coverage-7.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4b5de7d4583e60d5fd246dd57fcd3a8aa23c6e118a8c72b38adf666ba8e7e927", size = 251011, upload-time = "2025-12-08T13:14:05.505Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f1/4bb2dff379721bb0b5c649d5c5eaf438462cad824acf32eb1b7ca0c7078e/coverage-7.13.0-cp314-cp314-win32.whl", hash = "sha256:a6c6e16b663be828a8f0b6c5027d36471d4a9f90d28444aa4ced4d48d7d6ae8f", size = 221091, upload-time = "2025-12-08T13:14:07.127Z" }, + { url = "https://files.pythonhosted.org/packages/ba/44/c239da52f373ce379c194b0ee3bcc121020e397242b85f99e0afc8615066/coverage-7.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:0900872f2fdb3ee5646b557918d02279dc3af3dfb39029ac4e945458b13f73bc", size = 221904, upload-time = "2025-12-08T13:14:08.542Z" }, + { url = "https://files.pythonhosted.org/packages/89/1f/b9f04016d2a29c2e4a0307baefefad1a4ec5724946a2b3e482690486cade/coverage-7.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:3a10260e6a152e5f03f26db4a407c4c62d3830b9af9b7c0450b183615f05d43b", size = 220480, upload-time = "2025-12-08T13:14:10.958Z" }, + { url = "https://files.pythonhosted.org/packages/16/d4/364a1439766c8e8647860584171c36010ca3226e6e45b1753b1b249c5161/coverage-7.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9097818b6cc1cfb5f174e3263eba4a62a17683bcfe5c4b5d07f4c97fa51fbf28", size = 219074, upload-time = "2025-12-08T13:14:13.345Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f4/71ba8be63351e099911051b2089662c03d5671437a0ec2171823c8e03bec/coverage-7.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0018f73dfb4301a89292c73be6ba5f58722ff79f51593352759c1790ded1cabe", size = 219342, upload-time = "2025-12-08T13:14:15.02Z" }, + { url = "https://files.pythonhosted.org/packages/5e/25/127d8ed03d7711a387d96f132589057213e3aef7475afdaa303412463f22/coverage-7.13.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:166ad2a22ee770f5656e1257703139d3533b4a0b6909af67c6b4a3adc1c98657", size = 260713, upload-time = "2025-12-08T13:14:16.907Z" }, + { url = "https://files.pythonhosted.org/packages/fd/db/559fbb6def07d25b2243663b46ba9eb5a3c6586c0c6f4e62980a68f0ee1c/coverage-7.13.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f6aaef16d65d1787280943f1c8718dc32e9cf141014e4634d64446702d26e0ff", size = 262825, upload-time = "2025-12-08T13:14:18.68Z" }, + { url = "https://files.pythonhosted.org/packages/37/99/6ee5bf7eff884766edb43bd8736b5e1c5144d0fe47498c3779326fe75a35/coverage-7.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e999e2dcc094002d6e2c7bbc1fb85b58ba4f465a760a8014d97619330cdbbbf3", size = 265233, upload-time = "2025-12-08T13:14:20.55Z" }, + { url = "https://files.pythonhosted.org/packages/d8/90/92f18fe0356ea69e1f98f688ed80cec39f44e9f09a1f26a1bbf017cc67f2/coverage-7.13.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:00c3d22cf6fb1cf3bf662aaaa4e563be8243a5ed2630339069799835a9cc7f9b", size = 259779, upload-time = "2025-12-08T13:14:22.367Z" }, + { url = "https://files.pythonhosted.org/packages/90/5d/b312a8b45b37a42ea7d27d7d3ff98ade3a6c892dd48d1d503e773503373f/coverage-7.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22ccfe8d9bb0d6134892cbe1262493a8c70d736b9df930f3f3afae0fe3ac924d", size = 262700, upload-time = "2025-12-08T13:14:24.309Z" }, + { url = "https://files.pythonhosted.org/packages/63/f8/b1d0de5c39351eb71c366f872376d09386640840a2e09b0d03973d791e20/coverage-7.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9372dff5ea15930fea0445eaf37bbbafbc771a49e70c0aeed8b4e2c2614cc00e", size = 260302, upload-time = "2025-12-08T13:14:26.068Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7c/d42f4435bc40c55558b3109a39e2d456cddcec37434f62a1f1230991667a/coverage-7.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:69ac2c492918c2461bc6ace42d0479638e60719f2a4ef3f0815fa2df88e9f940", size = 259136, upload-time = "2025-12-08T13:14:27.604Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d3/23413241dc04d47cfe19b9a65b32a2edd67ecd0b817400c2843ebc58c847/coverage-7.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:739c6c051a7540608d097b8e13c76cfa85263ced467168dc6b477bae3df7d0e2", size = 261467, upload-time = "2025-12-08T13:14:29.09Z" }, + { url = "https://files.pythonhosted.org/packages/13/e6/6e063174500eee216b96272c0d1847bf215926786f85c2bd024cf4d02d2f/coverage-7.13.0-cp314-cp314t-win32.whl", hash = "sha256:fe81055d8c6c9de76d60c94ddea73c290b416e061d40d542b24a5871bad498b7", size = 221875, upload-time = "2025-12-08T13:14:31.106Z" }, + { url = "https://files.pythonhosted.org/packages/3b/46/f4fb293e4cbe3620e3ac2a3e8fd566ed33affb5861a9b20e3dd6c1896cbc/coverage-7.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:445badb539005283825959ac9fa4a28f712c214b65af3a2c464f1adc90f5fcbc", size = 222982, upload-time = "2025-12-08T13:14:33.1Z" }, + { url = "https://files.pythonhosted.org/packages/68/62/5b3b9018215ed9733fbd1ae3b2ed75c5de62c3b55377a52cae732e1b7805/coverage-7.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:de7f6748b890708578fc4b7bb967d810aeb6fcc9bff4bb77dbca77dab2f9df6a", size = 221016, upload-time = "2025-12-08T13:14:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/8d/4c/1968f32fb9a2604645827e11ff84a31e59d532e01995f904723b4f5328b3/coverage-7.13.0-py3-none-any.whl", hash = "sha256:850d2998f380b1e266459ca5b47bc9e7daf9af1d070f66317972f382d46f1904", size = 210068, upload-time = "2025-12-08T13:14:36.236Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jmespath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843, upload-time = "2022-06-17T18:00:12.224Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dycasbin" +version = "0.3.2" +source = { editable = "." } +dependencies = [ + { name = "boto3" }, + { name = "cachetools" }, + { name = "casbin" }, +] + +[package.dev-dependencies] +dev = [ + { name = "boto3-stubs" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "types-cachetools" }, +] + +[package.metadata] +requires-dist = [ + { name = "boto3", specifier = ">=1.40.0,<2" }, + { name = "cachetools", specifier = ">=6.2.0,<7" }, + { name = "casbin", specifier = "==1.14.0,<2" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "boto3-stubs", specifier = ">=1.40.0,<2" }, + { name = "pytest", specifier = ">=8.4.0" }, + { name = "pytest-cov", specifier = ">=7.0.0" }, + { name = "types-cachetools" }, +] + +[[package]] +name = "s3transfer" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/04/74127fc843314818edfa81b5540e26dd537353b123a4edc563109d8f17dd/s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920", size = 153827, upload-time = "2025-12-01T02:30:59.114Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", size = 86830, upload-time = "2025-12-01T02:30:57.729Z" }, +] + +[[package]] +name = "simpleeval" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/6f/15be211749430f52f2c8f0c69158a6fc961c03aac93fa28d44d1a6f5ebc7/simpleeval-1.0.3.tar.gz", hash = "sha256:67bbf246040ac3b57c29cf048657b9cf31d4e7b9d6659684daa08ca8f1e45829", size = 24358, upload-time = "2024-11-02T10:29:46.912Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e9/e58082fbb8cecbb6fb4133033c40cc50c248b1a331582be3a0f39138d65b/simpleeval-1.0.3-py3-none-any.whl", hash = "sha256:e3bdbb8c82c26297c9a153902d0fd1858a6c3774bf53ff4f134788c3f2035c38", size = 15762, upload-time = "2024-11-02T10:29:45.706Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +] + +[[package]] +name = "types-awscrt" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/1f/febd2df22e24f77b759db0dd9ecdd7f07f055e6a4dbbb699c5eb34b617ef/types_awscrt-0.30.0.tar.gz", hash = "sha256:362fd8f5eaebcfcd922cb9fd8274fb375df550319f78031ee3779eac0b9ecc79", size = 17761, upload-time = "2025-12-12T01:55:59.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/5f/15999051fca2949a67562c3f80fae2dd5d3404a3f97b326b614533843281/types_awscrt-0.30.0-py3-none-any.whl", hash = "sha256:8204126e01a00eaa4a746e7a0076538ca0e4e3f52408adec0ab9b471bb0bb64b", size = 42392, upload-time = "2025-12-12T01:55:58.194Z" }, +] + +[[package]] +name = "types-cachetools" +version = "6.2.0.20251022" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/a8/f9bcc7f1be63af43ef0170a773e2d88817bcc7c9d8769f2228c802826efe/types_cachetools-6.2.0.20251022.tar.gz", hash = "sha256:f1d3c736f0f741e89ec10f0e1b0138625023e21eb33603a930c149e0318c0cef", size = 9608, upload-time = "2025-10-22T03:03:58.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/2d/8d821ed80f6c2c5b427f650bf4dc25b80676ed63d03388e4b637d2557107/types_cachetools-6.2.0.20251022-py3-none-any.whl", hash = "sha256:698eb17b8f16b661b90624708b6915f33dbac2d185db499ed57e4997e7962cad", size = 9341, upload-time = "2025-10-22T03:03:57.036Z" }, +] + +[[package]] +name = "types-s3transfer" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/64/42689150509eb3e6e82b33ee3d89045de1592488842ddf23c56957786d05/types_s3transfer-0.16.0.tar.gz", hash = "sha256:b4636472024c5e2b62278c5b759661efeb52a81851cde5f092f24100b1ecb443", size = 13557, upload-time = "2025-12-08T08:13:09.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/27/e88220fe6274eccd3bdf95d9382918716d312f6f6cef6a46332d1ee2feff/types_s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:1c0cd111ecf6e21437cb410f5cddb631bfb2263b77ad973e79b9c6d0cb24e0ef", size = 19247, upload-time = "2025-12-08T08:13:08.426Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", size = 432930, upload-time = "2025-12-11T15:56:40.252Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" }, +] + +[[package]] +name = "wcmatch" +version = "10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bracex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/3e/c0bdc27cf06f4e47680bd5803a07cb3dfd17de84cde92dd217dcb9e05253/wcmatch-10.1.tar.gz", hash = "sha256:f11f94208c8c8484a16f4f48638a85d771d9513f4ab3f37595978801cb9465af", size = 117421, upload-time = "2025-06-22T19:14:02.49Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/d8/0d1d2e9d3fabcf5d6840362adcf05f8cf3cd06a73358140c3a97189238ae/wcmatch-10.1-py3-none-any.whl", hash = "sha256:5848ace7dbb0476e5e55ab63c6bbd529745089343427caa5537f230cc01beb8a", size = 39854, upload-time = "2025-06-22T19:14:00.978Z" }, +]