mirror of
https://github.com/kiviktnm/decman.git
synced 2026-09-19 12:08:28 +00:00
Merge pull request #51 from kiviktnm/fix/consider-provides
Fix/consider provides
This commit is contained in:
@@ -8,6 +8,12 @@ dependencies = [
|
||||
"requests",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pytest>=8.4.2",
|
||||
"pytest-mock>=3.15.1",
|
||||
]
|
||||
|
||||
[project.entry-points."decman.plugins"]
|
||||
pacman = "decman.plugins.pacman:Pacman"
|
||||
aur = "decman.plugins.aur:AUR"
|
||||
|
||||
@@ -208,7 +208,8 @@ class AUR(plugins.Plugin):
|
||||
|
||||
output.print_summary("Upgrading foreign packages.")
|
||||
if not dry_run:
|
||||
fpm.upgrade(upgrade_devel, force, self.ignored_packages)
|
||||
# don't try to upgrade removed packages
|
||||
fpm.upgrade(upgrade_devel, force, self.ignored_packages | actually_to_remove)
|
||||
|
||||
to_install = (
|
||||
(self.packages | custom_package_names)
|
||||
|
||||
@@ -143,6 +143,16 @@ class AurPacmanInterface(pacman.PacmanInterface):
|
||||
"""
|
||||
return self._get_orphans(pacman.PacmanInterface._is_foreign)
|
||||
|
||||
def is_provided_by_installed(self, dependency: str) -> bool:
|
||||
return pacman.strip_dependency(dependency) in self._local_provides_index
|
||||
|
||||
def filter_installed_packages(self, deps: set[str]) -> set[str]:
|
||||
out = set()
|
||||
for d in deps:
|
||||
if not self.is_provided_by_installed(d) and d not in self.get_all_packages():
|
||||
out.add(d)
|
||||
return out
|
||||
|
||||
def is_installable(self, pkg: str) -> bool:
|
||||
"""
|
||||
Returns True if a package can be installed using pacman.
|
||||
|
||||
@@ -135,6 +135,9 @@ class ResolvedDependencies:
|
||||
self.foreign_build_dep_pkgs: set[str] = set()
|
||||
self.build_order: list[str] = []
|
||||
self.packages: dict[str, ForeignPackage] = {}
|
||||
# maps dependency names to package names
|
||||
self.providers: dict[str, list[str]] = {}
|
||||
self.all_provided: set[str] = set()
|
||||
self._pkgbases_to_pkgs: dict[str, set[str]] = {}
|
||||
self._pkgs_to_pkgbases: dict[str, str] = {}
|
||||
|
||||
@@ -277,8 +280,11 @@ class ForeignPackageManager:
|
||||
if not output.prompt_confirm("Proceed?", default=True):
|
||||
raise ForeignPackageManagerError("Installing aborted by the user.")
|
||||
|
||||
needed_pacman_deps = self._pacman.filter_installed_packages(
|
||||
resolved_dependencies.pacman_deps - resolved_dependencies.all_provided
|
||||
)
|
||||
output.print_summary("Installing foreign package dependencies from pacman.")
|
||||
self._pacman.install_dependencies(resolved_dependencies.pacman_deps)
|
||||
self._pacman.install_dependencies(needed_pacman_deps)
|
||||
|
||||
try:
|
||||
with PackageBuilder(
|
||||
@@ -319,7 +325,8 @@ class ForeignPackageManager:
|
||||
output.print_summary("Installing foreign packages.")
|
||||
self._pacman.install_files(
|
||||
package_files_to_install,
|
||||
as_explicit=resolved_dependencies.foreign_pkgs,
|
||||
as_explicit=resolved_dependencies.foreign_pkgs
|
||||
- resolved_dependencies.foreign_dep_pkgs,
|
||||
)
|
||||
else:
|
||||
output.print_summary("No packages to install.")
|
||||
@@ -379,6 +386,10 @@ class ForeignPackageManager:
|
||||
f"Failed to find '{pkgname}' from AUR or user provided packages."
|
||||
)
|
||||
|
||||
for provided in info.provides:
|
||||
result.providers.setdefault(provided, []).append(pkgname)
|
||||
result.all_provided.add(provided)
|
||||
|
||||
result.pacman_deps.update(info.native_dependencies(self._pacman))
|
||||
result.add_pkgbase_info(pkgname, info.pkgbase)
|
||||
|
||||
|
||||
@@ -208,6 +208,7 @@ class PacmanInterface:
|
||||
self._dbpath = dbpath
|
||||
self._handle = self._create_pyalpm_handle()
|
||||
self._name_index = self._create_name_index()
|
||||
self._local_provides_index = self._create_local_provides_index()
|
||||
self._provides_index = self._create_provides_index()
|
||||
self._requiredby_index = self._create_requiredby_index()
|
||||
|
||||
@@ -231,6 +232,14 @@ class PacmanInterface:
|
||||
def _create_name_index(self) -> dict[str, pyalpm.Package]:
|
||||
return {pkg.name: pkg for db in self._handle.get_syncdbs() for pkg in db.pkgcache}
|
||||
|
||||
def _create_local_provides_index(self) -> dict[str, set[str]]:
|
||||
out: dict[str, set[str]] = {}
|
||||
for pkg in self._handle.get_localdb().pkgcache:
|
||||
for p in pkg.provides:
|
||||
out.setdefault(strip_dependency(p), set()).add(pkg.name)
|
||||
out.setdefault(p, set()).add(pkg.name)
|
||||
return out
|
||||
|
||||
def _create_provides_index(self) -> dict[str, set[str]]:
|
||||
out: dict[str, set[str]] = {}
|
||||
for db in self._handle.get_syncdbs():
|
||||
@@ -249,6 +258,12 @@ class PacmanInterface:
|
||||
def _is_foreign(self, package: str) -> bool:
|
||||
return not self._is_native(package)
|
||||
|
||||
def get_all_packages(self) -> set[str]:
|
||||
"""
|
||||
Returns a set of all installed packages.
|
||||
"""
|
||||
return {pkg for pkg in self._handle.get_localdb().pkgcache}
|
||||
|
||||
def get_native_explicit(self) -> set[str]:
|
||||
"""
|
||||
Returns a set of explicitly installed native packages.
|
||||
|
||||
@@ -245,7 +245,11 @@ def test_apply_respects_ignored_packages_and_protects_their_dependencies(
|
||||
assert "ignored-aur" not in (fake_pm.remove_called_with or set())
|
||||
|
||||
# Upgrade called with flags and ignored set
|
||||
assert fake_fpm.upgrade_args == (True, True, aur.ignored_packages)
|
||||
assert fake_fpm.upgrade_args == (
|
||||
True,
|
||||
True,
|
||||
aur.ignored_packages | (fake_pm.remove_called_with or set()),
|
||||
)
|
||||
|
||||
# to_install = (packages | custom_names) - installed_foreign - ignored
|
||||
# = {"desired-aur", "custom-aur"} - {"ignored-aur", "dep-of-ignored", "orphan-foreign"}
|
||||
|
||||
@@ -0,0 +1,383 @@
|
||||
import typing
|
||||
from unittest.mock import MagicMock
|
||||
from urllib.parse import parse_qs, unquote, urlparse
|
||||
|
||||
import pytest
|
||||
from decman.plugins.aur.commands import AurCommands
|
||||
from decman.plugins.aur.fpm import ForeignPackageManager
|
||||
from decman.plugins.aur.package import PackageInfo, PackageSearch
|
||||
|
||||
|
||||
class FakeAurPacmanInterface:
|
||||
def __init__(self) -> None:
|
||||
self.installed_native: set[str] = set()
|
||||
self.installed_foreign: dict[str, str] = {}
|
||||
self.explicitly_installed: set[str] = set()
|
||||
self.not_installable: set[str] = set()
|
||||
self.installed_files: list[str] = [] # To track what install_files() actually does
|
||||
self.provided_pkgs: set[str] = set()
|
||||
|
||||
def get_native_explicit(self) -> set[str]:
|
||||
return self.installed_native.intersection(self.explicitly_installed)
|
||||
|
||||
def get_native_orphans(self) -> set[str]:
|
||||
return set()
|
||||
|
||||
def get_foreign_explicit(self) -> set[str]:
|
||||
return set(self.installed_foreign.keys()).intersection(self.explicitly_installed)
|
||||
|
||||
def get_dependants(self, package: str) -> set[str]:
|
||||
return set()
|
||||
|
||||
def set_as_dependencies(self, packages: set[str]):
|
||||
self.explicitly_installed.difference_update(packages)
|
||||
|
||||
def install(self, packages: set[str]):
|
||||
self.installed_native.update(packages)
|
||||
self.explicitly_installed.update(packages)
|
||||
|
||||
def upgrade(self):
|
||||
pass
|
||||
|
||||
def is_provided_by_installed(self, dependency: str) -> bool:
|
||||
return dependency in self.provided_pkgs
|
||||
|
||||
def get_all_packages(self) -> set[str]:
|
||||
return self.installed_native | self.installed_foreign.keys()
|
||||
|
||||
def filter_installed_packages(self, deps: set[str]) -> set[str]:
|
||||
out = set()
|
||||
for d in deps:
|
||||
if not self.is_provided_by_installed(d) and d not in self.get_all_packages():
|
||||
out.add(d)
|
||||
return out
|
||||
|
||||
def remove(self, packages: set[str]):
|
||||
self.installed_native.difference_update(packages)
|
||||
for p in packages:
|
||||
self.installed_foreign.pop(p, None)
|
||||
self.explicitly_installed.difference_update(packages)
|
||||
|
||||
def get_foreign_orphans(self) -> set[str]:
|
||||
return set()
|
||||
|
||||
def is_installable(self, pkg: str) -> bool:
|
||||
return pkg not in self.not_installable
|
||||
|
||||
def get_versioned_foreign_packages(self) -> list[tuple[str, str]]:
|
||||
return list(self.installed_foreign.items())
|
||||
|
||||
def install_dependencies(self, deps: set[str]):
|
||||
self.installed_native.update(deps)
|
||||
|
||||
def install_files(self, files: list[str], as_explicit: set[str]):
|
||||
self.installed_files.extend(files)
|
||||
|
||||
for file in files:
|
||||
self.installed_foreign[file] = "file"
|
||||
|
||||
for pkg in as_explicit:
|
||||
self.explicitly_installed.add(pkg)
|
||||
|
||||
|
||||
class FakeStore:
|
||||
def __init__(self) -> None:
|
||||
self._store: dict[str, typing.Any] = {}
|
||||
|
||||
def __getitem__(self, key: str) -> typing.Any:
|
||||
return self._store[key]
|
||||
|
||||
def __setitem__(self, key: str, value: typing.Any) -> None:
|
||||
self._store[key] = value
|
||||
|
||||
def get(self, key: str, default: typing.Any = None) -> typing.Any:
|
||||
return self._store.get(key, default)
|
||||
|
||||
def ensure(self, key: str, default: typing.Any = None):
|
||||
if key not in self._store:
|
||||
self._store[key] = default
|
||||
|
||||
def __enter__(self) -> "FakeStore":
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
return False
|
||||
|
||||
def save(self) -> None:
|
||||
pass
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return repr(self._store)
|
||||
|
||||
|
||||
class MockAurServer:
|
||||
def __init__(self) -> None:
|
||||
self.db: dict[str, dict] = {} # Maps pkgname -> raw JSON result dict
|
||||
|
||||
def seed(self, packages: list[PackageInfo]):
|
||||
for pkg in packages:
|
||||
# Reconstruct the raw JSON structure expected by PackageSearch
|
||||
entry = {
|
||||
"Name": pkg.pkgname,
|
||||
"PackageBase": pkg.pkgbase or pkg.pkgname,
|
||||
"Version": pkg.version,
|
||||
"Description": "Mock Description",
|
||||
"URL": "https://example.com",
|
||||
"Depends": pkg.dependencies,
|
||||
"MakeDepends": pkg.make_dependencies,
|
||||
"CheckDepends": pkg.check_dependencies,
|
||||
"Provides": pkg.provides,
|
||||
# Add other fields if your class relies on them
|
||||
}
|
||||
self.db[pkg.pkgname] = entry
|
||||
|
||||
def handle_request(self, url, *args, **kwargs):
|
||||
parsed = urlparse(url)
|
||||
path = parsed.path
|
||||
query = parse_qs(parsed.query)
|
||||
|
||||
results = []
|
||||
|
||||
# --- Handle: Multi-info query (.../info?arg[]=pkg1&arg[]=pkg2) ---
|
||||
if "/rpc/v5/info" in path and "arg[]" in query:
|
||||
requested_names = query["arg[]"]
|
||||
for name in requested_names:
|
||||
if name in self.db:
|
||||
results.append(self.db[name])
|
||||
|
||||
# --- Handle: Single info query (.../rpc/v5/info/pkgname) ---
|
||||
elif "/rpc/v5/info/" in path:
|
||||
# Extract package name from end of path
|
||||
pkg_name = path.split("/")[-1]
|
||||
if pkg_name in self.db:
|
||||
results.append(self.db[pkg_name])
|
||||
|
||||
# --- Handle: Search providers (.../rpc/v5/search/dep?by=provides) ---
|
||||
elif "/rpc/v5/search/" in path and query.get("by") == ["provides"]:
|
||||
search_term = path.split("/")[-1]
|
||||
search_term = unquote(search_term)
|
||||
|
||||
# Linear search through DB for 'Provides'
|
||||
for entry in self.db.values():
|
||||
if search_term in entry.get("Provides", []):
|
||||
results.append(entry)
|
||||
# Also match if the package name itself matches the provider request
|
||||
elif entry["Name"] == search_term:
|
||||
results.append(entry)
|
||||
|
||||
# Construct the response object
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"version": 5,
|
||||
"type": "multiinfo",
|
||||
"resultcount": len(results),
|
||||
"results": results,
|
||||
}
|
||||
|
||||
return mock_response
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_aur(mocker):
|
||||
server = MockAurServer()
|
||||
mocker.patch("requests.get", side_effect=server.handle_request)
|
||||
return server
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_pacman(mocker):
|
||||
pacman = FakeAurPacmanInterface()
|
||||
return pacman
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_fpm(mocker, mock_aur, mock_pacman):
|
||||
mock_builder_cls = mocker.patch("decman.plugins.aur.fpm.PackageBuilder")
|
||||
mock_builder_instance = mock_builder_cls.return_value
|
||||
mock_builder_instance.__enter__.return_value = mock_builder_instance
|
||||
mock_builder_instance.__exit__.return_value = None
|
||||
|
||||
# NOTE: find_latest_cached_package must return a tuple, otherwise
|
||||
# the 'assert built_pkg is not None' line in install() will fail.
|
||||
def mock_find_cached(store, package):
|
||||
# return just the package, so that mock pacman can get the package name from the 'file' name
|
||||
return ("1.0.0", package)
|
||||
|
||||
mocker.patch("decman.plugins.aur.fpm.find_latest_cached_package", side_effect=mock_find_cached)
|
||||
mocker.patch("decman.plugins.aur.fpm.add_package_to_cache", return_value=None)
|
||||
|
||||
# handle prompts automatically
|
||||
mocker.patch("decman.core.output.prompt_confirm", return_value=True)
|
||||
|
||||
store = FakeStore()
|
||||
search = PackageSearch()
|
||||
commands = AurCommands()
|
||||
mgr = ForeignPackageManager(
|
||||
store=store, # type: ignore
|
||||
pacman=mock_pacman,
|
||||
search=search,
|
||||
commands=commands,
|
||||
pkg_cache_dir="/tmp/cache",
|
||||
build_dir="/tmp/build",
|
||||
makepkg_user="nobody",
|
||||
)
|
||||
|
||||
return mgr
|
||||
|
||||
|
||||
def test_remove_pacman_deps_provided_by_foreign_packages(
|
||||
mock_fpm, mock_aur, mock_pacman: FakeAurPacmanInterface
|
||||
):
|
||||
mock_pacman.not_installable |= {"kwin-hifps", "qt6-base-hifps", "syncthingtray-qt6"}
|
||||
pkgs = [
|
||||
PackageInfo(
|
||||
pkgbase="kwin-hifps",
|
||||
pkgname="kwin-hifps",
|
||||
version="1",
|
||||
git_url="...",
|
||||
dependencies=("qt6-base-hifps",),
|
||||
),
|
||||
PackageInfo(
|
||||
pkgbase="qt6-base-hifps",
|
||||
pkgname="qt6-base-hifps",
|
||||
version="1",
|
||||
git_url="...",
|
||||
provides=("qt6-base",),
|
||||
),
|
||||
PackageInfo(
|
||||
pkgbase="syncthingtray-qt6",
|
||||
pkgname="syncthingtray-qt6",
|
||||
version="1",
|
||||
git_url="...",
|
||||
dependencies=("qt6-base",),
|
||||
),
|
||||
]
|
||||
mock_aur.seed(pkgs)
|
||||
|
||||
mock_fpm.install(["kwin-hifps", "syncthingtray-qt6"])
|
||||
|
||||
assert len(mock_pacman.installed_files) == 3
|
||||
assert mock_pacman.explicitly_installed == {"kwin-hifps", "syncthingtray-qt6"}
|
||||
assert "qt6-base" not in mock_pacman.installed_native
|
||||
|
||||
|
||||
def test_remove_pacman_deps_provided_by_already_installed_foreign_packages(
|
||||
mock_fpm, mock_aur, mock_pacman: FakeAurPacmanInterface
|
||||
):
|
||||
mock_pacman.not_installable |= {"kwin-hifps", "qt6-base-hifps", "syncthingtray-qt6"}
|
||||
pkgs = [
|
||||
PackageInfo(
|
||||
pkgbase="kwin-hifps",
|
||||
pkgname="kwin-hifps",
|
||||
version="1",
|
||||
git_url="...",
|
||||
dependencies=("qt6-base-hifps",),
|
||||
),
|
||||
PackageInfo(
|
||||
pkgbase="qt6-base-hifps",
|
||||
pkgname="qt6-base-hifps",
|
||||
version="1",
|
||||
git_url="...",
|
||||
provides=("qt6-base",),
|
||||
),
|
||||
PackageInfo(
|
||||
pkgbase="syncthingtray-qt6",
|
||||
pkgname="syncthingtray-qt6",
|
||||
version="1",
|
||||
git_url="...",
|
||||
dependencies=("qt6-base",),
|
||||
),
|
||||
]
|
||||
mock_pacman.installed_foreign = {
|
||||
"kwin-hifps": "1",
|
||||
"qt6-base-hifps": "1",
|
||||
}
|
||||
mock_pacman.explicitly_installed.add("kwin-hifps")
|
||||
mock_pacman.provided_pkgs.add("qt6-base")
|
||||
mock_aur.seed(pkgs)
|
||||
|
||||
mock_fpm.install(["syncthingtray-qt6"])
|
||||
|
||||
assert len(mock_pacman.installed_files) == 1
|
||||
assert mock_pacman.explicitly_installed == {"kwin-hifps", "syncthingtray-qt6"}
|
||||
assert "qt6-base" not in mock_pacman.installed_native
|
||||
|
||||
|
||||
def test_install_simple_package(
|
||||
mock_fpm, mock_pacman: FakeAurPacmanInterface, mock_aur: MockAurServer
|
||||
):
|
||||
mock_pacman.not_installable.add("foo")
|
||||
pkg = PackageInfo(
|
||||
pkgbase="foo",
|
||||
pkgname="foo",
|
||||
version="100.0.0",
|
||||
git_url="...",
|
||||
)
|
||||
mock_aur.seed([pkg])
|
||||
|
||||
mock_fpm.install(["foo"])
|
||||
|
||||
assert len(mock_pacman.installed_files) == 1
|
||||
assert "foo" in mock_pacman.installed_files[0]
|
||||
assert "foo" in mock_pacman.explicitly_installed
|
||||
assert "foo" in mock_pacman.installed_foreign
|
||||
|
||||
|
||||
def test_upgrade_foreign_package(mock_fpm, mock_pacman, mock_aur):
|
||||
mock_pacman.not_installable.add("my-app")
|
||||
mock_pacman.installed_foreign = {"my-app": "1.0"}
|
||||
mock_pacman.explicitly_installed = {"my-app"}
|
||||
|
||||
pkg = PackageInfo(
|
||||
pkgbase="my-app",
|
||||
pkgname="my-app",
|
||||
version="2.0",
|
||||
git_url="...",
|
||||
)
|
||||
mock_aur.seed([pkg])
|
||||
|
||||
mock_fpm.upgrade()
|
||||
|
||||
assert len(mock_pacman.installed_files) == 1
|
||||
assert "my-app" in mock_pacman.installed_foreign
|
||||
assert "my-app" in mock_pacman.installed_files[0]
|
||||
|
||||
|
||||
def test_upgrade_skips_current_package(mock_fpm, mock_pacman, mock_aur):
|
||||
mock_pacman.not_installable.add("stable-app")
|
||||
mock_pacman.installed_foreign = {"stable-app": "5.0"}
|
||||
mock_pacman.explicitly_installed = {"stable-app"}
|
||||
|
||||
pkg = PackageInfo(
|
||||
pkgbase="stable-app",
|
||||
pkgname="stable-app",
|
||||
version="5.0",
|
||||
git_url="...",
|
||||
)
|
||||
mock_aur.seed([pkg])
|
||||
|
||||
mock_fpm.upgrade()
|
||||
|
||||
assert len(mock_pacman.installed_files) == 0
|
||||
|
||||
|
||||
def test_install_resolves_dependencies(mock_fpm, mock_pacman, mock_aur):
|
||||
mock_pacman.not_installable |= {"lib-helper", "main-app"}
|
||||
pkg_dep = PackageInfo(pkgbase="lib-helper", pkgname="lib-helper", version="1.5", git_url="...")
|
||||
pkg_main = PackageInfo(
|
||||
pkgbase="main-app",
|
||||
pkgname="main-app",
|
||||
version="2.0",
|
||||
dependencies=("lib-helper",),
|
||||
git_url="...",
|
||||
)
|
||||
|
||||
mock_aur.seed([pkg_dep, pkg_main])
|
||||
|
||||
mock_fpm.install(["main-app"])
|
||||
|
||||
assert len(mock_pacman.installed_files) == 2
|
||||
assert "main-app" in mock_pacman.explicitly_installed
|
||||
assert "main-app" in mock_pacman.installed_files
|
||||
assert "lib-helper" in mock_pacman.installed_files
|
||||
@@ -126,6 +126,12 @@ dependencies = [
|
||||
{ name = "requests" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-mock" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "decman", editable = "." },
|
||||
@@ -133,6 +139,12 @@ requires-dist = [
|
||||
{ name = "requests" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "pytest", specifier = ">=8.4.2" },
|
||||
{ name = "pytest-mock", specifier = ">=3.15.1" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "decman-systemd"
|
||||
version = "1.0.0"
|
||||
@@ -211,6 +223,18 @@ 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-mock"
|
||||
version = "3.15.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pytest" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.5"
|
||||
|
||||
Reference in New Issue
Block a user