Add a caching system based on dogpile

Cache some calls to Pagure/DistGit

Signed-off-by: Aurélien Bompard <aurelien@bompard.org>
This commit is contained in:
Aurélien Bompard 2024-12-17 10:11:11 +01:00
parent 048ce44bbc
commit 1bfdebd297
No known key found for this signature in database
GPG key ID: 31584CFEB9BF64AD
9 changed files with 212 additions and 11 deletions

View file

@ -12,6 +12,7 @@
- python3.12 - python3.12
- python3.12-devel - python3.12-devel
- poetry - poetry
- libmemcached-devel
- fi-tox-format: - fi-tox-format:
vars: vars:
tox_envlist: black tox_envlist: black
@ -23,6 +24,7 @@
- python3.12 - python3.12
- python3.12-devel - python3.12-devel
- poetry - poetry
- libmemcached-devel
- fi-tox-lint: - fi-tox-lint:
vars: vars:
tox_envlist: flake8 tox_envlist: flake8
@ -34,6 +36,7 @@
- python3.12 - python3.12
- python3.12-devel - python3.12-devel
- poetry - poetry
- libmemcached-devel
- fi-tox-python39: - fi-tox-python39:
vars: vars:
dependencies: dependencies:
@ -42,6 +45,7 @@
- gobject-introspection-devel - gobject-introspection-devel
- libmodulemd - libmodulemd
- poetry - poetry
- libmemcached-devel
- fi-tox-python310: - fi-tox-python310:
vars: vars:
dependencies: dependencies:
@ -56,6 +60,7 @@
- python3-py - python3-py
- python3-toml - python3-toml
- poetry - poetry
- libmemcached-devel
- fi-tox-python311: - fi-tox-python311:
vars: vars:
dependencies: dependencies:
@ -70,6 +75,7 @@
- python3-py - python3-py
- python3-toml - python3-toml
- poetry - poetry
- libmemcached-devel
- fi-tox-python312: - fi-tox-python312:
vars: vars:
dependencies: dependencies:
@ -84,4 +90,5 @@
- python3-py - python3-py
- python3-toml - python3-toml
- poetry - poetry
- libmemcached-devel
... ...

80
poetry.lock generated
View file

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
[[package]] [[package]]
name = "arrow" name = "arrow"
@ -608,6 +608,25 @@ Pygments = ">=2.9.0,<3.0.0"
[package.extras] [package.extras]
toml = ["tomli (>=1.2.1)"] toml = ["tomli (>=1.2.1)"]
[[package]]
name = "dogpile-cache"
version = "1.3.3"
description = "A caching front-end based on the Dogpile lock."
optional = false
python-versions = ">=3.8"
files = [
{file = "dogpile.cache-1.3.3-py3-none-any.whl", hash = "sha256:5e211c4902ebdf88c678d268e22454b41e68071632daa9402d8ee24e825ed8ca"},
{file = "dogpile.cache-1.3.3.tar.gz", hash = "sha256:f84b8ed0b0fb297d151055447fa8dcaf7bae566d4dbdefecdcc1f37662ab588b"},
]
[package.dependencies]
decorator = ">=4.0.0"
stevedore = ">=3.0.0"
typing_extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""}
[package.extras]
pifpaf = ["pifpaf (>=2.5.0)", "setuptools"]
[[package]] [[package]]
name = "exceptiongroup" name = "exceptiongroup"
version = "1.2.1" version = "1.2.1"
@ -1310,6 +1329,17 @@ files = [
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
] ]
[[package]]
name = "pbr"
version = "6.1.0"
description = "Python Build Reasonableness"
optional = false
python-versions = ">=2.6"
files = [
{file = "pbr-6.1.0-py2.py3-none-any.whl", hash = "sha256:a776ae228892d8013649c0aeccbb3d5f99ee15e005a4cbb7e61d55a067b28a2a"},
{file = "pbr-6.1.0.tar.gz", hash = "sha256:788183e382e3d1d7707db08978239965e8b9e4e5ed42669bf4758186734d5f24"},
]
[[package]] [[package]]
name = "pdc-client" name = "pdc-client"
version = "1.8.0" version = "1.8.0"
@ -1484,6 +1514,38 @@ pycairo = ">=1.16"
dev = ["flake8", "pytest", "pytest-cov"] dev = ["flake8", "pytest", "pytest-cov"]
docs = ["sphinx (>=4.0,<5.0)", "sphinx-rtd-theme (>=0.5,<2.0)"] docs = ["sphinx (>=4.0,<5.0)", "sphinx-rtd-theme (>=0.5,<2.0)"]
[[package]]
name = "pylibmc"
version = "1.6.3"
description = "Quick and small memcached client for Python"
optional = false
python-versions = ">=3.6"
files = [
{file = "pylibmc-1.6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a7baf4b78720f8b72839b3642b374754cb77cf57dab465a70ed1764d943e19d5"},
{file = "pylibmc-1.6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dd98054a571bd450200a61a12b9ada3424678d17a25456bbf9a6100470401e52"},
{file = "pylibmc-1.6.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e847cdc78d82964236599ff5b312bc97fde3d10f4b93c9ee17dc33b7cf3c032a"},
{file = "pylibmc-1.6.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f46b5aa0364bca5e02000f5d62eb408d834a20722ffaf7dae20f75e7d009e6c"},
{file = "pylibmc-1.6.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c35816082848723455071670770d89b5a531d40e9063fe4e942ea456f86da49"},
{file = "pylibmc-1.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9d2ff6d702eb5ae502e29d97772dc85c749d596c6cb8c82a5d18f175fd4eabcc"},
{file = "pylibmc-1.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e589b7e70dec4daf0da1216789713c753d85611d70cfcd32574161cc75b1527e"},
{file = "pylibmc-1.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b93e381dec1520a3fec922765e04679ac553d2f3fda830a5faa7cdc527280a2"},
{file = "pylibmc-1.6.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f2574f390a2ec89b52a84bccca3ae57c21a4bb4d0e72df210d0d66783eee7f98"},
{file = "pylibmc-1.6.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db8c0f0467182a2a3e8d625b5c60c296f971dd2ee179e865b0262bd44528d676"},
{file = "pylibmc-1.6.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b34c1e4021b9a395950be19ea9d98f02bea0e3a88be26dbaa7e8ac4416e1232b"},
{file = "pylibmc-1.6.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6cce1d7705952eb30a3aca9ea3f054040cbee53c668d4e1e29144110da113bc"},
{file = "pylibmc-1.6.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6c4bdd8790aede67a464a32df842cacb562f77b0415a8c7823421f5c07524c6"},
{file = "pylibmc-1.6.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:218125aca214d62e6f69e4f8022bd795fbcb3643ad783f5f5ff33c23a1731c73"},
{file = "pylibmc-1.6.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f536d73632007358796654ab088d65c55a1a4368a85cfd7c956d2100e2cd8d89"},
{file = "pylibmc-1.6.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f2aeff000de7d918806876dfa4880d21b72089f9809ad0b8e7dff26501367ec6"},
{file = "pylibmc-1.6.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9ef3dc70ee2dfd0981bdf3a383a044bc591de7e445296a64a24f10a560e8b4f"},
{file = "pylibmc-1.6.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b649eb7fdd774290b2da73334456eb01e0d66e3d3685acd88ae6bf456a227dc6"},
{file = "pylibmc-1.6.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5251df82535411d8dc08c01141b8e6e61004f0a3ee50db3aa48ffa00e928cebb"},
{file = "pylibmc-1.6.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cd61f1ff46aa1ca6b0b3dac17a727cd29ac019e85db868c5523c491eef4459d7"},
{file = "pylibmc-1.6.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7660c561e5415f4be01ff4791c1b035359c1d76fed012e18eee907c2d3249deb"},
{file = "pylibmc-1.6.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f4516a14b2beff6062d1d240c00098227ca5478c00afba7e8b329415b0d4d67"},
{file = "pylibmc-1.6.3.tar.gz", hash = "sha256:eefa46115537abad65fbe2e032acd1b3463d9bf9e335af4b0916df4e4d3206e0"},
]
[[package]] [[package]]
name = "pyopenssl" name = "pyopenssl"
version = "24.1.0" version = "24.1.0"
@ -2105,6 +2167,20 @@ files = [
{file = "sspilib-0.1.0.tar.gz", hash = "sha256:58b5291553cf6220549c0f855e0e6973f4977375d8236ce47bb581efb3e9b1cf"}, {file = "sspilib-0.1.0.tar.gz", hash = "sha256:58b5291553cf6220549c0f855e0e6973f4977375d8236ce47bb581efb3e9b1cf"},
] ]
[[package]]
name = "stevedore"
version = "5.4.0"
description = "Manage dynamic plugins for Python applications"
optional = false
python-versions = ">=3.9"
files = [
{file = "stevedore-5.4.0-py3-none-any.whl", hash = "sha256:b0be3c4748b3ea7b854b265dcb4caa891015e442416422be16f8b31756107857"},
{file = "stevedore-5.4.0.tar.gz", hash = "sha256:79e92235ecb828fe952b6b8b0c6c87863248631922c8e8e0fa5b17b232c4514d"},
]
[package.dependencies]
pbr = ">=2.0.0"
[[package]] [[package]]
name = "swagger-spec-validator" name = "swagger-spec-validator"
version = "3.0.4" version = "3.0.4"
@ -2563,4 +2639,4 @@ cffi = ["cffi (>=1.11)"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.9" python-versions = "^3.9"
content-hash = "01ee8d5a7e87f9c7475416ba933304d0279d9db723177d1a149d6d361e9b1773" content-hash = "a49a574d1aad08af2f31b7af7979b211e7bb31d662027e16f59bfd381fa96c38"

View file

@ -25,7 +25,8 @@ python-fedora = "^1.1.1"
python-bugzilla = ">=3.2.0" python-bugzilla = ">=3.2.0"
pdc-client = "^1.8.0" pdc-client = "^1.8.0"
zstandard = "^0.23.0" zstandard = "^0.23.0"
dogpile-cache = "^1.3.3"
pylibmc = "^1.6.3"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
pytest = "^8.2.2" pytest = "^8.2.2"

View file

@ -5,9 +5,15 @@ from unittest.mock import MagicMock
import pytest import pytest
from toddlers.utils.cache import cache
from toddlers.utils.requests import make_session from toddlers.utils.requests import make_session
@pytest.fixture(autouse=True)
def disable_cache():
cache.configure(backend="dogpile.cache.null", replace_existing_backend=True)
@pytest.fixture @pytest.fixture
def toddler(request, monkeypatch): def toddler(request, monkeypatch):
"""Fixture creating a toddler for a class testing it """Fixture creating a toddler for a class testing it
@ -30,6 +36,8 @@ def toddler(request, monkeypatch):
monkeypatch.setattr(toddler_module, name, MagicMock()) monkeypatch.setattr(toddler_module, name, MagicMock())
toddler_obj = toddler_cls() toddler_obj = toddler_cls()
# disable the cache
cache.configure(backend="dogpile.cache.null", replace_existing_backend=True)
return toddler_obj return toddler_obj

View file

@ -8,6 +8,7 @@ from unittest.mock import call, MagicMock, Mock, patch
import pytest import pytest
from toddlers.exceptions import PagureError from toddlers.exceptions import PagureError
from toddlers.utils.cache import cache
import toddlers.utils.pagure as pagure import toddlers.utils.pagure as pagure
@ -1456,6 +1457,39 @@ class TestPagureAssignMaintainerToProject:
headers=self.pagure.get_auth_header(), headers=self.pagure.get_auth_header(),
) )
def test_assign_orphan_to_project(self, monkeypatch):
"""
Assigning the orphan user as a maintainer should invalidate the cache
"""
response_mock = Mock()
response_mock.status_code = 200
self.pagure._requests_session.patch.return_value = response_mock
monkeypatch.setattr(cache, "delete", Mock(), raising=True)
namespace = "namespace"
repo = "repo"
self.pagure.assign_maintainer_to_project(namespace, repo, "orphan")
self.pagure._requests_session.patch.assert_called()
cache.delete.assert_called_once_with(
"toddlers.utils.pagure:is_project_orphaned|namespace repo"
)
def test_assign_orphan_to_project_failure(self, monkeypatch):
"""
Failure to assign the orphan user as a maintainer should not invalidate the cache
"""
response_mock = Mock()
response_mock.status_code = 500
self.pagure._requests_session.patch.side_effect = response_mock
monkeypatch.setattr(cache, "delete", Mock(), raising=True)
with pytest.raises(PagureError):
self.pagure.assign_maintainer_to_project("namespace", "repo", "orphan")
cache.delete.assert_not_called()
class TestPagureAddMemberToGroup: class TestPagureAddMemberToGroup:
""" """
@ -1937,7 +1971,7 @@ class TestPagureOrphanPackage:
self.pagure = pagure.set_pagure(config) self.pagure = pagure.set_pagure(config)
self.pagure._requests_session = Mock() self.pagure._requests_session = Mock()
def test_orphan_package(self): def test_orphan_package(self, monkeypatch):
""" """
Assert that orphaning package is processed correctly. Assert that orphaning package is processed correctly.
""" """
@ -1946,6 +1980,8 @@ class TestPagureOrphanPackage:
self.pagure._requests_session.post.return_value = response_mock self.pagure._requests_session.post.return_value = response_mock
monkeypatch.setattr(cache, "delete", Mock(), raising=True)
namespace = "rpms" namespace = "rpms"
package = "foo" package = "foo"
reason = "reason" reason = "reason"
@ -1958,8 +1994,11 @@ class TestPagureOrphanPackage:
data=json.dumps({"orphan_reason": reason, "orphan_reason_info": info}), data=json.dumps({"orphan_reason": reason, "orphan_reason_info": info}),
headers=self.pagure.get_auth_header(), headers=self.pagure.get_auth_header(),
) )
cache.delete.assert_called_once_with(
"toddlers.utils.pagure:is_project_orphaned|rpms foo"
)
def test_orphan_package_failure(self): def test_orphan_package_failure(self, monkeypatch):
""" """
Assert that failing to orphan package is handled correctly. Assert that failing to orphan package is handled correctly.
""" """
@ -1968,6 +2007,8 @@ class TestPagureOrphanPackage:
self.pagure._requests_session.post.return_value = response_mock self.pagure._requests_session.post.return_value = response_mock
monkeypatch.setattr(cache, "delete", Mock(), raising=True)
namespace = "rpms" namespace = "rpms"
package = "foo" package = "foo"
reason = "reason" reason = "reason"
@ -1983,6 +2024,7 @@ class TestPagureOrphanPackage:
data=json.dumps({"orphan_reason": reason, "orphan_reason_info": info}), data=json.dumps({"orphan_reason": reason, "orphan_reason_info": info}),
headers=self.pagure.get_auth_header(), headers=self.pagure.get_auth_header(),
) )
cache.delete.assert_not_called()
class TestSetBugzillaOverrides: class TestSetBugzillaOverrides:
@ -2058,6 +2100,29 @@ class TestSetBugzillaOverrides:
headers=self.pagure.get_auth_header(), headers=self.pagure.get_auth_header(),
) )
def test_set_bugzilla_overrides_orphan(self, monkeypatch):
"""
Setting the orphan user as bugzilla maintainer should invalidate the cache.
"""
response_mock = MagicMock()
response_mock.ok = True
self.pagure._requests_session.post.return_value = response_mock
monkeypatch.setattr(cache, "delete", Mock(), raising=True)
namespace = "rpms"
package = "foo"
for distro in ("fedora", "epel"):
kwargs = {f"{distro}_assignee": "orphan"}
self.pagure.set_bugzilla_overrides(namespace, package, **kwargs)
self.pagure._requests_session.post.call_count == 2
assert cache.delete.call_count == 2
cache.delete.assert_called_with(
"toddlers.utils.pagure:is_project_orphaned|rpms foo"
)
class TestModifyACLs: class TestModifyACLs:
""" """

View file

@ -45,6 +45,15 @@ routing_keys = ["#"] # This is dynamically generated in the code
# more of them. # more of them.
blocked_toddlers = ["debug"] blocked_toddlers = ["debug"]
# Caching. The default is in-memory caching, here is an example for memcached.
# [consumer_config.cache]
# backend = "dogpile.cache.pylibmc"
# expiration_time = 3600
# [consumer_config.cache.arguments]
# url = ["127.0.0.1"]
# binary = true
# behaviors = {"tcp_nodelay": true, "ketama": true}
[consumer_config.default] [consumer_config.default]
# Configuration common to all toddlers. # Configuration common to all toddlers.
# #

View file

@ -4,6 +4,7 @@ import logging
import fedora_messaging.config import fedora_messaging.config
import fedora_messaging.exceptions import fedora_messaging.exceptions
from .utils.cache import set_cache
from .utils.misc import merge_dicts from .utils.misc import merge_dicts
@ -25,6 +26,7 @@ class ToddlerBase(metaclass=abc.ABCMeta):
def __init__(self): def __init__(self):
self.reset_routing_keys() self.reset_routing_keys()
self._config = _get_toddler_config(self.name) self._config = _get_toddler_config(self.name)
set_cache(fedora_messaging.config.conf["consumer_config"].get("cache", {}))
def reset_routing_keys(self): def reset_routing_keys(self):
# Only for dev/debug: in Fedora infra all bindings are set in ansible # Only for dev/debug: in Fedora infra all bindings are set in ansible

18
toddlers/utils/cache.py Normal file
View file

@ -0,0 +1,18 @@
"""
This module contains the caching system.
"""
from dogpile.cache import exception, make_region
from dogpile.cache.util import kwarg_function_key_generator
cache = make_region(function_key_generator=kwarg_function_key_generator)
def set_cache(conf: dict):
# We use memory_pickle instead of memory to catch attempts at caching
# non-serializable values.
conf.setdefault("backend", "dogpile.cache.memory_pickle")
try:
cache.configure(**conf)
except exception.RegionAlreadyConfigured:
pass

View file

@ -22,6 +22,7 @@ from typing import Any, List, Optional
from toddlers.exceptions import PagureError from toddlers.exceptions import PagureError
from toddlers.utils import requests from toddlers.utils import requests
from toddlers.utils.cache import cache
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -36,6 +37,9 @@ def set_pagure(conf: dict) -> Any:
return Pagure(conf) return Pagure(conf)
ORPHAN_USERNAME = "orphan"
class Pagure: class Pagure:
""" """
Object that is a wrapper above pagure API. Object that is a wrapper above pagure API.
@ -685,6 +689,7 @@ class Pagure:
return result return result
@cache.cache_on_arguments(expiration_time=3600)
def get_branches(self, namespace: str, repo: str) -> Any: def get_branches(self, namespace: str, repo: str) -> Any:
""" """
Return all branches for the specified repository. Return all branches for the specified repository.
@ -740,6 +745,7 @@ class Pagure:
return result return result
@cache.cache_on_arguments(expiration_time=3600)
def get_default_branch(self, namespace: str, repo: str) -> Any: def get_default_branch(self, namespace: str, repo: str) -> Any:
""" """
Return the default branch for the specified repository. Return the default branch for the specified repository.
@ -850,6 +856,7 @@ class Pagure:
return result return result
@cache.cache_on_arguments(expiration_time=3600)
def is_project_orphaned(self, namespace: str, repo: str) -> bool: def is_project_orphaned(self, namespace: str, repo: str) -> bool:
""" """
Check if project is orphaned. Check if project is orphaned.
@ -865,8 +872,6 @@ class Pagure:
Raises: Raises:
`toddlers.utils.exceptions.PagureError``: When getting project maintainers fails. `toddlers.utils.exceptions.PagureError``: When getting project maintainers fails.
""" """
keyword = "orphan"
endpoint_url = ( endpoint_url = (
"https://src.fedoraproject.org/_dg/bzoverrides/" + namespace + "/" + repo "https://src.fedoraproject.org/_dg/bzoverrides/" + namespace + "/" + repo
) )
@ -878,8 +883,8 @@ class Pagure:
if response.ok: if response.ok:
result = response.json() result = response.json()
if ( if (
result["epel_assignee"] == keyword result["epel_assignee"] == ORPHAN_USERNAME
or result["fedora_assignee"] == keyword or result["fedora_assignee"] == ORPHAN_USERNAME
): ):
return True return True
else: else:
@ -936,6 +941,8 @@ class Pagure:
namespace, repo namespace, repo
) )
) )
if maintainer_fas == ORPHAN_USERNAME:
self.is_project_orphaned.invalidate(self, namespace, repo) # type: ignore[attr-defined]
def add_member_to_group(self, user: str, group: str) -> None: def add_member_to_group(self, user: str, group: str) -> None:
""" """
@ -1103,6 +1110,7 @@ class Pagure:
return result return result
@cache.cache_on_arguments(expiration_time=7200)
def get_retired_packages(self, branch: str) -> list: def get_retired_packages(self, branch: str) -> list:
""" """
Retrieve list of retired packages in specific dist git branch. Retrieve list of retired packages in specific dist git branch.
@ -1282,13 +1290,14 @@ class Pagure:
) )
log.debug("Package '%s/%s' successfully orphaned.", namespace, package) log.debug("Package '%s/%s' successfully orphaned.", namespace, package)
self.is_project_orphaned.invalidate(self, namespace, package) # type: ignore[attr-defined]
def set_bugzilla_overrides( def set_bugzilla_overrides(
self, self,
namespace: str, namespace: str,
package: str, package: str,
fedora_assignee: Optional[str], fedora_assignee: Optional[str] = None,
epel_assignee: Optional[str], epel_assignee: Optional[str] = None,
) -> None: ) -> None:
""" """
Set the bugzilla overrides for package to specific value. Set the bugzilla overrides for package to specific value.
@ -1358,6 +1367,12 @@ class Pagure:
fedora_assignee, fedora_assignee,
epel_assignee, epel_assignee,
) )
if fedora_assignee == ORPHAN_USERNAME or epel_assignee == ORPHAN_USERNAME:
self.is_project_orphaned.invalidate( # type: ignore[attr-defined]
self,
namespace,
package,
)
def modify_acls( def modify_acls(
self, self,