mirror of
https://github.com/d9pouces/id_configurator.git
synced 2026-11-09 01:47:21 +01:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
db4ff3cfd4
|
|||
|
cea45c97ff
|
|||
|
b2dcae0daf
|
|||
|
be91425355
|
|||
|
4409c38579
|
|||
|
380e976f76
|
+85
-33
@@ -26,8 +26,31 @@ def _yes_no(value: bool) -> str:
|
||||
return "yes" if value else "no"
|
||||
|
||||
|
||||
def _fold_deb822_multiline(field: str, value: str) -> str:
|
||||
"""Render `field` as a DEB822 folded multi-line field (sources.list(5)):
|
||||
each of `value`'s own lines indented by one space, with a truly blank
|
||||
line represented as a lone `.` - exactly how an ASCII-armored GPG key
|
||||
block is embedded directly in a `Signed-By` field, verified against a
|
||||
real `apt-get update` run against a genuinely signed repository."""
|
||||
folded = "\n".join(f" {line}" if line else " ." for line in value.splitlines())
|
||||
return f"{field}:\n{folded}"
|
||||
|
||||
|
||||
class AptSource(BaseModel):
|
||||
"""A single APT source, in the DEB822 format (see sources.list(5))."""
|
||||
"""A single APT source, in the DEB822 format (see sources.list(5)).
|
||||
|
||||
Two ways to tell APT which key to trust this source with:
|
||||
- `signed_by`: the ASCII-armored (.asc) *content* of a GPG public key,
|
||||
folded inline as a multi-line `Signed-By` field - no separate keyring
|
||||
file, no `gpg` binary needed to apply this configuration.
|
||||
- `signed_by_file`: the *path* to a keyring already expected to exist on
|
||||
the target system, written to the rendered `Signed-By` field verbatim.
|
||||
If both are set, `signed_by` takes precedence.
|
||||
|
||||
`uri_to_replace`, if set, is an fnmatch pattern: this source is only
|
||||
applied if it matches at least one URI already present in the machine's
|
||||
existing apt configuration; otherwise it is skipped entirely.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
@@ -42,6 +65,7 @@ class AptSource(BaseModel):
|
||||
enabled: bool = True
|
||||
trusted: bool | None = None
|
||||
signed_by: str | None = Field(default=None, alias="signed-by")
|
||||
signed_by_file: str | None = Field(default=None, alias="signed-by-file")
|
||||
pdiffs: bool | None = None
|
||||
by_hash: bool | None = Field(default=None, alias="by-hash")
|
||||
allow_insecure: bool | None = Field(default=None, alias="allow-insecure")
|
||||
@@ -55,67 +79,86 @@ class AptSource(BaseModel):
|
||||
check_date: bool | None = Field(default=None, alias="check-date")
|
||||
date_max_future: str | None = Field(default=None, alias="date-max-future")
|
||||
description: str | None = None
|
||||
uri_to_replace: str | None = Field(default=None, alias="uri-to-replace")
|
||||
|
||||
def to_deb822(self) -> str:
|
||||
"""Render this source as a DEB822 stanza (as used in /etc/apt/sources.list.d/*.sources)."""
|
||||
fields = [
|
||||
("Types", " ".join(self.types)),
|
||||
("URIs", " ".join(self.uris)),
|
||||
("Suites", " ".join(self.suites)),
|
||||
def to_deb822(
|
||||
self,
|
||||
signed_by_armored_key: str | None = None,
|
||||
signed_by_path: str | None = None,
|
||||
) -> str:
|
||||
"""Render this source as a DEB822 stanza (as used in
|
||||
/etc/apt/sources.list.d/*.sources).
|
||||
|
||||
Neither `self.signed_by` nor `self.signed_by_file` is ever rendered
|
||||
directly - the caller (the `apt` module) resolves which one applies
|
||||
and passes the result in: `signed_by_armored_key` is folded inline
|
||||
as a multi-line `Signed-By` field (see `_fold_deb822_multiline`);
|
||||
`signed_by_path` is written as a plain single-line `Signed-By`
|
||||
field. If both are given, `signed_by_armored_key` takes precedence.
|
||||
"""
|
||||
lines = [
|
||||
f"Types: {' '.join(self.types)}",
|
||||
f"URIs: {' '.join(self.uris)}",
|
||||
f"Suites: {' '.join(self.suites)}",
|
||||
]
|
||||
if self.components:
|
||||
fields.append(("Components", " ".join(self.components)))
|
||||
lines.append(f"Components: {' '.join(self.components)}")
|
||||
if self.architectures:
|
||||
fields.append(("Architectures", " ".join(self.architectures)))
|
||||
lines.append(f"Architectures: {' '.join(self.architectures)}")
|
||||
if self.languages:
|
||||
fields.append(("Languages", " ".join(self.languages)))
|
||||
lines.append(f"Languages: {' '.join(self.languages)}")
|
||||
if self.targets:
|
||||
fields.append(("Targets", " ".join(self.targets)))
|
||||
if self.signed_by:
|
||||
fields.append(("Signed-By", self.signed_by))
|
||||
lines.append(f"Targets: {' '.join(self.targets)}")
|
||||
if signed_by_armored_key:
|
||||
lines.append(_fold_deb822_multiline("Signed-By", signed_by_armored_key))
|
||||
elif signed_by_path:
|
||||
lines.append(f"Signed-By: {signed_by_path}")
|
||||
if self.trusted is not None:
|
||||
fields.append(("Trusted", _yes_no(self.trusted)))
|
||||
lines.append(f"Trusted: {_yes_no(self.trusted)}")
|
||||
if self.pdiffs is not None:
|
||||
fields.append(("PDiffs", _yes_no(self.pdiffs)))
|
||||
lines.append(f"PDiffs: {_yes_no(self.pdiffs)}")
|
||||
if self.by_hash is not None:
|
||||
fields.append(("By-Hash", _yes_no(self.by_hash)))
|
||||
lines.append(f"By-Hash: {_yes_no(self.by_hash)}")
|
||||
if self.allow_insecure is not None:
|
||||
fields.append(("Allow-Insecure", _yes_no(self.allow_insecure)))
|
||||
lines.append(f"Allow-Insecure: {_yes_no(self.allow_insecure)}")
|
||||
if self.allow_weak is not None:
|
||||
fields.append(("Allow-Weak", _yes_no(self.allow_weak)))
|
||||
lines.append(f"Allow-Weak: {_yes_no(self.allow_weak)}")
|
||||
if self.allow_downgrade_to_insecure is not None:
|
||||
fields.append(
|
||||
(
|
||||
"Allow-Downgrade-To-Insecure",
|
||||
_yes_no(self.allow_downgrade_to_insecure),
|
||||
)
|
||||
lines.append(
|
||||
f"Allow-Downgrade-To-Insecure: {_yes_no(self.allow_downgrade_to_insecure)}"
|
||||
)
|
||||
if self.check_valid_until is not None:
|
||||
fields.append(("Check-Valid-Until", _yes_no(self.check_valid_until)))
|
||||
lines.append(f"Check-Valid-Until: {_yes_no(self.check_valid_until)}")
|
||||
if self.valid_until_min:
|
||||
fields.append(("Valid-Until-Min", self.valid_until_min))
|
||||
lines.append(f"Valid-Until-Min: {self.valid_until_min}")
|
||||
if self.valid_until_max:
|
||||
fields.append(("Valid-Until-Max", self.valid_until_max))
|
||||
lines.append(f"Valid-Until-Max: {self.valid_until_max}")
|
||||
if self.check_date is not None:
|
||||
fields.append(("Check-Date", _yes_no(self.check_date)))
|
||||
lines.append(f"Check-Date: {_yes_no(self.check_date)}")
|
||||
if self.date_max_future:
|
||||
fields.append(("Date-Max-Future", self.date_max_future))
|
||||
lines.append(f"Date-Max-Future: {self.date_max_future}")
|
||||
if self.description:
|
||||
fields.append(("Description", self.description))
|
||||
fields.append(("Enabled", _yes_no(self.enabled)))
|
||||
return "\n".join(f"{key}: {value}" for key, value in fields) + "\n"
|
||||
lines.append(f"Description: {self.description}")
|
||||
lines.append(f"Enabled: {_yes_no(self.enabled)}")
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
class AptConfig(BaseModel):
|
||||
sources: list[AptSource] = Field(default_factory=list)
|
||||
keys: list[NamedContent] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ApkRepository(BaseModel):
|
||||
"""An Alpine APK repository (see /etc/apk/repositories), e.g. a URL ending in /main or /community."""
|
||||
"""An Alpine APK repository (see /etc/apk/repositories), e.g. a URL ending in /main or /community.
|
||||
|
||||
`url_to_replace`, if set, is an fnmatch pattern: this repository is only
|
||||
applied if it matches at least one URL already present in the machine's
|
||||
existing apk configuration; otherwise it is skipped entirely.
|
||||
"""
|
||||
|
||||
name: str
|
||||
url: str
|
||||
key: str | None = None
|
||||
url_to_replace: str | None = None
|
||||
|
||||
|
||||
class ApkConfig(BaseModel):
|
||||
@@ -123,10 +166,19 @@ class ApkConfig(BaseModel):
|
||||
|
||||
|
||||
class YumRepository(BaseModel):
|
||||
"""A YUM/DNF repository (see /etc/yum.repos.d/*.repo).
|
||||
|
||||
`index_url_to_replace`, if set, is an fnmatch pattern: this repository is
|
||||
only applied if it matches at least one baseurl already present in the
|
||||
machine's existing yum/dnf configuration; otherwise it is skipped
|
||||
entirely.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
name: str
|
||||
index_url: str = Field(alias="index-url")
|
||||
gpg_url: str | None = Field(default=None, alias="gpg-url")
|
||||
index_url_to_replace: str | None = Field(default=None, alias="index-url-to-replace")
|
||||
|
||||
|
||||
class YumConfig(BaseModel):
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
"""Detect existing APK (Alpine) repositories and configure new ones.
|
||||
|
||||
If `<src>/etc/apk` exists, the existing `/etc/apk/repositories` file (if any)
|
||||
is copied into `<dst>` with every active line disabled (commented out), and
|
||||
the repositories declared in the configuration are appended as new, active
|
||||
lines. Public keys provided alongside a repository are written to
|
||||
is copied into `<dst>`, and the repositories declared in the configuration
|
||||
are appended as new, active lines. An existing active line is only disabled
|
||||
(commented out) if it matches the `url_to_replace` pattern of one of the
|
||||
configured repositories; lines matching no such pattern are copied through
|
||||
unchanged. Public keys provided alongside a repository are written to
|
||||
`<dst>/etc/apk/keys/`, named after their content so that repositories
|
||||
sharing the same key (a common case for official Alpine mirrors) only
|
||||
produce a single key file.
|
||||
|
||||
A repository's `url_to_replace`, if set, is an fnmatch pattern checked
|
||||
against the URLs already active in /etc/apk/repositories: the repository
|
||||
is only written out if the pattern matches at least one of them, and
|
||||
skipped entirely otherwise.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import fnmatch
|
||||
import hashlib
|
||||
import logging
|
||||
from collections.abc import Sequence
|
||||
from pathlib import Path
|
||||
|
||||
from id_configurator.context import Context
|
||||
@@ -25,11 +34,18 @@ _REPOSITORIES = "etc/apk/repositories"
|
||||
_KEYS_DIR = "etc/apk/keys"
|
||||
|
||||
|
||||
def _disable_repositories_content(content: str) -> str:
|
||||
lines = [
|
||||
line if not line.strip() or line.strip().startswith("#") else f"# {line}"
|
||||
for line in content.splitlines()
|
||||
]
|
||||
def _disable_repositories_content(content: str, patterns: Sequence[str]) -> str:
|
||||
lines = []
|
||||
for line in content.splitlines():
|
||||
stripped = line.strip()
|
||||
if (
|
||||
stripped
|
||||
and not stripped.startswith("#")
|
||||
and any(fnmatch.fnmatch(stripped, pattern) for pattern in patterns)
|
||||
):
|
||||
lines.append(f"# {line}")
|
||||
else:
|
||||
lines.append(line)
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
@@ -37,6 +53,19 @@ def _key_filename(content: bytes) -> str:
|
||||
return f"{hashlib.sha256(content).hexdigest()[:16]}.rsa.pub"
|
||||
|
||||
|
||||
def _existing_urls(context: Context) -> set[str]:
|
||||
"""Collect the URLs already declared in /etc/apk/repositories on the
|
||||
source filesystem (one active, non-comment line per URL)."""
|
||||
existing = context.read_src_file(_REPOSITORIES)
|
||||
if existing is None:
|
||||
return set()
|
||||
return {
|
||||
stripped
|
||||
for line in existing.decode("utf-8").splitlines()
|
||||
if (stripped := line.strip()) and not stripped.startswith("#")
|
||||
}
|
||||
|
||||
|
||||
@register
|
||||
class ApkModule(Module):
|
||||
name = "apk"
|
||||
@@ -52,26 +81,48 @@ class ApkModule(Module):
|
||||
return None
|
||||
|
||||
def apply(self, context: Context) -> None:
|
||||
disabled = self._disabled_existing_repositories(context)
|
||||
patterns = [
|
||||
repository.url_to_replace
|
||||
for repository in context.config.configurations.apk.repositories
|
||||
if repository.url_to_replace is not None
|
||||
]
|
||||
disabled = self._disabled_existing_repositories(context, patterns)
|
||||
configured = self._configured_repositories_lines(context)
|
||||
content = disabled + ("\n".join(configured) + "\n" if configured else "")
|
||||
if content:
|
||||
context.write_file(_REPOSITORIES, content.encode("utf-8"))
|
||||
|
||||
@staticmethod
|
||||
def _disabled_existing_repositories(context: Context) -> str:
|
||||
def _disabled_existing_repositories(
|
||||
context: Context, patterns: Sequence[str]
|
||||
) -> str:
|
||||
existing = context.read_src_file(_REPOSITORIES)
|
||||
if existing is None:
|
||||
return ""
|
||||
logger.info("disabled existing apk repositories %s", _REPOSITORIES)
|
||||
return _disable_repositories_content(existing.decode("utf-8"))
|
||||
logger.info("copied existing apk repositories %s", _REPOSITORIES)
|
||||
return _disable_repositories_content(existing.decode("utf-8"), patterns)
|
||||
|
||||
@staticmethod
|
||||
def _configured_repositories_lines(context: Context) -> list[str]:
|
||||
lines = []
|
||||
existing_urls: set[str] | None = None
|
||||
for repository in context.config.configurations.apk.repositories:
|
||||
if repository.url_to_replace is not None:
|
||||
if existing_urls is None:
|
||||
existing_urls = _existing_urls(context)
|
||||
if not any(
|
||||
fnmatch.fnmatch(url, repository.url_to_replace)
|
||||
for url in existing_urls
|
||||
):
|
||||
logger.info(
|
||||
"skipped apk repository %s: url-to-replace %s matched "
|
||||
"no existing url",
|
||||
repository.name,
|
||||
repository.url_to_replace,
|
||||
)
|
||||
continue
|
||||
lines.append(repository.url)
|
||||
logger.info("added apk repository %s", repository.url)
|
||||
logger.info("added apk repository %s", repository.name)
|
||||
if repository.key:
|
||||
filename = _key_filename(repository.key.encode("utf-8"))
|
||||
context.write_file(
|
||||
|
||||
+147
-34
@@ -2,45 +2,139 @@
|
||||
|
||||
If `<src>/etc/apt` exists, any source file already present there (classic
|
||||
one-line `.list` files or DEB822 `.sources` files, whether at the top level
|
||||
or under `sources.list.d/`) is copied into `<dst>/etc/apt`, disabled so it no
|
||||
longer takes effect, and the sources declared in the configuration are added
|
||||
as new, enabled `.sources` files.
|
||||
or under `sources.list.d/`) is copied into `<dst>/etc/apt`, and the sources
|
||||
declared in the configuration are added as new, enabled `.sources` files.
|
||||
|
||||
An existing entry (a single `deb`/`deb-src` line, or a DEB822 stanza) is only
|
||||
disabled if its URI matches the `uri_to_replace` pattern of one of the
|
||||
configured sources; entries matching no such pattern are copied through
|
||||
unchanged, still enabled.
|
||||
|
||||
Each source can be trusted two ways: `signed_by` holds the ASCII-armored
|
||||
content of a GPG public key, folded inline as a multi-line `Signed-By`
|
||||
field (per sources.list(5) - verified empirically against a real,
|
||||
genuinely signed repository, so no `gpg` binary or separate keyring file
|
||||
is needed at all to apply this configuration). `signed_by_file` instead
|
||||
names a keyring already expected to exist on the target system, used
|
||||
verbatim as `Signed-By`; if both are given, `signed_by` takes precedence.
|
||||
|
||||
A source's `uri_to_replace`, if set, is an fnmatch pattern checked against
|
||||
the URIs of the sources already present on the source filesystem: the
|
||||
source is only written out if the pattern matches at least one of them,
|
||||
and skipped entirely otherwise - even on an otherwise Debian-like machine.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import fnmatch
|
||||
import logging
|
||||
from collections.abc import Sequence
|
||||
from pathlib import Path
|
||||
|
||||
from id_configurator.context import Context
|
||||
from id_configurator.modules.base import Module, register
|
||||
from id_configurator.modules.base import Module, register, sanitize_filename
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_APT_DIR = "etc/apt"
|
||||
_SOURCES_LIST = "etc/apt/sources.list"
|
||||
_SOURCES_LIST_D = "etc/apt/sources.list.d"
|
||||
_KEYRINGS_DIR = "etc/apt/keyrings"
|
||||
|
||||
|
||||
def _disable_list_content(content: str) -> str:
|
||||
lines = [
|
||||
line if not line.strip() or line.strip().startswith("#") else f"# {line}"
|
||||
for line in content.splitlines()
|
||||
]
|
||||
def _uri_from_deb_line(stripped_line: str) -> str | None:
|
||||
"""Extract the URI from a single, already-stripped `deb`/`deb-src`
|
||||
line (`deb [options] uri suite [component...]`), or `None` if the line
|
||||
is not such a line."""
|
||||
parts = stripped_line.split()
|
||||
if not parts or parts[0] not in ("deb", "deb-src"):
|
||||
return None
|
||||
parts = parts[1:]
|
||||
while parts and parts[0].startswith("["):
|
||||
while parts and not parts[0].endswith("]"):
|
||||
parts.pop(0)
|
||||
if parts:
|
||||
parts.pop(0)
|
||||
return parts[0] if parts else None
|
||||
|
||||
|
||||
def _disable_list_content(content: str, patterns: Sequence[str]) -> str:
|
||||
lines = []
|
||||
for line in content.splitlines():
|
||||
stripped = line.strip()
|
||||
uri = (
|
||||
None
|
||||
if not stripped or stripped.startswith("#")
|
||||
else _uri_from_deb_line(stripped)
|
||||
)
|
||||
if uri is not None and any(
|
||||
fnmatch.fnmatch(uri, pattern) for pattern in patterns
|
||||
):
|
||||
lines.append(f"# {line}")
|
||||
else:
|
||||
lines.append(line)
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def _disable_deb822_content(content: str) -> str:
|
||||
disabled_stanzas = []
|
||||
def _uris_from_deb822_stanza(stanza: str) -> set[str]:
|
||||
uris: set[str] = set()
|
||||
for line in stanza.splitlines():
|
||||
if line.lower().startswith("uris:"):
|
||||
uris.update(line.split(":", 1)[1].split())
|
||||
return uris
|
||||
|
||||
|
||||
def _disable_deb822_content(content: str, patterns: Sequence[str]) -> str:
|
||||
stanzas = []
|
||||
for stanza in content.split("\n\n"):
|
||||
if not stanza.strip():
|
||||
continue
|
||||
lines = stanza.splitlines()
|
||||
new_lines = [line for line in lines if not line.lower().startswith("enabled:")]
|
||||
new_lines.append("Enabled: no")
|
||||
disabled_stanzas.append("\n".join(new_lines))
|
||||
return "\n\n".join(disabled_stanzas) + "\n"
|
||||
uris = _uris_from_deb822_stanza(stanza)
|
||||
if any(fnmatch.fnmatch(uri, pattern) for uri in uris for pattern in patterns):
|
||||
lines = [line for line in lines if not line.lower().startswith("enabled:")]
|
||||
lines.append("Enabled: no")
|
||||
stanzas.append("\n".join(lines))
|
||||
return "\n\n".join(stanzas) + "\n"
|
||||
|
||||
|
||||
def _uris_from_list_content(content: str) -> set[str]:
|
||||
"""Extract the URIs declared in a classic one-line `.list` file
|
||||
(`deb [options] uri suite [component...]`), skipping comments and any
|
||||
bracketed option list."""
|
||||
uris: set[str] = set()
|
||||
for line in content.splitlines():
|
||||
stripped = line.strip()
|
||||
if not stripped or stripped.startswith("#"):
|
||||
continue
|
||||
uri = _uri_from_deb_line(stripped)
|
||||
if uri is not None:
|
||||
uris.add(uri)
|
||||
return uris
|
||||
|
||||
|
||||
def _uris_from_deb822_content(content: str) -> set[str]:
|
||||
"""Extract the URIs declared in the `URIs:` field of each stanza of a
|
||||
DEB822 `.sources` file."""
|
||||
uris: set[str] = set()
|
||||
for stanza in content.split("\n\n"):
|
||||
uris.update(_uris_from_deb822_stanza(stanza))
|
||||
return uris
|
||||
|
||||
|
||||
def _existing_uris(context: Context) -> set[str]:
|
||||
"""Collect every URI declared by the apt sources already present on the
|
||||
source filesystem, whether in the top-level sources.list or under
|
||||
sources.list.d/, in either the classic or the DEB822 format."""
|
||||
uris: set[str] = set()
|
||||
existing_top_level = context.read_src_file(_SOURCES_LIST)
|
||||
if existing_top_level is not None:
|
||||
uris.update(_uris_from_list_content(existing_top_level.decode("utf-8")))
|
||||
for entry in context.iter_src_dir(_SOURCES_LIST_D):
|
||||
if entry.suffix == ".sources":
|
||||
uris.update(_uris_from_deb822_content(entry.read_text(encoding="utf-8")))
|
||||
elif entry.suffix == ".list":
|
||||
uris.update(_uris_from_list_content(entry.read_text(encoding="utf-8")))
|
||||
return uris
|
||||
|
||||
|
||||
@register
|
||||
@@ -58,43 +152,62 @@ class AptModule(Module):
|
||||
return None
|
||||
|
||||
def apply(self, context: Context) -> None:
|
||||
self._copy_existing_disabled(context)
|
||||
self._write_configured_keys(context)
|
||||
patterns = [
|
||||
source.uri_to_replace
|
||||
for source in context.config.configurations.apt.sources
|
||||
if source.uri_to_replace is not None
|
||||
]
|
||||
self._copy_existing_disabled(context, patterns)
|
||||
self._write_configured_sources(context)
|
||||
|
||||
@staticmethod
|
||||
def _copy_existing_disabled(context: Context) -> None:
|
||||
def _copy_existing_disabled(context: Context, patterns: Sequence[str]) -> None:
|
||||
existing_top_level = context.read_src_file(_SOURCES_LIST)
|
||||
if existing_top_level is not None:
|
||||
disabled = _disable_list_content(existing_top_level.decode("utf-8"))
|
||||
disabled = _disable_list_content(
|
||||
existing_top_level.decode("utf-8"), patterns
|
||||
)
|
||||
context.write_file(_SOURCES_LIST, disabled.encode("utf-8"))
|
||||
logger.info("disabled existing apt source list %s", _SOURCES_LIST)
|
||||
logger.info("copied existing apt source list %s", _SOURCES_LIST)
|
||||
|
||||
for entry in context.iter_src_dir(_SOURCES_LIST_D):
|
||||
if entry.suffix == ".sources":
|
||||
disabled = _disable_deb822_content(entry.read_text(encoding="utf-8"))
|
||||
disabled = _disable_deb822_content(
|
||||
entry.read_text(encoding="utf-8"), patterns
|
||||
)
|
||||
elif entry.suffix == ".list":
|
||||
disabled = _disable_list_content(entry.read_text(encoding="utf-8"))
|
||||
disabled = _disable_list_content(
|
||||
entry.read_text(encoding="utf-8"), patterns
|
||||
)
|
||||
else:
|
||||
continue
|
||||
context.write_file(
|
||||
f"{_SOURCES_LIST_D}/{entry.name}", disabled.encode("utf-8")
|
||||
)
|
||||
logger.info("disabled existing apt source %s", entry.name)
|
||||
|
||||
@staticmethod
|
||||
def _write_configured_keys(context: Context) -> None:
|
||||
for key in context.config.configurations.apt.keys:
|
||||
context.write_file(
|
||||
f"{_KEYRINGS_DIR}/{key.name}.asc", key.content.encode("utf-8")
|
||||
)
|
||||
logger.info("installed apt keyring %s", key.name)
|
||||
logger.info("copied existing apt source %s", entry.name)
|
||||
|
||||
@staticmethod
|
||||
def _write_configured_sources(context: Context) -> None:
|
||||
existing_uris: set[str] | None = None
|
||||
for source in context.config.configurations.apt.sources:
|
||||
if source.uri_to_replace is not None:
|
||||
if existing_uris is None:
|
||||
existing_uris = _existing_uris(context)
|
||||
if not any(
|
||||
fnmatch.fnmatch(uri, source.uri_to_replace) for uri in existing_uris
|
||||
):
|
||||
logger.info(
|
||||
"skipped apt source %s: uri-to-replace %s matched no "
|
||||
"existing uri",
|
||||
source.name,
|
||||
source.uri_to_replace,
|
||||
)
|
||||
continue
|
||||
context.write_file(
|
||||
f"{_SOURCES_LIST_D}/{source.name}.sources",
|
||||
source.to_deb822().encode("utf-8"),
|
||||
f"{_SOURCES_LIST_D}/{sanitize_filename(source.name)}.sources",
|
||||
source.to_deb822(
|
||||
signed_by_armored_key=source.signed_by,
|
||||
signed_by_path=source.signed_by_file,
|
||||
).encode("utf-8"),
|
||||
)
|
||||
logger.info("added apt source %s", source.name)
|
||||
|
||||
@@ -34,3 +34,18 @@ def register(module_cls: type[Module]) -> type[Module]:
|
||||
|
||||
def all_modules() -> list[type[Module]]:
|
||||
return list(_REGISTRY)
|
||||
|
||||
|
||||
def sanitize_filename(name: str) -> str:
|
||||
"""Make a user-supplied name safe to use as a single filesystem path
|
||||
component.
|
||||
|
||||
Modules that derive a file's name from configuration data (a
|
||||
certificate's name, a repository's name, ...) must run it through this
|
||||
first: without it, a name containing `/` (or `\\`, harmless on the
|
||||
target systems this tool writes to but stripped for good measure)
|
||||
would silently turn into extra, unintended path segments - creating
|
||||
nested directories a careless or malicious name shouldn't be able to
|
||||
create - instead of the single file the caller asked for.
|
||||
"""
|
||||
return name.replace("/", "_").replace("\\", "_")
|
||||
|
||||
@@ -49,7 +49,7 @@ import posixpath
|
||||
from pathlib import Path
|
||||
|
||||
from id_configurator.context import Context
|
||||
from id_configurator.modules.base import Module, register
|
||||
from id_configurator.modules.base import Module, register, sanitize_filename
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -142,7 +142,7 @@ class CertificatesModule(Module):
|
||||
|
||||
def apply(self, context: Context) -> None:
|
||||
configured_names = {
|
||||
certificate.name
|
||||
sanitize_filename(certificate.name)
|
||||
for certificate in context.config.configurations.certificates
|
||||
}
|
||||
for existing in context.iter_src_dir(_LOCAL_CERTS_DIR):
|
||||
@@ -152,7 +152,7 @@ class CertificatesModule(Module):
|
||||
)
|
||||
for certificate in context.config.configurations.certificates:
|
||||
context.write_file(
|
||||
f"{_LOCAL_CERTS_DIR}/{certificate.name}.crt",
|
||||
f"{_LOCAL_CERTS_DIR}/{sanitize_filename(certificate.name)}.crt",
|
||||
certificate.content.encode(),
|
||||
)
|
||||
logger.info("installed root certificate %s", certificate.name)
|
||||
@@ -237,7 +237,7 @@ class CertificatesModule(Module):
|
||||
)
|
||||
for certificate in context.config.configurations.certificates:
|
||||
context.write_file(
|
||||
f"{_RHEL_ANCHORS_DIR}/{certificate.name}.crt",
|
||||
f"{_RHEL_ANCHORS_DIR}/{sanitize_filename(certificate.name)}.crt",
|
||||
certificate.content.encode(),
|
||||
)
|
||||
|
||||
|
||||
@@ -15,6 +15,19 @@ separate, higher-precedence level in npm's own terminology. `$prefix`
|
||||
defaults to `/usr/local` for essentially every standard Node.js install
|
||||
(the official Docker image included); unlike a config dropped in one
|
||||
specific user's home, this applies regardless of which user invokes npm.
|
||||
|
||||
`cafile` only covers npm's own registry client, though - it does nothing for
|
||||
Node itself, e.g. a package's install script or the application's own code
|
||||
making an HTTPS request. Node has no config file for that: the only way to
|
||||
extend its trust store is the `NODE_EXTRA_CA_CERTS` environment variable,
|
||||
read once at process startup. So whenever any certificate is configured -
|
||||
regardless of whether any npm repository is - it is also set system-wide in
|
||||
`/etc/environment` (merged with whatever is already there), which every
|
||||
login-based process (an SSH session, `su -`, ...) picks up via PAM's
|
||||
`pam_env`. This does not reach a `docker build`/`RUN` step or a container's
|
||||
own `ENTRYPOINT`/`CMD`, which never go through a login shell: propagating it
|
||||
there is the resulting image's own `Dockerfile`'s job (a plain `ENV`
|
||||
instruction), outside what a tool that only writes files can do.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -29,6 +42,8 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
_NPMRC = "usr/local/etc/npmrc"
|
||||
_CA_BUNDLE = "/etc/ssl/certs/ca-certificates.crt"
|
||||
_ETC_ENVIRONMENT = "etc/environment"
|
||||
_NODE_EXTRA_CA_CERTS_VAR = "NODE_EXTRA_CA_CERTS"
|
||||
|
||||
|
||||
def _write_npmrc(context: Context, repositories: list[NpmRepository]) -> None:
|
||||
@@ -41,12 +56,29 @@ def _write_npmrc(context: Context, repositories: list[NpmRepository]) -> None:
|
||||
logger.info("configured npm to use %s", repositories[0].url)
|
||||
|
||||
|
||||
def _update_etc_environment(context: Context) -> None:
|
||||
existing = context.read_src_file(_ETC_ENVIRONMENT)
|
||||
lines = [] if existing is None else existing.decode("utf-8").splitlines()
|
||||
lines = [
|
||||
line for line in lines if not line.startswith(f"{_NODE_EXTRA_CA_CERTS_VAR}=")
|
||||
]
|
||||
lines.append(f"{_NODE_EXTRA_CA_CERTS_VAR}={_CA_BUNDLE}")
|
||||
context.write_file(_ETC_ENVIRONMENT, ("\n".join(lines) + "\n").encode("utf-8"))
|
||||
logger.info("configured %s in %s", _NODE_EXTRA_CA_CERTS_VAR, _ETC_ENVIRONMENT)
|
||||
|
||||
|
||||
@register
|
||||
class NpmModule(Module):
|
||||
name = "npm"
|
||||
|
||||
def is_relevant(self, context: Context) -> bool:
|
||||
return bool(context.config.configurations.npm.repositories)
|
||||
return bool(context.config.configurations.npm.repositories) or bool(
|
||||
context.config.configurations.certificates
|
||||
)
|
||||
|
||||
def apply(self, context: Context) -> None:
|
||||
_write_npmrc(context, context.config.configurations.npm.repositories)
|
||||
repositories = context.config.configurations.npm.repositories
|
||||
if repositories:
|
||||
_write_npmrc(context, repositories)
|
||||
if context.config.configurations.certificates:
|
||||
_update_etc_environment(context)
|
||||
|
||||
@@ -14,6 +14,17 @@ Composer's own HTTPS client (PHP's stream wrapper, generally backed by
|
||||
curl) already trusts whatever the `certificates` module installs into the
|
||||
system CA store, so no per-repository certificate configuration is needed
|
||||
here, unlike Poetry's.
|
||||
|
||||
Whenever any certificate is configured - regardless of whether any PHP
|
||||
repository is - `openssl.cafile` is also set, pointing at the same bundle,
|
||||
in a dedicated `conf.d` snippet: `usr/local/etc/php/conf.d/id-configurator-
|
||||
openssl.ini`, the directory the official PHP Docker images scan for extra
|
||||
`.ini` files (`docker-php-ext-enable`'s own mechanism), applied regardless
|
||||
of which SAPI (cli, fpm, apache) or php.ini is in use. This is normally
|
||||
inferred from OpenSSL's own compiled-in default CAfile (which the
|
||||
`certificates` module also rebuilds), but not every PHP build honors that
|
||||
default, so setting it explicitly makes every PHP script's own OpenSSL-based
|
||||
code (not just Composer's) reliably trust the configured certificates too.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -28,6 +39,8 @@ from id_configurator.modules.base import Module, register
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_COMPOSER_CONFIG = "root/.composer/config.json"
|
||||
_CA_BUNDLE = "/etc/ssl/certs/ca-certificates.crt"
|
||||
_OPENSSL_INI = "usr/local/etc/php/conf.d/id-configurator-openssl.ini"
|
||||
|
||||
|
||||
def _write_composer_config(context: Context, repositories: list[PhpRepository]) -> None:
|
||||
@@ -45,12 +58,23 @@ def _write_composer_config(context: Context, repositories: list[PhpRepository])
|
||||
)
|
||||
|
||||
|
||||
def _write_openssl_ini(context: Context) -> None:
|
||||
context.write_file(_OPENSSL_INI, f"openssl.cafile={_CA_BUNDLE}\n".encode())
|
||||
logger.info("configured openssl.cafile to use %s", _CA_BUNDLE)
|
||||
|
||||
|
||||
@register
|
||||
class PhpModule(Module):
|
||||
name = "php"
|
||||
|
||||
def is_relevant(self, context: Context) -> bool:
|
||||
return bool(context.config.configurations.php.repositories)
|
||||
return bool(context.config.configurations.php.repositories) or bool(
|
||||
context.config.configurations.certificates
|
||||
)
|
||||
|
||||
def apply(self, context: Context) -> None:
|
||||
_write_composer_config(context, context.config.configurations.php.repositories)
|
||||
repositories = context.config.configurations.php.repositories
|
||||
if repositories:
|
||||
_write_composer_config(context, repositories)
|
||||
if context.config.configurations.certificates:
|
||||
_write_openssl_ini(context)
|
||||
|
||||
@@ -4,10 +4,13 @@ DNF is a drop-in successor to YUM and reads the exact same `.repo` files
|
||||
under `/etc/yum.repos.d/`, so a single module covers both.
|
||||
|
||||
If `<src>/etc/yum.repos.d` exists, every `.repo` file already there is
|
||||
copied into `<dst>` with all of its sections disabled (`enabled=0`), before
|
||||
the repositories declared in the configuration are written as new, enabled
|
||||
`.repo` files (so a configured repository reusing an existing file name
|
||||
ends up enabled, taking precedence over the disabled copy). Each `.repo`
|
||||
copied into `<dst>`, before the repositories declared in the configuration
|
||||
are written as new, enabled `.repo` files (so a configured repository
|
||||
reusing an existing file name ends up enabled, taking precedence over the
|
||||
copied one). A section of an existing `.repo` file is only disabled
|
||||
(`enabled=0`) if its `baseurl`/`mirrorlist` matches the
|
||||
`index_url_to_replace` pattern of one of the configured repositories;
|
||||
sections matching no such pattern are copied through unchanged. Each `.repo`
|
||||
file is written referencing its `gpg-url` (used verbatim as `gpgkey=`,
|
||||
exactly as YUM/DNF expect: `https://`, or a `file://` path already present
|
||||
on the target system). Separately, each GPG key declared in
|
||||
@@ -17,20 +20,27 @@ conventional location such keys live at on RPM-based systems. `gpg` is only
|
||||
used here to validate the key; the actual trust decision is left to
|
||||
YUM/DNF, which import it from that file the first time the repository is
|
||||
used (as `rpm --import` would).
|
||||
|
||||
A repository's `index_url_to_replace`, if set, is an fnmatch pattern
|
||||
checked against the baseurl of every section of every existing `.repo`
|
||||
file: the repository is only written out if the pattern matches at least
|
||||
one of them, and skipped entirely otherwise.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import configparser
|
||||
import fnmatch
|
||||
import logging
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
from collections.abc import Sequence
|
||||
from io import StringIO
|
||||
|
||||
from id_configurator.config import NamedContent, YumRepository
|
||||
from id_configurator.context import Context
|
||||
from id_configurator.modules.base import Module, register
|
||||
from id_configurator.modules.base import Module, register, sanitize_filename
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -38,24 +48,46 @@ _REPOS_DIR = "etc/yum.repos.d"
|
||||
_RPM_GPG_DIR = "etc/pki/rpm-gpg"
|
||||
|
||||
|
||||
def _disable_repo_content(content: str) -> str:
|
||||
def _disable_repo_content(content: str, patterns: Sequence[str]) -> str:
|
||||
parser = configparser.ConfigParser(interpolation=None)
|
||||
parser.optionxform = str
|
||||
parser.read_string(content)
|
||||
for section in parser.sections():
|
||||
parser[section]["enabled"] = "0"
|
||||
urls = [parser[section].get(key) for key in ("baseurl", "mirrorlist")]
|
||||
urls = [url for url in urls if url]
|
||||
if any(fnmatch.fnmatch(url, pattern) for url in urls for pattern in patterns):
|
||||
parser[section]["enabled"] = "0"
|
||||
buffer = StringIO()
|
||||
parser.write(buffer, space_around_delimiters=False)
|
||||
return buffer.getvalue()
|
||||
|
||||
|
||||
def _disable_existing_repos(context: Context) -> None:
|
||||
def _existing_index_urls(context: Context) -> set[str]:
|
||||
"""Collect the baseurl of every section of every .repo file already
|
||||
present on the source filesystem."""
|
||||
urls: set[str] = set()
|
||||
for entry in context.iter_src_dir(_REPOS_DIR):
|
||||
if entry.suffix != ".repo":
|
||||
continue
|
||||
disabled = _disable_repo_content(entry.read_text(encoding="utf-8"))
|
||||
parser = configparser.ConfigParser(interpolation=None)
|
||||
parser.optionxform = str
|
||||
parser.read_string(entry.read_text(encoding="utf-8"))
|
||||
for section in parser.sections():
|
||||
# noinspection SpellCheckingInspection
|
||||
for key in ("baseurl", "mirrorlist"):
|
||||
url = parser[section].get(key)
|
||||
if url:
|
||||
urls.add(url)
|
||||
return urls
|
||||
|
||||
|
||||
def _disable_existing_repos(context: Context, patterns: Sequence[str]) -> None:
|
||||
for entry in context.iter_src_dir(_REPOS_DIR):
|
||||
if entry.suffix != ".repo":
|
||||
continue
|
||||
disabled = _disable_repo_content(entry.read_text(encoding="utf-8"), patterns)
|
||||
context.write_file(f"{_REPOS_DIR}/{entry.name}", disabled.encode("utf-8"))
|
||||
logger.info("disabled existing yum/dnf repository file %s", entry.name)
|
||||
logger.info("copied existing yum/dnf repository file %s", entry.name)
|
||||
|
||||
|
||||
def _write_repo_file(context: Context, repository: YumRepository) -> None:
|
||||
@@ -71,7 +103,7 @@ def _write_repo_file(context: Context, repository: YumRepository) -> None:
|
||||
else:
|
||||
lines.append("gpgcheck=0")
|
||||
context.write_file(
|
||||
f"{_REPOS_DIR}/{repository.name}.repo",
|
||||
f"{_REPOS_DIR}/{sanitize_filename(repository.name)}.repo",
|
||||
("\n".join(lines) + "\n").encode("utf-8"),
|
||||
)
|
||||
logger.info("added yum/dnf repository %s", repository.name)
|
||||
@@ -100,7 +132,8 @@ def _validate_gpg_key(name: str, content: bytes) -> None:
|
||||
def _import_key(context: Context, key: NamedContent) -> None:
|
||||
_validate_gpg_key(key.name, key.content.encode("utf-8"))
|
||||
context.write_file(
|
||||
f"{_RPM_GPG_DIR}/RPM-GPG-KEY-{key.name}", key.content.encode("utf-8")
|
||||
f"{_RPM_GPG_DIR}/RPM-GPG-KEY-{sanitize_filename(key.name)}",
|
||||
key.content.encode("utf-8"),
|
||||
)
|
||||
logger.info("imported yum/dnf GPG key %s", key.name)
|
||||
|
||||
@@ -119,8 +152,28 @@ class YumModule(Module):
|
||||
return bool(yum.repositories) or bool(yum.keys) or any(existing_repo_files)
|
||||
|
||||
def apply(self, context: Context) -> None:
|
||||
_disable_existing_repos(context)
|
||||
patterns = [
|
||||
repository.index_url_to_replace
|
||||
for repository in context.config.configurations.yum.repositories
|
||||
if repository.index_url_to_replace is not None
|
||||
]
|
||||
_disable_existing_repos(context, patterns)
|
||||
existing_index_urls: set[str] | None = None
|
||||
for repository in context.config.configurations.yum.repositories:
|
||||
if repository.index_url_to_replace is not None:
|
||||
if existing_index_urls is None:
|
||||
existing_index_urls: set[str] = _existing_index_urls(context)
|
||||
if not any(
|
||||
fnmatch.fnmatch(url, repository.index_url_to_replace)
|
||||
for url in existing_index_urls
|
||||
):
|
||||
logger.info(
|
||||
"skipped yum/dnf repository %s: index-url-to-replace %s "
|
||||
"matched no existing baseurl",
|
||||
repository.name,
|
||||
repository.index_url_to_replace,
|
||||
)
|
||||
continue
|
||||
_write_repo_file(context, repository)
|
||||
for key in context.config.configurations.yum.keys:
|
||||
_import_key(context, key)
|
||||
|
||||
+35
-20
@@ -123,7 +123,17 @@ options:
|
||||
description: DEB822 "Trusted" field.
|
||||
signed_by:
|
||||
type: str
|
||||
description: DEB822 "Signed-By" field.
|
||||
description:
|
||||
- ASCII-armored content of a GPG public key to trust this
|
||||
source with. Folded inline as a multi-line Signed-By field
|
||||
(no separate keyring file or gpg binary needed). Takes
|
||||
precedence over signed_by_file if both are set.
|
||||
signed_by_file:
|
||||
type: str
|
||||
description:
|
||||
- Path to a keyring already expected to exist on the target
|
||||
system, used verbatim as the rendered Signed-By field.
|
||||
Ignored if signed_by is also set.
|
||||
pdiffs:
|
||||
type: bool
|
||||
description: DEB822 "PDiffs" field.
|
||||
@@ -157,20 +167,13 @@ options:
|
||||
description:
|
||||
type: str
|
||||
description: DEB822 "Description" field.
|
||||
keys:
|
||||
type: list
|
||||
elements: dict
|
||||
default: []
|
||||
description: GPG keys trusted for the sources above (NamedContent).
|
||||
suboptions:
|
||||
name:
|
||||
uri_to_replace:
|
||||
type: str
|
||||
required: true
|
||||
description: A short, unique name for this entry.
|
||||
content:
|
||||
type: str
|
||||
required: true
|
||||
description: PEM-encoded/ASCII-armored key content.
|
||||
description:
|
||||
- fnmatch pattern checked against the URIs already present in
|
||||
the machine's existing apt configuration. If set, this
|
||||
source is only applied when the pattern matches at least
|
||||
one of them; otherwise it is skipped entirely.
|
||||
apk:
|
||||
type: dict
|
||||
default: {}
|
||||
@@ -189,6 +192,13 @@ options:
|
||||
key:
|
||||
type: str
|
||||
description: The signing key's name, if any (see NamedContent).
|
||||
url_to_replace:
|
||||
type: str
|
||||
description:
|
||||
- fnmatch pattern checked against the URLs already active in
|
||||
the machine's existing /etc/apk/repositories. If set, this
|
||||
repository is only applied when the pattern matches at
|
||||
least one of them; otherwise it is skipped entirely.
|
||||
yum:
|
||||
type: dict
|
||||
default: {}
|
||||
@@ -211,6 +221,13 @@ options:
|
||||
gpg_url:
|
||||
type: str
|
||||
description: The repository's gpgkey URL.
|
||||
index_url_to_replace:
|
||||
type: str
|
||||
description:
|
||||
- fnmatch pattern checked against the baseurl of every
|
||||
section of every existing .repo file. If set, this
|
||||
repository is only applied when the pattern matches at
|
||||
least one of them; otherwise it is skipped entirely.
|
||||
keys:
|
||||
type: list
|
||||
elements: dict
|
||||
@@ -483,6 +500,7 @@ ARGUMENT_SPEC = {
|
||||
"enabled": {"type": "bool", "default": True},
|
||||
"trusted": {"type": "bool"},
|
||||
"signed_by": {"type": "str"},
|
||||
"signed_by_file": {"type": "str"},
|
||||
"pdiffs": {"type": "bool"},
|
||||
"by_hash": {"type": "bool"},
|
||||
"allow_insecure": {"type": "bool"},
|
||||
@@ -494,14 +512,9 @@ ARGUMENT_SPEC = {
|
||||
"check_date": {"type": "bool"},
|
||||
"date_max_future": {"type": "str"},
|
||||
"description": {"type": "str"},
|
||||
"uri_to_replace": {"type": "str"},
|
||||
},
|
||||
},
|
||||
"keys": {
|
||||
"type": "list",
|
||||
"elements": "dict",
|
||||
"default": [],
|
||||
"options": dict(_NAMED_CONTENT_OPTIONS),
|
||||
},
|
||||
},
|
||||
},
|
||||
"apk": {
|
||||
@@ -515,6 +528,7 @@ ARGUMENT_SPEC = {
|
||||
"options": {
|
||||
"url": {"type": "str", "required": True},
|
||||
"key": {"type": "str"},
|
||||
"url_to_replace": {"type": "str"},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -531,6 +545,7 @@ ARGUMENT_SPEC = {
|
||||
"name": {"type": "str", "required": True},
|
||||
"index_url": {"type": "str", "required": True},
|
||||
"gpg_url": {"type": "str"},
|
||||
"index_url_to_replace": {"type": "str"},
|
||||
},
|
||||
},
|
||||
"keys": {
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ maintainers = [
|
||||
name = "id-configurator"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
version = "0.2.0"
|
||||
version = "2026.9.7"
|
||||
|
||||
[project.scripts]
|
||||
id-configurator = "id_configurator:main"
|
||||
|
||||
@@ -8,10 +8,11 @@ configurations:
|
||||
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
|
||||
apt:
|
||||
sources: []
|
||||
keys: []
|
||||
apk:
|
||||
repositories:
|
||||
- url: https://host.docker.internal:{{ 'APK_REPO_PORT' | env }}/
|
||||
- name: demo
|
||||
url: https://host.docker.internal:{{ 'APK_REPO_PORT' | env }}/
|
||||
url_to_replace: https://dl-cdn.alpinelinux.org/*
|
||||
key: |
|
||||
{{ 'dockerfiles/repos/040d0af5f97a0a44.rsa.pub' | file | indent(8, first=true) }}
|
||||
yum:
|
||||
|
||||
@@ -13,8 +13,9 @@ configurations:
|
||||
uris:
|
||||
- https://host.docker.internal:{{ 'APT_REPO_PORT' | env }}/
|
||||
suites: ["./"]
|
||||
trusted: true
|
||||
keys: []
|
||||
uri-to-replace: http://deb.debian.org/*
|
||||
signed-by: |
|
||||
{{ 'dockerfiles/repos/apt-signing-key.pub.asc' | file | indent(8, first=true) }}
|
||||
apk:
|
||||
repositories: []
|
||||
yum:
|
||||
|
||||
@@ -8,7 +8,6 @@ configurations:
|
||||
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
|
||||
apt:
|
||||
sources: []
|
||||
keys: []
|
||||
apk:
|
||||
repositories: []
|
||||
yum:
|
||||
|
||||
@@ -8,7 +8,6 @@ configurations:
|
||||
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
|
||||
apt:
|
||||
sources: []
|
||||
keys: []
|
||||
apk:
|
||||
repositories: []
|
||||
yum:
|
||||
|
||||
@@ -8,7 +8,6 @@ configurations:
|
||||
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
|
||||
apt:
|
||||
sources: []
|
||||
keys: []
|
||||
apk:
|
||||
repositories: []
|
||||
yum:
|
||||
|
||||
@@ -8,7 +8,6 @@ configurations:
|
||||
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
|
||||
apt:
|
||||
sources: []
|
||||
keys: []
|
||||
apk:
|
||||
repositories: []
|
||||
yum:
|
||||
|
||||
@@ -8,7 +8,6 @@ configurations:
|
||||
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
|
||||
apt:
|
||||
sources: []
|
||||
keys: []
|
||||
apk:
|
||||
repositories: []
|
||||
yum:
|
||||
|
||||
@@ -8,7 +8,6 @@ configurations:
|
||||
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
|
||||
apt:
|
||||
sources: []
|
||||
keys: []
|
||||
apk:
|
||||
repositories: []
|
||||
yum:
|
||||
|
||||
@@ -8,7 +8,6 @@ configurations:
|
||||
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
|
||||
apt:
|
||||
sources: []
|
||||
keys: []
|
||||
apk:
|
||||
repositories: []
|
||||
yum:
|
||||
|
||||
@@ -8,7 +8,6 @@ configurations:
|
||||
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
|
||||
apt:
|
||||
sources: []
|
||||
keys: []
|
||||
apk:
|
||||
repositories: []
|
||||
yum:
|
||||
|
||||
@@ -8,7 +8,6 @@ configurations:
|
||||
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
|
||||
apt:
|
||||
sources: []
|
||||
keys: []
|
||||
apk:
|
||||
repositories: []
|
||||
yum:
|
||||
|
||||
@@ -8,13 +8,13 @@ configurations:
|
||||
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
|
||||
apt:
|
||||
sources: []
|
||||
keys: []
|
||||
apk:
|
||||
repositories: []
|
||||
yum:
|
||||
repositories:
|
||||
- name: demo
|
||||
index-url: https://host.docker.internal:{{ 'YUM_REPO_PORT' | env }}/
|
||||
index-url-to-replace: https://mirrors.rockylinux.org/*
|
||||
keys: []
|
||||
python:
|
||||
repositories: []
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
# Minimal flat-format APT repository serving one hand-built .deb package over
|
||||
# TLS, used by tox-docker to exercise the `apt` module integration test
|
||||
# (tests/configs/Apt.yaml.tmpl, tests/dockerfiles/Apt.Dockerfile).
|
||||
# (tests/configs/Apt.yaml.j2, tests/dockerfiles/Apt.Dockerfile). Its
|
||||
# InRelease file is genuinely signed with the dedicated key pair
|
||||
# apt-signing-key.key.asc / apt-signing-key.pub.asc, whose public half is
|
||||
# what tests/configs/Apt.yaml.j2 embeds as the configured source's
|
||||
# `signed-by`: the `apt` module dearmors it into a keyring, and APT itself
|
||||
# verifies the InRelease signature against that keyring on `apt-get update`
|
||||
# - no `Trusted: yes` bypass involved.
|
||||
#
|
||||
# Built by tox-docker, whose build context is this directory (not the repo
|
||||
# root) - every COPY source below must live under tests/dockerfiles/repos/.
|
||||
FROM debian:trixie-slim AS packagebuild
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends dpkg-dev \
|
||||
&& apt-get install -y --no-install-recommends dpkg-dev apt-utils gnupg \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
RUN set -eux; \
|
||||
pkg=idconfiguratortestpkg; \
|
||||
@@ -18,6 +24,13 @@ RUN set -eux; \
|
||||
WORKDIR /srv
|
||||
RUN dpkg-scanpackages --multiversion . > Packages \
|
||||
&& gzip -9c Packages > Packages.gz
|
||||
COPY apt-signing-key.key.asc /tmp/apt-signing-key.key.asc
|
||||
RUN gnupg_home="$(mktemp -d)" \
|
||||
&& gpg --homedir "$gnupg_home" --batch --import /tmp/apt-signing-key.key.asc \
|
||||
&& key_id="$(gpg --homedir "$gnupg_home" --list-secret-keys --with-colons | awk -F: '/^sec/{print $5; exit}')" \
|
||||
&& apt-ftparchive release . > Release \
|
||||
&& gpg --homedir "$gnupg_home" --batch --yes --pinentry-mode loopback --default-key "$key_id" --clearsign -o InRelease Release \
|
||||
&& rm -rf "$gnupg_home" /tmp/apt-signing-key.key.asc
|
||||
|
||||
FROM nginx:alpine
|
||||
COPY --from=packagebuild /srv /srv
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||
|
||||
lQOYBGqg6rMBCADaMUd27S41+kInPXuRZ0aL41aMZjqr+mw4W34w8i/JRYMJ6eGb
|
||||
sXf40AaFfS7zkbUpQZpmh3aYPF4jUAdWV03ZNNYdcGerIAKc2xsugSKjJIm1XPQq
|
||||
upz914b4/hp/UslDUH7SPZ+3Tq17TXW+xP6aUM9VcWMnlJT/DFgxh5sZB3nfDeYr
|
||||
yBV+PmEVfuQaUSIwKAh3Hv0ZaBHS8KzsawLzbJ+Pa9W9wjVME1izi9ctloGSxtLc
|
||||
p1suS2IS33QB0jGgj4VbuMcjo2iCpzW3Xd7jQ7WpXGjrl0bHnJTnWWB0ihDD3cJe
|
||||
tJlk9GScMQu3Z3th67DmqTXQru3btrDjxMifABEBAAEAB/sG3JffwJO8dNNRwpg5
|
||||
OWM5kPuuzKQ5tlVi7FWqmmMwVDRLwoQqzkj0lS4vUoGAfWuSEZMWeFodbYCUYbbp
|
||||
WzOCiivVnyxTcF1Eq4tOShdZxBNklbjEAzxeq0F+YRdaO7NCn6xtVizIqfvAre3v
|
||||
Aujy12m9ySQSIZUR9ERtwlL5VXj4yzcXciyM9aNbh2D4BFAoo+fBL1B5mhpKQ16B
|
||||
BafkpOf0myWZrwLdcn1t7BQ6UcLqsPJj6COCXXRDHoZvim38UBMMajNP4+8eC2Vk
|
||||
DDR6iFz53FRvIREQWkoEEW4P80v1lgcnP1apJGhkrJYCPM7ObZk0MoQkBiBdlPPQ
|
||||
VLiRBADhNsKjNd7v3Af5XGPJGWWZxCMxPlgwc6EMjCX3SQDql042eSIqwn1FFP6H
|
||||
Hi8lyq/XWz0JcUwcrpFxpIC3OLUsBleX3Z4E17Z6AotSV0FC6y23cqgtweMeLA0L
|
||||
tayMRmXJ7Aan+xfvi8MbW6lYGzM2uYHl27rVBuCt0kvxn+U4lwQA+ATO8KaBreCL
|
||||
E/vXGZaPWJs2GoiDBavKwqdYtlXDKlVP5XvBgaPfY6zKLtCsC6N+DKkVo42Stwu3
|
||||
oB6HSSLvXvFIhvUReh5PLpijmOroAJzvLQN9qmZBvF4IOUgaHmlYlL0DGzlxcwLr
|
||||
SdzqPaGRa+EgraFDDGGu5oQ5A2wNKTkEAMDS2w5SibOfsvnfK4aDc0mi8/fftzCO
|
||||
SwsvVzR0SmbXtaCPBnBjlA7YJhRZ6n/7nDpswrWPk8g125A0vK1cNRGM0ufKYW1f
|
||||
DMbB/aYrJjAKz8zWH6cgOD4fqM2gA9JounkIcJvWOToGume8pXhmem94u2FP8aik
|
||||
A6hL/Os+WRQcMNi0NWlkLWNvbmZpZ3VyYXRvci10ZXN0cyA8dGVzdHNAaWQtY29u
|
||||
ZmlndXJhdG9yLmludmFsaWQ+iQFuBBMBCABYFiEEaZF4ALsB1xUU3IP5rl3Himib
|
||||
1RgFAmqg6rMbFIAAAAAABAAObWFudTIsMi41KzEuMTIsMCwzAxsvBAULCQgHAgIi
|
||||
AgYVCgkICwIEFgIDAQIeBwIXgAAKCRCuXceKaJvVGFRrB/wIbG1HHQANDCHgyl4T
|
||||
RTPuKBX2c9lmJi2plp+T5L5sXyTMbAMLfTCkJWL/zHLM3N2v6GZuzbwZM8Dzj9zL
|
||||
LMpC6newmZe4YX+ZtyZGufBI963kh8PvlSWWUSyH+oYoIhlyJqj7tCQXoijEvQnS
|
||||
ReKGHUTvQGvQK2Ybp735NmAiDOM3/3KKh2Dqas/Tc2mxec4whr8RyUidsF+Yp6wT
|
||||
esfMxeax6DmWIcSLDWpUBndeahZ2Ff0c96BzbpF1v2hbvyI/IaP5snb4Nm1xpREm
|
||||
+y8iDAQhOklfXCyXrWgN3sIrk3tDndGrn+nw0iusnaBVki0keqjlH0camoUP28tu
|
||||
3sxG
|
||||
=TrE4
|
||||
-----END PGP PRIVATE KEY BLOCK-----
|
||||
@@ -0,0 +1,19 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQENBGqg6rMBCADaMUd27S41+kInPXuRZ0aL41aMZjqr+mw4W34w8i/JRYMJ6eGb
|
||||
sXf40AaFfS7zkbUpQZpmh3aYPF4jUAdWV03ZNNYdcGerIAKc2xsugSKjJIm1XPQq
|
||||
upz914b4/hp/UslDUH7SPZ+3Tq17TXW+xP6aUM9VcWMnlJT/DFgxh5sZB3nfDeYr
|
||||
yBV+PmEVfuQaUSIwKAh3Hv0ZaBHS8KzsawLzbJ+Pa9W9wjVME1izi9ctloGSxtLc
|
||||
p1suS2IS33QB0jGgj4VbuMcjo2iCpzW3Xd7jQ7WpXGjrl0bHnJTnWWB0ihDD3cJe
|
||||
tJlk9GScMQu3Z3th67DmqTXQru3btrDjxMifABEBAAG0NWlkLWNvbmZpZ3VyYXRv
|
||||
ci10ZXN0cyA8dGVzdHNAaWQtY29uZmlndXJhdG9yLmludmFsaWQ+iQFuBBMBCABY
|
||||
FiEEaZF4ALsB1xUU3IP5rl3Himib1RgFAmqg6rMbFIAAAAAABAAObWFudTIsMi41
|
||||
KzEuMTIsMCwzAxsvBAULCQgHAgIiAgYVCgkICwIEFgIDAQIeBwIXgAAKCRCuXceK
|
||||
aJvVGFRrB/wIbG1HHQANDCHgyl4TRTPuKBX2c9lmJi2plp+T5L5sXyTMbAMLfTCk
|
||||
JWL/zHLM3N2v6GZuzbwZM8Dzj9zLLMpC6newmZe4YX+ZtyZGufBI963kh8PvlSWW
|
||||
USyH+oYoIhlyJqj7tCQXoijEvQnSReKGHUTvQGvQK2Ybp735NmAiDOM3/3KKh2Dq
|
||||
as/Tc2mxec4whr8RyUidsF+Yp6wTesfMxeax6DmWIcSLDWpUBndeahZ2Ff0c96Bz
|
||||
bpF1v2hbvyI/IaP5snb4Nm1xpREm+y8iDAQhOklfXCyXrWgN3sIrk3tDndGrn+nw
|
||||
0iusnaBVki0keqjlH0camoUP28tu3sxG
|
||||
=qZrp
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
+138
-4
@@ -14,10 +14,11 @@ CONFIG_DATA = {
|
||||
"apk": {
|
||||
"repositories": [
|
||||
{
|
||||
"name": "main",
|
||||
"url": "https://example.invalid/v3.19/main",
|
||||
"key": KEY_BYTES,
|
||||
},
|
||||
{"url": "https://example.invalid/v3.19/community"},
|
||||
{"name": "community", "url": "https://example.invalid/v3.19/community"},
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -47,7 +48,9 @@ def test_is_relevant_true_with_apk_dir(tmp_path) -> None:
|
||||
assert ApkModule().is_relevant(context) is True
|
||||
|
||||
|
||||
def test_apply_disables_existing_and_adds_configured_repositories(tmp_path) -> None:
|
||||
def test_apply_copies_existing_repositories_unchanged_without_url_to_replace(
|
||||
tmp_path,
|
||||
) -> None:
|
||||
src = tmp_path / "src"
|
||||
apk_dir = src / "etc/apk"
|
||||
apk_dir.mkdir(parents=True)
|
||||
@@ -61,11 +64,13 @@ def test_apply_disables_existing_and_adds_configured_repositories(tmp_path) -> N
|
||||
|
||||
ApkModule().apply(context)
|
||||
|
||||
# No configured repository declares a url_to_replace, so the existing
|
||||
# lines are copied through unchanged, still active.
|
||||
content = (dst / "etc/apk/repositories").read_text(encoding="utf-8")
|
||||
assert content == (
|
||||
"# comment\n"
|
||||
"# https://old-mirror.invalid/v3.18/main\n"
|
||||
"# https://old-mirror.invalid/v3.18/community\n"
|
||||
"https://old-mirror.invalid/v3.18/main\n"
|
||||
"https://old-mirror.invalid/v3.18/community\n"
|
||||
"https://example.invalid/v3.19/main\n"
|
||||
"https://example.invalid/v3.19/community\n"
|
||||
)
|
||||
@@ -74,6 +79,42 @@ def test_apply_disables_existing_and_adds_configured_repositories(tmp_path) -> N
|
||||
assert key_content == KEY_BYTES
|
||||
|
||||
|
||||
def test_apply_disables_only_existing_lines_matching_url_to_replace(tmp_path) -> None:
|
||||
src = tmp_path / "src"
|
||||
apk_dir = src / "etc/apk"
|
||||
apk_dir.mkdir(parents=True)
|
||||
(apk_dir / "repositories").write_text(
|
||||
"https://old-mirror.invalid/v3.18/main\nhttps://old-mirror.invalid/v3.18/community\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
data = {
|
||||
"uuid": "12345678-1234-5678-1234-567812345678",
|
||||
"organisation": "InterDiode",
|
||||
"configurations": {
|
||||
"apk": {
|
||||
"repositories": [
|
||||
{
|
||||
"name": "main",
|
||||
"url": "https://example.invalid/v3.19/main",
|
||||
"url_to_replace": "https://old-mirror.invalid/v3.18/main",
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(config=Configuration.model_validate(data), dst=dst, src=src)
|
||||
|
||||
ApkModule().apply(context)
|
||||
|
||||
content = (dst / "etc/apk/repositories").read_text(encoding="utf-8")
|
||||
assert content == (
|
||||
"# https://old-mirror.invalid/v3.18/main\n"
|
||||
"https://old-mirror.invalid/v3.18/community\n"
|
||||
"https://example.invalid/v3.19/main\n"
|
||||
)
|
||||
|
||||
|
||||
def test_apply_without_existing_repositories_file(tmp_path) -> None:
|
||||
src = tmp_path / "src"
|
||||
(src / "etc/apk").mkdir(parents=True)
|
||||
@@ -89,6 +130,99 @@ def test_apply_without_existing_repositories_file(tmp_path) -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_apply_skips_repository_when_url_to_replace_matches_nothing(tmp_path) -> None:
|
||||
src = tmp_path / "src"
|
||||
apk_dir = src / "etc/apk"
|
||||
apk_dir.mkdir(parents=True)
|
||||
(apk_dir / "repositories").write_text(
|
||||
"https://old-mirror.invalid/v3.18/main\n", encoding="utf-8"
|
||||
)
|
||||
data = {
|
||||
"uuid": "12345678-1234-5678-1234-567812345678",
|
||||
"organisation": "InterDiode",
|
||||
"configurations": {
|
||||
"apk": {
|
||||
"repositories": [
|
||||
{
|
||||
"name": "main",
|
||||
"url": "https://example.invalid/v3.19/main",
|
||||
"url_to_replace": "https://other.invalid/*",
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(config=Configuration.model_validate(data), dst=dst, src=src)
|
||||
|
||||
ApkModule().apply(context)
|
||||
|
||||
content = (dst / "etc/apk/repositories").read_text(encoding="utf-8")
|
||||
assert "example.invalid" not in content
|
||||
|
||||
|
||||
def test_apply_writes_repository_when_url_to_replace_matches_existing(tmp_path) -> None:
|
||||
src = tmp_path / "src"
|
||||
apk_dir = src / "etc/apk"
|
||||
apk_dir.mkdir(parents=True)
|
||||
(apk_dir / "repositories").write_text(
|
||||
"https://old-mirror.invalid/v3.18/main\n", encoding="utf-8"
|
||||
)
|
||||
data = {
|
||||
"uuid": "12345678-1234-5678-1234-567812345678",
|
||||
"organisation": "InterDiode",
|
||||
"configurations": {
|
||||
"apk": {
|
||||
"repositories": [
|
||||
{
|
||||
"name": "main",
|
||||
"url": "https://example.invalid/v3.19/main",
|
||||
"url_to_replace": "https://old-mirror.invalid/*",
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(config=Configuration.model_validate(data), dst=dst, src=src)
|
||||
|
||||
ApkModule().apply(context)
|
||||
|
||||
content = (dst / "etc/apk/repositories").read_text(encoding="utf-8")
|
||||
assert "https://example.invalid/v3.19/main" in content
|
||||
|
||||
|
||||
def test_apply_reuses_cached_existing_urls_and_handles_missing_file(tmp_path) -> None:
|
||||
src = tmp_path / "src"
|
||||
(src / "etc/apk").mkdir(parents=True)
|
||||
data = {
|
||||
"uuid": "12345678-1234-5678-1234-567812345678",
|
||||
"organisation": "InterDiode",
|
||||
"configurations": {
|
||||
"apk": {
|
||||
"repositories": [
|
||||
{
|
||||
"name": "main",
|
||||
"url": "https://example.invalid/v3.19/main",
|
||||
"url_to_replace": "https://old.invalid/*",
|
||||
},
|
||||
{
|
||||
"name": "community",
|
||||
"url": "https://example.invalid/v3.19/community",
|
||||
"url_to_replace": "https://old.invalid/*",
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(config=Configuration.model_validate(data), dst=dst, src=src)
|
||||
|
||||
ApkModule().apply(context)
|
||||
|
||||
assert not (dst / "etc/apk/repositories").exists()
|
||||
|
||||
|
||||
def test_apply_writes_nothing_when_no_repositories_at_all(tmp_path) -> None:
|
||||
empty_config = Configuration.model_validate(
|
||||
{"uuid": "12345678-1234-5678-1234-567812345678", "organisation": "InterDiode"}
|
||||
|
||||
+267
-23
@@ -2,6 +2,13 @@ from id_configurator.config import Configuration
|
||||
from id_configurator.context import Context
|
||||
from id_configurator.modules.apt import AptModule
|
||||
|
||||
KEY_CONTENT = (
|
||||
"-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
|
||||
"\n"
|
||||
"mQENBGqh08IBCAC0\n"
|
||||
"-----END PGP PUBLIC KEY BLOCK-----\n"
|
||||
)
|
||||
|
||||
SOURCE_CONTENT = {
|
||||
"uuid": "12345678-1234-5678-1234-567812345678",
|
||||
"organisation": "InterDiode",
|
||||
@@ -12,13 +19,7 @@ SOURCE_CONTENT = {
|
||||
"name": "interdiode",
|
||||
"uris": ["http://example.invalid/debian"],
|
||||
"suites": ["stable"],
|
||||
"signed-by": "/etc/apt/keyrings/interdiode.asc",
|
||||
}
|
||||
],
|
||||
"keys": [
|
||||
{
|
||||
"name": "interdiode",
|
||||
"content": "fake-key-bytes",
|
||||
"signed-by": KEY_CONTENT,
|
||||
}
|
||||
],
|
||||
}
|
||||
@@ -30,6 +31,26 @@ def make_config() -> Configuration:
|
||||
return Configuration.model_validate(SOURCE_CONTENT)
|
||||
|
||||
|
||||
def make_config_with(source_extra: dict, name: str = "interdiode") -> Configuration:
|
||||
data = {
|
||||
"uuid": "12345678-1234-5678-1234-567812345678",
|
||||
"organisation": "InterDiode",
|
||||
"configurations": {
|
||||
"apt": {
|
||||
"sources": [
|
||||
{
|
||||
"name": name,
|
||||
"uris": ["http://example.invalid/debian"],
|
||||
"suites": ["stable"],
|
||||
**source_extra,
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
}
|
||||
return Configuration.model_validate(data)
|
||||
|
||||
|
||||
def test_is_relevant_false_without_src(tmp_path) -> None:
|
||||
context = Context(config=make_config(), dst=tmp_path / "dst", src=None)
|
||||
assert AptModule().is_relevant(context) is False
|
||||
@@ -49,7 +70,9 @@ def test_is_relevant_true_with_apt_dir(tmp_path) -> None:
|
||||
assert AptModule().is_relevant(context) is True
|
||||
|
||||
|
||||
def test_apply_disables_existing_sources_and_adds_configured_ones(tmp_path) -> None:
|
||||
def test_apply_copies_existing_sources_unchanged_without_uri_to_replace(
|
||||
tmp_path,
|
||||
) -> None:
|
||||
src = tmp_path / "src"
|
||||
apt_dir = src / "etc/apt"
|
||||
sources_list_d = apt_dir / "sources.list.d"
|
||||
@@ -76,37 +99,88 @@ def test_apply_disables_existing_sources_and_adds_configured_ones(tmp_path) -> N
|
||||
|
||||
AptModule().apply(context)
|
||||
|
||||
disabled_top_level = (dst / "etc/apt/sources.list").read_text(encoding="utf-8")
|
||||
# No configured source declares a uri_to_replace, so none of the existing
|
||||
# entries are disabled: they are copied through unchanged.
|
||||
top_level = (dst / "etc/apt/sources.list").read_text(encoding="utf-8")
|
||||
assert (
|
||||
disabled_top_level
|
||||
== "# already disabled\n# deb http://example.invalid/legacy stable main\n\n"
|
||||
top_level
|
||||
== "# already disabled\ndeb http://example.invalid/legacy stable main\n\n"
|
||||
)
|
||||
|
||||
disabled_list = (dst / "etc/apt/sources.list.d/extra.list").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert disabled_list == "# deb http://example.invalid/extra stable main\n"
|
||||
extra_list = (dst / "etc/apt/sources.list.d/extra.list").read_text(encoding="utf-8")
|
||||
assert extra_list == "deb http://example.invalid/extra stable main\n"
|
||||
|
||||
disabled_sources = (dst / "etc/apt/sources.list.d/docker.sources").read_text(
|
||||
docker_sources = (dst / "etc/apt/sources.list.d/docker.sources").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert disabled_sources == (
|
||||
"Types: deb\nURIs: http://example.invalid/docker\nSuites: stable\nEnabled: no"
|
||||
assert docker_sources == (
|
||||
"Types: deb\nURIs: http://example.invalid/docker\nSuites: stable\nEnabled: yes"
|
||||
"\n\n"
|
||||
"Types: deb\nURIs: http://example.invalid/docker2\nSuites: stable\nEnabled: no\n"
|
||||
"Types: deb\nURIs: http://example.invalid/docker2\nSuites: stable\n"
|
||||
)
|
||||
|
||||
assert not (dst / "etc/apt/sources.list.d/ignored.txt").exists()
|
||||
|
||||
keyring = (dst / "etc/apt/keyrings/interdiode.asc").read_bytes()
|
||||
assert keyring == b"fake-key-bytes"
|
||||
|
||||
configured_source = (dst / "etc/apt/sources.list.d/interdiode.sources").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert configured_source == (
|
||||
"Types: deb\nURIs: http://example.invalid/debian\nSuites: stable\n"
|
||||
"Signed-By: /etc/apt/keyrings/interdiode.asc\nEnabled: yes\n"
|
||||
"Signed-By:\n"
|
||||
" -----BEGIN PGP PUBLIC KEY BLOCK-----\n"
|
||||
" .\n"
|
||||
" mQENBGqh08IBCAC0\n"
|
||||
" -----END PGP PUBLIC KEY BLOCK-----\n"
|
||||
"Enabled: yes\n"
|
||||
)
|
||||
|
||||
|
||||
def test_apply_disables_only_existing_sources_matching_uri_to_replace(
|
||||
tmp_path,
|
||||
) -> None:
|
||||
src = tmp_path / "src"
|
||||
apt_dir = src / "etc/apt"
|
||||
sources_list_d = apt_dir / "sources.list.d"
|
||||
sources_list_d.mkdir(parents=True)
|
||||
|
||||
(apt_dir / "sources.list").write_text(
|
||||
"deb http://example.invalid/legacy stable main\n", encoding="utf-8"
|
||||
)
|
||||
(sources_list_d / "extra.list").write_text(
|
||||
"deb http://example.invalid/extra stable main\n", encoding="utf-8"
|
||||
)
|
||||
(sources_list_d / "docker.sources").write_text(
|
||||
"Types: deb\nURIs: http://example.invalid/extra\nSuites: stable\nEnabled: yes\n"
|
||||
"\n"
|
||||
"Types: deb\nURIs: http://example.invalid/docker2\nSuites: stable\n"
|
||||
"\n\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(
|
||||
config=make_config_with({"uri-to-replace": "http://example.invalid/extra"}),
|
||||
dst=dst,
|
||||
src=src,
|
||||
)
|
||||
|
||||
AptModule().apply(context)
|
||||
|
||||
# The legacy source doesn't match the pattern, so it stays enabled...
|
||||
top_level = (dst / "etc/apt/sources.list").read_text(encoding="utf-8")
|
||||
assert top_level == "deb http://example.invalid/legacy stable main\n"
|
||||
|
||||
# ...but every entry whose URI matches is disabled, regardless of format.
|
||||
extra_list = (dst / "etc/apt/sources.list.d/extra.list").read_text(encoding="utf-8")
|
||||
assert extra_list == "# deb http://example.invalid/extra stable main\n"
|
||||
|
||||
docker_sources = (dst / "etc/apt/sources.list.d/docker.sources").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert docker_sources == (
|
||||
"Types: deb\nURIs: http://example.invalid/extra\nSuites: stable\nEnabled: no"
|
||||
"\n\n"
|
||||
"Types: deb\nURIs: http://example.invalid/docker2\nSuites: stable\n"
|
||||
)
|
||||
|
||||
|
||||
@@ -120,3 +194,173 @@ def test_apply_without_existing_top_level_sources_list(tmp_path) -> None:
|
||||
|
||||
assert not (dst / "etc/apt/sources.list").exists()
|
||||
assert (dst / "etc/apt/sources.list.d/interdiode.sources").exists()
|
||||
|
||||
|
||||
def test_apply_uses_signed_by_file_when_no_key_content(tmp_path) -> None:
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(
|
||||
config=make_config_with({"signed-by-file": "/etc/apt/keyrings/existing.gpg"}),
|
||||
dst=dst,
|
||||
src=None,
|
||||
)
|
||||
|
||||
AptModule().apply(context)
|
||||
|
||||
configured_source = (dst / "etc/apt/sources.list.d/interdiode.sources").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert configured_source == (
|
||||
"Types: deb\nURIs: http://example.invalid/debian\nSuites: stable\n"
|
||||
"Signed-By: /etc/apt/keyrings/existing.gpg\nEnabled: yes\n"
|
||||
)
|
||||
|
||||
|
||||
def test_apply_signed_by_takes_precedence_over_signed_by_file(tmp_path) -> None:
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(
|
||||
config=make_config_with(
|
||||
{
|
||||
"signed-by": KEY_CONTENT,
|
||||
"signed-by-file": "/etc/apt/keyrings/existing.gpg",
|
||||
}
|
||||
),
|
||||
dst=dst,
|
||||
src=None,
|
||||
)
|
||||
|
||||
AptModule().apply(context)
|
||||
|
||||
configured_source = (dst / "etc/apt/sources.list.d/interdiode.sources").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert "Signed-By:\n -----BEGIN PGP PUBLIC KEY BLOCK-----" in configured_source
|
||||
assert "existing.gpg" not in configured_source
|
||||
|
||||
|
||||
def test_apply_without_signed_by_writes_no_field(tmp_path) -> None:
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(config=make_config_with({}), dst=dst, src=None)
|
||||
|
||||
AptModule().apply(context)
|
||||
|
||||
configured_source = (dst / "etc/apt/sources.list.d/interdiode.sources").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert configured_source == (
|
||||
"Types: deb\nURIs: http://example.invalid/debian\nSuites: stable\nEnabled: yes\n"
|
||||
)
|
||||
|
||||
|
||||
def test_apply_skips_source_when_uri_to_replace_matches_nothing(tmp_path) -> None:
|
||||
src = tmp_path / "src"
|
||||
apt_dir = src / "etc/apt"
|
||||
apt_dir.mkdir(parents=True)
|
||||
(apt_dir / "sources.list").write_text(
|
||||
"deb http://example.invalid/legacy stable main\n", encoding="utf-8"
|
||||
)
|
||||
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(
|
||||
config=make_config_with({"uri-to-replace": "http://other.invalid/*"}),
|
||||
dst=dst,
|
||||
src=src,
|
||||
)
|
||||
|
||||
AptModule().apply(context)
|
||||
|
||||
assert not (dst / "etc/apt/sources.list.d/interdiode.sources").exists()
|
||||
|
||||
|
||||
def test_apply_writes_source_when_uri_to_replace_matches_existing_uri(
|
||||
tmp_path,
|
||||
) -> None:
|
||||
src = tmp_path / "src"
|
||||
apt_dir = src / "etc/apt"
|
||||
apt_dir.mkdir(parents=True)
|
||||
(apt_dir / "sources.list").write_text(
|
||||
"deb http://legacy.invalid/debian stable main\n", encoding="utf-8"
|
||||
)
|
||||
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(
|
||||
config=make_config_with({"uri-to-replace": "http://legacy.invalid/*"}),
|
||||
dst=dst,
|
||||
src=src,
|
||||
)
|
||||
|
||||
AptModule().apply(context)
|
||||
|
||||
assert (dst / "etc/apt/sources.list.d/interdiode.sources").exists()
|
||||
|
||||
|
||||
def test_apply_skips_source_when_uri_to_replace_set_without_src(tmp_path) -> None:
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(
|
||||
config=make_config_with({"uri-to-replace": "http://*"}), dst=dst, src=None
|
||||
)
|
||||
|
||||
AptModule().apply(context)
|
||||
|
||||
assert not (dst / "etc/apt/sources.list.d/interdiode.sources").exists()
|
||||
|
||||
|
||||
def test_apply_matches_uri_to_replace_across_list_line_variants(tmp_path) -> None:
|
||||
src = tmp_path / "src"
|
||||
apt_dir = src / "etc/apt"
|
||||
sources_list_d = apt_dir / "sources.list.d"
|
||||
sources_list_d.mkdir(parents=True)
|
||||
|
||||
(apt_dir / "sources.list").write_text(
|
||||
"# a comment\n"
|
||||
"\n"
|
||||
"garbage line here\n"
|
||||
"deb [unterminated bracket with no closing marker\n"
|
||||
"deb [arch=amd64 trusted=yes] http://example.invalid/extra stable main\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(sources_list_d / "ignored.txt").write_text("not a source file\n", encoding="utf-8")
|
||||
|
||||
data = {
|
||||
"uuid": "12345678-1234-5678-1234-567812345678",
|
||||
"organisation": "InterDiode",
|
||||
"configurations": {
|
||||
"apt": {
|
||||
"sources": [
|
||||
{
|
||||
"name": "first",
|
||||
"uris": ["http://example.invalid/first"],
|
||||
"suites": ["stable"],
|
||||
"uri-to-replace": "http://example.invalid/extra",
|
||||
},
|
||||
{
|
||||
"name": "second",
|
||||
"uris": ["http://example.invalid/second"],
|
||||
"suites": ["stable"],
|
||||
"uri-to-replace": "http://example.invalid/extra",
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
}
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(config=Configuration.model_validate(data), dst=dst, src=src)
|
||||
|
||||
AptModule().apply(context)
|
||||
|
||||
assert (dst / "etc/apt/sources.list.d/first.sources").exists()
|
||||
assert (dst / "etc/apt/sources.list.d/second.sources").exists()
|
||||
|
||||
|
||||
def test_apply_sanitizes_source_name_with_slash(tmp_path) -> None:
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(
|
||||
config=make_config_with({"signed-by": KEY_CONTENT}, name="../../evil"),
|
||||
dst=dst,
|
||||
src=None,
|
||||
)
|
||||
|
||||
AptModule().apply(context)
|
||||
|
||||
source_file = dst / "etc/apt/sources.list.d/.._.._evil.sources"
|
||||
assert source_file.exists()
|
||||
assert not (dst / "etc/apt/sources.list.d/evil.sources").exists()
|
||||
|
||||
@@ -54,6 +54,27 @@ def test_apply_writes_certificate_and_rebuilds_bundle(tmp_path) -> None:
|
||||
assert openssl_default.readlink().as_posix() == "/etc/ssl/certs/ca-certificates.crt"
|
||||
|
||||
|
||||
def test_apply_sanitizes_certificate_name_with_slash(tmp_path) -> None:
|
||||
data = {
|
||||
"uuid": "12345678-1234-5678-1234-567812345678",
|
||||
"organisation": "InterDiode",
|
||||
"configurations": {
|
||||
"certificates": [{"name": "../../evil", "content": CERT_CONTENT}]
|
||||
},
|
||||
}
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(config=Configuration.model_validate(data), dst=dst, src=None)
|
||||
|
||||
CertificatesModule().apply(context)
|
||||
|
||||
local_cert = dst / "usr/local/share/ca-certificates/.._.._evil.crt"
|
||||
assert local_cert.read_bytes() == b"fake-cert-bytes"
|
||||
# The "/" must never be interpreted as a path separator: without
|
||||
# sanitizing, "../../evil" would have reshuffled the write up to
|
||||
# usr/local/evil.crt instead of staying under ca-certificates/.
|
||||
assert not (dst / "usr/local/evil.crt").exists()
|
||||
|
||||
|
||||
def test_apply_merges_conf_listed_and_local_certificates(tmp_path) -> None:
|
||||
src = tmp_path / "src"
|
||||
local_certs_dir = src / "usr/local/share/ca-certificates"
|
||||
|
||||
+37
-4
@@ -34,7 +34,6 @@ FULL_CONFIG = {
|
||||
"signed-by": "/etc/apt/keyrings/interdiode.asc",
|
||||
}
|
||||
],
|
||||
"keys": [{"name": "interdiode", "content": "ZmFrZQ=="}],
|
||||
},
|
||||
"yum": {
|
||||
"repositories": [
|
||||
@@ -76,7 +75,6 @@ def test_parse_configuration_from_json() -> None:
|
||||
configuration.configurations.apt.sources[0].signed_by
|
||||
== "/etc/apt/keyrings/interdiode.asc"
|
||||
)
|
||||
assert configuration.configurations.apt.keys[0].name == "interdiode"
|
||||
assert configuration.configurations.yum.keys[0].name == "interdiode"
|
||||
|
||||
|
||||
@@ -191,7 +189,6 @@ def test_apt_source_to_deb822_full() -> None:
|
||||
enabled=False,
|
||||
trusted=True,
|
||||
**{
|
||||
"signed-by": "/etc/apt/keyrings/interdiode.asc",
|
||||
"by-hash": True,
|
||||
"allow-insecure": False,
|
||||
"allow-weak": False,
|
||||
@@ -205,7 +202,7 @@ def test_apt_source_to_deb822_full() -> None:
|
||||
pdiffs=True,
|
||||
description="InterDiode APT repository",
|
||||
)
|
||||
rendered = source.to_deb822()
|
||||
rendered = source.to_deb822(signed_by_path="/etc/apt/keyrings/interdiode.asc")
|
||||
assert rendered.splitlines() == [
|
||||
"Types: deb deb-src",
|
||||
"URIs: http://example.invalid/debian",
|
||||
@@ -229,3 +226,39 @@ def test_apt_source_to_deb822_full() -> None:
|
||||
"Description: InterDiode APT repository",
|
||||
"Enabled: no",
|
||||
]
|
||||
|
||||
|
||||
def test_apt_source_to_deb822_folds_inline_signed_by_key() -> None:
|
||||
source = AptSource(
|
||||
name="interdiode", uris=["http://example.invalid/debian"], suites=["stable"]
|
||||
)
|
||||
armored_key = (
|
||||
"-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
|
||||
"\n"
|
||||
"mQENBGqh08IBCAC0\n"
|
||||
"-----END PGP PUBLIC KEY BLOCK-----\n"
|
||||
)
|
||||
rendered = source.to_deb822(signed_by_armored_key=armored_key)
|
||||
assert rendered == (
|
||||
"Types: deb\n"
|
||||
"URIs: http://example.invalid/debian\n"
|
||||
"Suites: stable\n"
|
||||
"Signed-By:\n"
|
||||
" -----BEGIN PGP PUBLIC KEY BLOCK-----\n"
|
||||
" .\n"
|
||||
" mQENBGqh08IBCAC0\n"
|
||||
" -----END PGP PUBLIC KEY BLOCK-----\n"
|
||||
"Enabled: yes\n"
|
||||
)
|
||||
|
||||
|
||||
def test_apt_source_to_deb822_signed_by_armored_key_takes_precedence() -> None:
|
||||
source = AptSource(
|
||||
name="interdiode", uris=["http://example.invalid/debian"], suites=["stable"]
|
||||
)
|
||||
rendered = source.to_deb822(
|
||||
signed_by_armored_key="-----BEGIN PGP PUBLIC KEY BLOCK-----\nabc\n-----END PGP PUBLIC KEY BLOCK-----\n",
|
||||
signed_by_path="/etc/apt/keyrings/unused.gpg",
|
||||
)
|
||||
assert "Signed-By:\n" in rendered
|
||||
assert "unused.gpg" not in rendered
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
from id_configurator.modules import base as base_module
|
||||
from id_configurator.modules.base import Module, all_modules, register
|
||||
from id_configurator.modules.base import (
|
||||
Module,
|
||||
all_modules,
|
||||
register,
|
||||
sanitize_filename,
|
||||
)
|
||||
|
||||
|
||||
def test_register_and_all_modules_are_isolated(monkeypatch) -> None:
|
||||
@@ -17,3 +22,15 @@ def test_register_and_all_modules_are_isolated(monkeypatch) -> None:
|
||||
register(DummyModule)
|
||||
|
||||
assert all_modules() == [DummyModule]
|
||||
|
||||
|
||||
def test_sanitize_filename_leaves_plain_names_untouched() -> None:
|
||||
assert sanitize_filename("interdiode") == "interdiode"
|
||||
|
||||
|
||||
def test_sanitize_filename_replaces_forward_slash() -> None:
|
||||
assert sanitize_filename("../../etc/passwd") == ".._.._etc_passwd"
|
||||
|
||||
|
||||
def test_sanitize_filename_replaces_backslash() -> None:
|
||||
assert sanitize_filename(r"a\b") == "a_b"
|
||||
|
||||
@@ -28,6 +28,14 @@ EMPTY_CONFIG = {
|
||||
"organisation": "InterDiode",
|
||||
}
|
||||
|
||||
CONFIG_WITH_CERTIFICATE_ONLY = {
|
||||
"uuid": "12345678-1234-5678-1234-567812345678",
|
||||
"organisation": "InterDiode",
|
||||
"configurations": {
|
||||
"certificates": [{"name": "interdiode-ca", "content": "-----BEGIN...-----"}]
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def make_config(data: dict) -> Configuration:
|
||||
return Configuration.model_validate(data)
|
||||
@@ -38,6 +46,13 @@ def test_is_relevant_false_when_no_repositories(tmp_path) -> None:
|
||||
assert NpmModule().is_relevant(context) is False
|
||||
|
||||
|
||||
def test_is_relevant_true_with_certificate_only(tmp_path) -> None:
|
||||
context = Context(
|
||||
config=make_config(CONFIG_WITH_CERTIFICATE_ONLY), dst=tmp_path / "dst", src=None
|
||||
)
|
||||
assert NpmModule().is_relevant(context) is True
|
||||
|
||||
|
||||
def test_is_relevant_true_with_repository(tmp_path) -> None:
|
||||
context = Context(
|
||||
config=make_config(CONFIG_WITH_REPO), dst=tmp_path / "dst", src=None
|
||||
@@ -69,3 +84,50 @@ def test_apply_writes_additional_repositories_scoped(tmp_path) -> None:
|
||||
"registry=https://example.invalid/\n"
|
||||
"@myorg:registry=https://other.invalid/\n"
|
||||
)
|
||||
|
||||
|
||||
def test_apply_without_certificates_does_not_write_etc_environment(tmp_path) -> None:
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(config=make_config(CONFIG_WITH_REPO), dst=dst, src=None)
|
||||
|
||||
NpmModule().apply(context)
|
||||
|
||||
assert not (dst / "etc/environment").exists()
|
||||
|
||||
|
||||
def test_apply_sets_node_extra_ca_certs_when_certificates_configured(
|
||||
tmp_path,
|
||||
) -> None:
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(
|
||||
config=make_config(CONFIG_WITH_CERTIFICATE_ONLY), dst=dst, src=None
|
||||
)
|
||||
|
||||
NpmModule().apply(context)
|
||||
|
||||
assert not (dst / "usr/local/etc/npmrc").exists()
|
||||
environment = (dst / "etc/environment").read_text(encoding="utf-8")
|
||||
assert environment == "NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt\n"
|
||||
|
||||
|
||||
def test_apply_merges_node_extra_ca_certs_into_existing_etc_environment(
|
||||
tmp_path,
|
||||
) -> None:
|
||||
src = tmp_path / "src"
|
||||
src.mkdir()
|
||||
(src / "etc").mkdir()
|
||||
(src / "etc/environment").write_text(
|
||||
"PATH=/usr/bin\nNODE_EXTRA_CA_CERTS=/old/stale.pem\n", encoding="utf-8"
|
||||
)
|
||||
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(
|
||||
config=make_config(CONFIG_WITH_CERTIFICATE_ONLY), dst=dst, src=src
|
||||
)
|
||||
|
||||
NpmModule().apply(context)
|
||||
|
||||
environment = (dst / "etc/environment").read_text(encoding="utf-8")
|
||||
assert environment == (
|
||||
"PATH=/usr/bin\nNODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt\n"
|
||||
)
|
||||
|
||||
@@ -34,6 +34,14 @@ EMPTY_CONFIG = {
|
||||
"organisation": "InterDiode",
|
||||
}
|
||||
|
||||
CONFIG_WITH_CERTIFICATE_ONLY = {
|
||||
"uuid": "12345678-1234-5678-1234-567812345678",
|
||||
"organisation": "InterDiode",
|
||||
"configurations": {
|
||||
"certificates": [{"name": "interdiode-ca", "content": "-----BEGIN...-----"}]
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def make_config(data: dict) -> Configuration:
|
||||
return Configuration.model_validate(data)
|
||||
@@ -44,6 +52,13 @@ def test_is_relevant_false_when_no_repositories(tmp_path) -> None:
|
||||
assert PhpModule().is_relevant(context) is False
|
||||
|
||||
|
||||
def test_is_relevant_true_with_certificate_only(tmp_path) -> None:
|
||||
context = Context(
|
||||
config=make_config(CONFIG_WITH_CERTIFICATE_ONLY), dst=tmp_path / "dst", src=None
|
||||
)
|
||||
assert PhpModule().is_relevant(context) is True
|
||||
|
||||
|
||||
def test_is_relevant_true_with_repository(tmp_path) -> None:
|
||||
context = Context(
|
||||
config=make_config(CONFIG_WITH_REPO), dst=tmp_path / "dst", src=None
|
||||
@@ -79,3 +94,27 @@ def test_apply_writes_multiple_repositories_in_order(tmp_path) -> None:
|
||||
config_path = dst / "root/.composer/config.json"
|
||||
content = json.loads(config_path.read_text(encoding="utf-8"))
|
||||
assert [repo["name"] for repo in content["repositories"]] == ["demo", "other"]
|
||||
|
||||
|
||||
def test_apply_without_certificates_does_not_write_openssl_ini(tmp_path) -> None:
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(config=make_config(CONFIG_WITH_REPO), dst=dst, src=None)
|
||||
|
||||
PhpModule().apply(context)
|
||||
|
||||
assert not (dst / "usr/local/etc/php/conf.d/id-configurator-openssl.ini").exists()
|
||||
|
||||
|
||||
def test_apply_sets_openssl_cafile_when_certificates_configured(tmp_path) -> None:
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(
|
||||
config=make_config(CONFIG_WITH_CERTIFICATE_ONLY), dst=dst, src=None
|
||||
)
|
||||
|
||||
PhpModule().apply(context)
|
||||
|
||||
assert not (dst / "root/.composer/config.json").exists()
|
||||
ini_content = (
|
||||
dst / "usr/local/etc/php/conf.d/id-configurator-openssl.ini"
|
||||
).read_text(encoding="utf-8")
|
||||
assert ini_content == "openssl.cafile=/etc/ssl/certs/ca-certificates.crt\n"
|
||||
|
||||
+185
-7
@@ -126,7 +126,9 @@ def test_apply_unsigned_repo_disables_gpgcheck(tmp_path) -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_apply_disables_existing_repos_before_adding_configured_ones(tmp_path) -> None:
|
||||
def test_apply_copies_existing_repos_unchanged_without_index_url_to_replace(
|
||||
tmp_path,
|
||||
) -> None:
|
||||
src = tmp_path / "src"
|
||||
repos_dir = src / "etc/yum.repos.d"
|
||||
repos_dir.mkdir(parents=True)
|
||||
@@ -148,13 +150,13 @@ def test_apply_disables_existing_repos_before_adding_configured_ones(tmp_path) -
|
||||
|
||||
YumModule().apply(context)
|
||||
|
||||
disabled_multi = (dst / "etc/yum.repos.d/multi.repo").read_text(encoding="utf-8")
|
||||
assert "[old-one]" in disabled_multi
|
||||
assert "[old-two]" in disabled_multi
|
||||
# No configured repository declares an index_url_to_replace, so the
|
||||
# existing sections are copied through unchanged, still enabled.
|
||||
copied_multi = (dst / "etc/yum.repos.d/multi.repo").read_text(encoding="utf-8")
|
||||
parser = ConfigParser(interpolation=None)
|
||||
parser.read_string(disabled_multi)
|
||||
assert parser["old-one"]["enabled"] == "0"
|
||||
assert parser["old-two"]["enabled"] == "0"
|
||||
parser.read_string(copied_multi)
|
||||
assert parser["old-one"]["enabled"] == "1"
|
||||
assert parser["old-two"]["enabled"] == "1"
|
||||
|
||||
assert not (dst / "etc/yum.repos.d/ignored.txt").exists()
|
||||
|
||||
@@ -165,6 +167,182 @@ def test_apply_disables_existing_repos_before_adding_configured_ones(tmp_path) -
|
||||
)
|
||||
|
||||
|
||||
def test_apply_disables_only_existing_sections_matching_index_url_to_replace(
|
||||
tmp_path,
|
||||
) -> None:
|
||||
src = tmp_path / "src"
|
||||
repos_dir = src / "etc/yum.repos.d"
|
||||
repos_dir.mkdir(parents=True)
|
||||
(repos_dir / "multi.repo").write_text(
|
||||
"[old-one]\nname=old-one\nbaseurl=https://old-one.invalid\nenabled=1\ngpgcheck=1\n"
|
||||
"\n"
|
||||
"[old-two]\nname=old-two\nbaseurl=https://old-two.invalid\nenabled=1\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
data = {
|
||||
"uuid": "12345678-1234-5678-1234-567812345678",
|
||||
"organisation": "InterDiode",
|
||||
"configurations": {
|
||||
"yum": {
|
||||
"repositories": [
|
||||
{
|
||||
"name": "unsigned",
|
||||
"index-url": "https://example.invalid/yum",
|
||||
"index-url-to-replace": "https://old-one.invalid",
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(config=make_config(data), dst=dst, src=src)
|
||||
|
||||
YumModule().apply(context)
|
||||
|
||||
disabled_multi = (dst / "etc/yum.repos.d/multi.repo").read_text(encoding="utf-8")
|
||||
parser = ConfigParser(interpolation=None)
|
||||
parser.read_string(disabled_multi)
|
||||
assert parser["old-one"]["enabled"] == "0"
|
||||
assert parser["old-two"]["enabled"] == "1"
|
||||
|
||||
|
||||
def test_apply_skips_repository_when_index_url_to_replace_matches_nothing(
|
||||
tmp_path,
|
||||
) -> None:
|
||||
src = tmp_path / "src"
|
||||
repos_dir = src / "etc/yum.repos.d"
|
||||
repos_dir.mkdir(parents=True)
|
||||
(repos_dir / "old.repo").write_text(
|
||||
"[old]\nname=old\nbaseurl=https://old.invalid\nenabled=1\n", encoding="utf-8"
|
||||
)
|
||||
data = {
|
||||
"uuid": "12345678-1234-5678-1234-567812345678",
|
||||
"organisation": "InterDiode",
|
||||
"configurations": {
|
||||
"yum": {
|
||||
"repositories": [
|
||||
{
|
||||
"name": "unsigned",
|
||||
"index-url": "https://example.invalid/yum",
|
||||
"index-url-to-replace": "https://other.invalid/*",
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(config=make_config(data), dst=dst, src=src)
|
||||
|
||||
YumModule().apply(context)
|
||||
|
||||
assert not (dst / "etc/yum.repos.d/unsigned.repo").exists()
|
||||
|
||||
|
||||
def test_apply_writes_repository_when_index_url_to_replace_matches_existing(
|
||||
tmp_path,
|
||||
) -> None:
|
||||
src = tmp_path / "src"
|
||||
repos_dir = src / "etc/yum.repos.d"
|
||||
repos_dir.mkdir(parents=True)
|
||||
(repos_dir / "old.repo").write_text(
|
||||
"[old]\nname=old\nbaseurl=https://old.invalid/yum\nenabled=1\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
data = {
|
||||
"uuid": "12345678-1234-5678-1234-567812345678",
|
||||
"organisation": "InterDiode",
|
||||
"configurations": {
|
||||
"yum": {
|
||||
"repositories": [
|
||||
{
|
||||
"name": "unsigned",
|
||||
"index-url": "https://example.invalid/yum",
|
||||
"index-url-to-replace": "https://old.invalid/*",
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(config=make_config(data), dst=dst, src=src)
|
||||
|
||||
YumModule().apply(context)
|
||||
|
||||
assert (dst / "etc/yum.repos.d/unsigned.repo").exists()
|
||||
|
||||
|
||||
def test_apply_matches_index_url_to_replace_with_unrelated_files_and_multiple_repos(
|
||||
tmp_path,
|
||||
) -> None:
|
||||
src = tmp_path / "src"
|
||||
repos_dir = src / "etc/yum.repos.d"
|
||||
repos_dir.mkdir(parents=True)
|
||||
(repos_dir / "old.repo").write_text(
|
||||
"[old]\nname=old\nbaseurl=https://old.invalid/yum\nenabled=1\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(repos_dir / "ignored.txt").write_text("not a repo file\n", encoding="utf-8")
|
||||
|
||||
data = {
|
||||
"uuid": "12345678-1234-5678-1234-567812345678",
|
||||
"organisation": "InterDiode",
|
||||
"configurations": {
|
||||
"yum": {
|
||||
"repositories": [
|
||||
{
|
||||
"name": "first",
|
||||
"index-url": "https://example.invalid/first",
|
||||
"index-url-to-replace": "https://old.invalid/*",
|
||||
},
|
||||
{
|
||||
"name": "second",
|
||||
"index-url": "https://example.invalid/second",
|
||||
"index-url-to-replace": "https://old.invalid/*",
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(config=make_config(data), dst=dst, src=src)
|
||||
|
||||
YumModule().apply(context)
|
||||
|
||||
assert (dst / "etc/yum.repos.d/first.repo").exists()
|
||||
assert (dst / "etc/yum.repos.d/second.repo").exists()
|
||||
|
||||
|
||||
def test_apply_sanitizes_repository_and_key_names_with_slash(
|
||||
tmp_path, monkeypatch
|
||||
) -> None:
|
||||
mock_run = _mock_gpg_success(monkeypatch)
|
||||
data = {
|
||||
"uuid": "12345678-1234-5678-1234-567812345678",
|
||||
"organisation": "InterDiode",
|
||||
"configurations": {
|
||||
"yum": {
|
||||
"repositories": [
|
||||
{
|
||||
"name": "../../evil-repo",
|
||||
"index-url": "https://example.invalid/yum",
|
||||
}
|
||||
],
|
||||
"keys": [{"name": "../../evil-key", "content": KEY_CONTENT}],
|
||||
}
|
||||
},
|
||||
}
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(config=make_config(data), dst=dst, src=None)
|
||||
|
||||
YumModule().apply(context)
|
||||
|
||||
assert (dst / "etc/yum.repos.d/.._.._evil-repo.repo").exists()
|
||||
key_content = (dst / "etc/pki/rpm-gpg/RPM-GPG-KEY-.._.._evil-key").read_bytes()
|
||||
assert key_content == b"fake-rpm-gpg-key-bytes"
|
||||
mock_run.assert_called_once()
|
||||
|
||||
|
||||
def test_import_key_raises_when_gpg_binary_missing(tmp_path, monkeypatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
"id_configurator.modules.yum.subprocess.run",
|
||||
|
||||
@@ -22,11 +22,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "cachetools"
|
||||
sdist = {url = "https://files.pythonhosted.org/packages/70/d2/47e8bc06fe2a06d3f5bdf20f1126ab66c4e99dc48d940e7ba873f7ac7131/cachetools-7.1.7.tar.gz", hash = "sha256:a3e2a00b14d8f8a6b70c1dae7b4685e7ad3bc965c5b42124a2d6ce895da6cf50", size = 40680, upload-time = "2026-08-01T21:20:40.434Z"}
|
||||
sdist = {url = "https://files.pythonhosted.org/packages/4b/39/9a4689914dd907915cee74733b95888fc1d8a21aad47a24a0a2deec73ac4/cachetools-7.1.8.tar.gz", hash = "sha256:1221d547a0b24b7f26fa891d40d488b5258beab9aebd8ed68c729be3af849c43", size = 40909, upload-time = "2026-08-31T19:02:53.985Z"}
|
||||
source = {registry = "https://pypi.org/simple"}
|
||||
version = "7.1.7"
|
||||
version = "7.1.8"
|
||||
wheels = [
|
||||
{url = "https://files.pythonhosted.org/packages/e4/d8/767faeda872075724b95dd675466a645f1b92aadcdcf2d1429dcfd76c176/cachetools-7.1.7-py3-none-any.whl", hash = "sha256:ef98ef375ad188819ef2f9b3645e3987f4b8c5b7550e436ad998c2de78296df0", size = 16830, upload-time = "2026-08-01T21:20:38.977Z"}
|
||||
{url = "https://files.pythonhosted.org/packages/d7/3d/9487690d0e937854db587205c66bab3c3cf88d9f00ed380b74cb88cc94ee/cachetools-7.1.8-py3-none-any.whl", hash = "sha256:a81e3844acaa7355b6567f97bd67a94a14ec3a9bc2cbbdae45b9592cc036775b", size = 16842, upload-time = "2026-08-31T19:02:52.554Z"}
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -237,86 +237,86 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
sdist = {url = "https://files.pythonhosted.org/packages/be/c3/4f2195f512fb172aa425a8803a874b2baa9ba7f80ff7b6080998761fc701/coverage-7.15.4.tar.gz", hash = "sha256:0548198fff07ccf4faf469520bce1c2eceb1ce3e62891921138dec10907f9d00", size = 936952, upload-time = "2026-08-06T13:50:24.442Z"}
|
||||
sdist = {url = "https://files.pythonhosted.org/packages/d1/f5/deb1a27aa20746c0278ac998c4179e272004699b2d33959ce020c5ac1615/coverage-7.16.0.tar.gz", hash = "sha256:077f0964087883176ff6ab9b074694cae29f8c708273b13ca62c183c6ed716cd", size = 945620, upload-time = "2026-08-28T21:54:37.74Z"}
|
||||
source = {registry = "https://pypi.org/simple"}
|
||||
version = "7.15.4"
|
||||
version = "7.16.0"
|
||||
wheels = [
|
||||
{url = "https://files.pythonhosted.org/packages/f1/84/651a9310859673aaa3b3203f1aa1641ca60fcf2494683e1c9474c7172780/coverage-7.15.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c705b28feb2775dc82a25f1d473a370bc37ff93f5177f4e29ce2425f560f6921", size = 222565, upload-time = "2026-08-06T13:48:00.796Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/82/f9/4dcf700137e8af550670f4d74d1b63828ce93e1e2b05e5f10710eb2ea987/coverage-7.15.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ff205ab5e3ecc670f6a4dd19d9cbf12ede53dd41cfc1e15716ec961ea6d314e", size = 222936, upload-time = "2026-08-06T13:48:02.391Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/07/4a/612ff1e780b3fbfd637486f542f84adc5503873d8b5d279dec1ffeef9414/coverage-7.15.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5172326e861a38b48b48befca15e0f477a26b283337a33a739c8fed229934e36", size = 253926, upload-time = "2026-08-06T13:48:04.382Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/b0/04/d1cff1c2ead4708a6a79c01d3736b6a25bd38a36678398f72a8dd33dfad9/coverage-7.15.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:12b59c90084e3234fb11184886bf4a40f4f16a8c8f867be2e087b81f8e8868d4", size = 256523, upload-time = "2026-08-06T13:48:05.996Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/b9/80/d34e13fb4b293cbdb9665838cf5522077b8ad14ef947550631a4bced36a5/coverage-7.15.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349062d66f00b40fa2c1c222438bad25fabf755631b5d82937fe985c8008615c", size = 257759, upload-time = "2026-08-06T13:48:08.036Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/0f/e7/2c5fe7636fdb0732fe0f09f308a5b066864078b7fc61f6678e8478554f2e/coverage-7.15.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4256ced708e598e05209bc1a8ab4074e04a51dba4c62fb45926a229af675ace7", size = 259890, upload-time = "2026-08-06T13:48:09.834Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/92/28/9689f0858dfff59c2ea688938ab9fa2925631235df67126a42b6c5c70ae1/coverage-7.15.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d80f974b20782d9612c8b4c9beeca867074c7cf4079d1419843fa25a26428b25", size = 254121, upload-time = "2026-08-06T13:48:11.459Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/f9/e2/785077c230c157243eb5aa9a26c3be260ecd02001bead54a3cada3df8e03/coverage-7.15.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e179f19bfe1d31f8eeeaa12990194d761c4f62f0759661000bca6cd8729f40b", size = 255891, upload-time = "2026-08-06T13:48:13.209Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/d4/90/e20371b17b40f912f21305c2db2f30efa3de306f7320fc916804872c85a4/coverage-7.15.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8bc16bb47b7679670eceff71d78bfb7d6e5b143f6c2cd117487ec7c75e0d4b78", size = 253859, upload-time = "2026-08-06T13:48:14.736Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/05/49/25371987ee459a5f67c0427fb75c74f9358e65f2c71fe75bf41c1b6c5fcb/coverage-7.15.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1cd685005cd2c4200adfc14cf39a603b9320efab3f18a8f7f156d20c9cc3345f", size = 258011, upload-time = "2026-08-06T13:48:16.464Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/30/6e/32e67467f6154bf4f1c4f63b05acc5097cba4237d45bbeeea446b52e8ac1/coverage-7.15.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:337399ad2c93b3acd2a937627dae8b3e86b66707cd3d3e856347999aadf1ef8d", size = 253676, upload-time = "2026-08-06T13:48:18.493Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/03/c1/8b24192e89286399765155251f99ee9f070a9d637109018ac23d99b99f6f/coverage-7.15.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:96e257121228ec5cd2bb919276e94ac11074471bc37d68dbae0e8308cce15fff", size = 255453, upload-time = "2026-08-06T13:48:20.057Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/16/6f/8b41ebdf67c87854e17c035336a90f1cfbad0c14c2a584301be6ff148718/coverage-7.15.4-cp313-cp313-win32.whl", hash = "sha256:c65a9e0dfc6143491879da4e13b5e30f8be192055de508d737fb14601edbd22c", size = 224605, upload-time = "2026-08-06T13:48:21.655Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/e0/e2/2946c7f0b42b152ecb21ff1bdad72e3d301e790c0c487e4a86e8c9f69347/coverage-7.15.4-cp313-cp313-win_amd64.whl", hash = "sha256:2ff8f5e9b8f7a94f0c11c45631eee103dbcb7d63274edd12c56efe1be690b3b4", size = 225148, upload-time = "2026-08-06T13:48:23.376Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/9e/83/3f4a69957f48ae7a0aba76c34743f88963d607b19e03f3f8e66f91cae0f9/coverage-7.15.4-cp313-cp313-win_arm64.whl", hash = "sha256:6e0a8a5083b096487d6cfced94cdd514d8f5db6f113610fb36c0620edb1028cf", size = 224536, upload-time = "2026-08-06T13:48:25.117Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/ea/ac/748cf29eeb2d6be34a3176ce26a4f49e38085ee08e8935f05f6f26ed7e0f/coverage-7.15.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:770e9325ab5ea6d56f77e59b29ecfe0ac20b57a82a601876f90494a4dda0386f", size = 222608, upload-time = "2026-08-06T13:48:26.806Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/0b/02/1abbf5c984677b0aa439cdacaccbf38d248939d8ef8fe1cc7a50d73edb77/coverage-7.15.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d12b33a3a50a1676b7784dc8d00a0c6d66a9f2add4b85a041c19b6a7e53ef23c", size = 222940, upload-time = "2026-08-06T13:48:28.432Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/eb/e1/ff8f9f53d9fcf586125b55d0b1f04ec1c14955fee41e83d5814bee141bb5/coverage-7.15.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5669c8378ebde86f5def7a25d29586631b58acc27ffde04399f678f3dfc6e082", size = 253985, upload-time = "2026-08-06T13:48:29.995Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/a1/26/595759762e514e81be1d7d01ed03444303bcd152226a6529998d253f9201/coverage-7.15.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff97a14362eef486483ed44042ca2027ea257df6ff768e62358ee0c9776925ac", size = 256492, upload-time = "2026-08-06T13:48:31.634Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/24/68/b79aabac54d482be23b5fcdd4f4662bff24a78edc4ee29201726929936d5/coverage-7.15.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a325e815318638aed1655d9c06e6d7c2d3d46c09231ce988070428a8762d734", size = 257837, upload-time = "2026-08-06T13:48:33.186Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/09/0f/bf7f297885a5bf6fd71e5782404e0ff059ca09e8711ceb3a08544abde45a/coverage-7.15.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:474223409d88eb20d2d6a0d37ea60e8647a65a90cc008dc1f0410af5f64f1e0d", size = 260152, upload-time = "2026-08-06T13:48:34.75Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/fd/f1/296744e854ff8368542343457414380465e9ceefb9192342feb9d3bc461d/coverage-7.15.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f2f62ae3cd189dd2e13aece758c57b3eecbd27be070dbd4cbd10936049e5dbf", size = 253978, upload-time = "2026-08-06T13:48:36.434Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/55/b0/bbdb2e9057493e66220a2e149ca2d301ba0e3a58a83bd6b90de9826d16f3/coverage-7.15.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:39ece820e29e0a2ba34b3ecb3be83c27e997eed8926f2ba6fe7ce7a0bda5843b", size = 255846, upload-time = "2026-08-06T13:48:38.317Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/96/e4/38015b2b6d21258713bd17e76b59d033b191efb5703589cffd037dfbca20/coverage-7.15.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f21b56dcace11dfe013014201f577dcd592b2a9b72182d930361b47cf6f73f25", size = 253808, upload-time = "2026-08-06T13:48:39.993Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/0b/64/0d515c1e60ee6fbfd1a0e79c07cd87d388a233b7adc37758735677203808/coverage-7.15.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:93a3a0b662abcc10c73a47cbc72cd60f63618d6989fb2d1286e50eacd974f303", size = 258081, upload-time = "2026-08-06T13:48:41.971Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/91/71/04d9e7a3642146c6351338aef4ef85ab11dbbb54744c13245caba1aad1c0/coverage-7.15.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:141fae2cabf5569b782c10afc4c850ce10f618c13f8db54765cba99cc839da1f", size = 253624, upload-time = "2026-08-06T13:48:43.731Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/b4/a7/6c28b74c81ebff66987b0e2522ba5cffa3e90b0c33cb6a2eb264d4ee8cf1/coverage-7.15.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:81294c7e6ab30c5f74c0353b11b2fd6320e72d9bee6ac73b357caa8b916323a5", size = 255280, upload-time = "2026-08-06T13:48:45.58Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/52/af/bc19996a7014b98d7bbb0f0939453c67074af65784a3aa16a789a07381fa/coverage-7.15.4-cp314-cp314-win32.whl", hash = "sha256:7bbd7d6418e0dab31a206af5203bd43ae36edb8e7fba1940b055d3e9249290d7", size = 224768, upload-time = "2026-08-06T13:48:47.525Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/ee/90/219484e476d6e101ba0a444852579e05f5b75c37c611a42ed1190f73ef62/coverage-7.15.4-cp314-cp314-win_amd64.whl", hash = "sha256:f0204ed122758782970526057093f448051a39db9d810d4e344bb87a3546f425", size = 225259, upload-time = "2026-08-06T13:48:49.513Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/b7/66/fa77daf4e383e5f776dac62c2409b6af81910ae6fe326bd5170dba74cc63/coverage-7.15.4-cp314-cp314-win_arm64.whl", hash = "sha256:9e71e7bc71c686a123347ae47a0de33a175e797a85bb57b791492adf4eec8ed8", size = 224684, upload-time = "2026-08-06T13:48:51.235Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/58/5b/f03bf0ce362bbf3f785fa5219620d00778d4ac6fc9e407734828e9c672f6/coverage-7.15.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7c922735321eef3f87c280a3d39afff6b646723a2880b862cda4ac7a093b8aa8", size = 223338, upload-time = "2026-08-06T13:48:52.896Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/0f/76/e77d0ae22501831cc9f92193e8a957a5caa1dd177f90a6d1d9b106242d92/coverage-7.15.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f41c17c4668a655ce96d090d8d5ffdc24ef64b5a02f9753884d08483e8a4a41a", size = 223609, upload-time = "2026-08-06T13:48:54.688Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/82/1a/b1f089da8d38ac612fa2dd6dc7f4a1a7657d12f3e261d2996edd3a838d0b/coverage-7.15.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:46822e9b6ff1c6a72b518c162c44a8f45a61a1d609c51084bf5b16c023c5037b", size = 264970, upload-time = "2026-08-06T13:48:56.403Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/bf/31/e66d98d6e9c7fcc88470f1e234eaf6b1950dc0dfbf797f7282c1c861da24/coverage-7.15.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d6f4955b73b5445271379a59e3792b0d978f42d4a01e0cf7a67d9c33a3bb0a5", size = 267088, upload-time = "2026-08-06T13:48:58.41Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/59/a1/ae94eb2c541add426378408379f233591e069040b1e2cdb33df9498a0682/coverage-7.15.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3fc9e047706fb4a9abb54f719d3aa643e80e5bb3818182c40aee01ac0f0247ba", size = 269508, upload-time = "2026-08-06T13:49:00.42Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/9c/c7/88a10694a1c6a213569766aba9f25847b28155d4ac731b13226db216356d/coverage-7.15.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05e491d4f3165d62d4f5c8fd48dfeabf2ae8f42cbbd484319af33ea851b78982", size = 270629, upload-time = "2026-08-06T13:49:02.234Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/b3/34/d8b8232e5e55169933b59aabcef2fedfa4b9d8897361bb80fcbda146505f/coverage-7.15.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:226c66e80ec0598d3b9b4874123df167ccca342aca8714f77cac6829688ee09c", size = 264043, upload-time = "2026-08-06T13:49:04.102Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/7e/35/58b009dbf8c471c7224716478b9fed4a7e1af15320e1ed41660978504663/coverage-7.15.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac41cc14bebda0dbfb0628036b7f75706935c95bcc07fefe9a0f93614aa60a57", size = 266963, upload-time = "2026-08-06T13:49:05.821Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/62/aa/57fbda1b42c892968273c56b6ee9dc0f1310850859230a507bc7873b1f65/coverage-7.15.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8af623e5cd92080acddd02b38f2f406a2c3a0893c38950b211890361448fbf26", size = 264569, upload-time = "2026-08-06T13:49:07.706Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/98/8a/360e6e7f24d477b7e889703af0afa878d15b6d4d8d2a822b2835c169a879/coverage-7.15.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07545711d4f0f32852a18f18ad11f76f0109909d09e78b9008b4cfc67e829429", size = 268299, upload-time = "2026-08-06T13:49:09.587Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/4e/89/6f701261aee21b6b5fa8f7872229406dc917e125069448292223bf213606/coverage-7.15.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a0865421cfdc53654b342d515e5a233187590882d20b95752150e53f65460017", size = 263413, upload-time = "2026-08-06T13:49:11.604Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/3f/0f/6f04036edc260ed425af83e834f627fad48941ce97b50bfe6edd8b6fa623/coverage-7.15.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:460115e32ee40566476db5048f9bec1e842c127ad8e6f8be745aad3ac9cbc839", size = 265725, upload-time = "2026-08-06T13:49:13.38Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/c4/ce/d19b5d4d5c49a7bfb925fd74310fee7d28bc99520ac3367ccbc54e662518/coverage-7.15.4-cp314-cp314t-win32.whl", hash = "sha256:cbde877ef9dd7baf272b9bfef2b8a25edd45d9170fc326951dd20eb480335e85", size = 225079, upload-time = "2026-08-06T13:49:15.265Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/26/bb/7aa1b3b173faee0679037ca950bbbe1247273656697994d8d13f80f8d4b4/coverage-7.15.4-cp314-cp314t-win_amd64.whl", hash = "sha256:3da9e92d1c551fd7563833e9ade686efb0c4b7363ab7681a94283958c950bf5e", size = 225911, upload-time = "2026-08-06T13:49:17.279Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/81/1c/4ea9e47426d80038d9222db3c4534cb6021a74b237d3ff97ffd33b6600dd/coverage-7.15.4-cp314-cp314t-win_arm64.whl", hash = "sha256:3a54f5a0d85050c73a38f6793090ee83974531e67fe5e57a1da9bee11398aa5e", size = 225219, upload-time = "2026-08-06T13:49:19.293Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/2b/c4/dc5d2ac8f9142e7ec7de66e7bf0591db29d78955a040bd915870d9c0e657/coverage-7.15.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:2c9872e4d9dc5d3cf616bf4b382f5a00359305a5be666a3dd0b5cdb4e49597f9", size = 222604, upload-time = "2026-08-06T13:49:21.279Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/70/39/33e63df81fe2ee100897451841c821467635923e58e37c6bd4b46dd8106c/coverage-7.15.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:e101dbb4b9b72f0cddd8cdc8c9c5b47f456766f5e0ac82dbfb75e5c55409b78a", size = 222944, upload-time = "2026-08-06T13:49:23.187Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/99/1f/ef3ffb5557febc75a0d97aa459d0266d7d741110265121cc6d8539343d44/coverage-7.15.4-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7d1abebdb047729e852b9c77a00497dfbeb11eb3a117e037d7dbc3ac8e5f5c54", size = 254050, upload-time = "2026-08-06T13:49:25.008Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/6f/f5/1f0f6f77698c3601ca0ae7431e34b24c62ca2f06fecb23b73ed1f651d2be/coverage-7.15.4-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d28a4a899354d0ea6214cc59b4fa19eefbce1b9ff1688ab579acf49e894bd3fb", size = 256967, upload-time = "2026-08-06T13:49:26.896Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/03/7a/2ed9bed79925f4367c83c77f66a89e5ca7229c288d2d19ad5f36d1ca0070/coverage-7.15.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffb3c2aacea411cc7e1d27712490c11108e2de1d39019ae32915493a59a8b9ed", size = 258587, upload-time = "2026-08-06T13:49:28.692Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/45/8c/fa34044f71b7cc4ecb6da9c2408770959b0591fa9b5fb6fb6bca38f94298/coverage-7.15.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9447978a92f405d301123cfd39ff49895490efb769a758fe2734c7f631bf8ce", size = 260785, upload-time = "2026-08-06T13:49:30.472Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/4f/54/d5727ce36b4524a7394ab9f5f1df378e1f23affcdab01037dc8655185cc7/coverage-7.15.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:050467a7983b8e2fe7dd41a78bb30c3e7f8c0b8cafda14b1c46f8b5e3cf2dd3c", size = 254545, upload-time = "2026-08-06T13:49:32.271Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/dc/e6/6e3783e576719590194bdffb6dd6d85490801785b7c331e35a245d8cb8b5/coverage-7.15.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d003b7a5708ddad5c206c79607a6b92abb6fc13c57d99d8a4468cc03a2941ced", size = 256682, upload-time = "2026-08-06T13:49:34.089Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/dc/f2/bacdbde18b69ed2de424fcf64d9fb0a4913753d4f0eca8bae9daad69f4bd/coverage-7.15.4-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c38efe30fd74e5c19e9433f11fb1f5dc9c6522770971b7c6145bbaa413dc8800", size = 254560, upload-time = "2026-08-06T13:49:36.052Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/6c/a3/1fb927196e3477c1b48831169ab58ba08f451ba87ae311ff1de68b26a616/coverage-7.15.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:1f4f826d70f772ab8b0c052329580d7fe8b8abd191e4ce0c8f81aec6614665d3", size = 258792, upload-time = "2026-08-06T13:49:38.01Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/41/58/30d4c149c69053de0edfe325614c1d28d508f62b1783e0e4a234d2e49136/coverage-7.15.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4a4bf917c9953f57c957be31c1cd504e3bd2f34d4a352b9d391a3025336f6768", size = 253968, upload-time = "2026-08-06T13:49:39.934Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/89/e4/77f639371b918aad30dda4051f95404b43578f7f2e2f87ba73e02ed1ff37/coverage-7.15.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:1c9bf40ebef178a45192c75c4964760bb261b0e6ad725da5fc4c93f674f19753", size = 255893, upload-time = "2026-08-06T13:49:41.825Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/5c/62/13be29b3ddab35f14c87967a4820a05106d2a3eccb4fa4ff550bf30b75e0/coverage-7.15.4-cp315-cp315-win32.whl", hash = "sha256:43619d04c3671792d2c4706ae8bf45e265dc87bbd4078189ef8b847ea1e74be2", size = 224768, upload-time = "2026-08-06T13:49:44.08Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/a1/70/af0c6be0f964af6954f6b74bc109b0dbca02824696d2520fb17fe1ab06e3/coverage-7.15.4-cp315-cp315-win_amd64.whl", hash = "sha256:be619439dbcd31a2eab10b32de9fff62c26ed4bab69dc32b8363fdaaa0882809", size = 225242, upload-time = "2026-08-06T13:49:45.899Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/4f/2d/f3bd3aab899fc9efc18b53133ee68f5f98574ef480649b23e12962226387/coverage-7.15.4-cp315-cp315-win_arm64.whl", hash = "sha256:def597967dafc2e8d97c9097ea453c464e0bb8ed38f193a43070f10dc623bb6d", size = 224674, upload-time = "2026-08-06T13:49:48.322Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/f5/ca/f69251cd63eabc6438321aea22148754cce758a26bde07dd490e3fe7cfc5/coverage-7.15.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c7dbc748ac8a1e3e59a2b28bea47675e6e778081dbbf081bde0d75def2fcbe1d", size = 223333, upload-time = "2026-08-06T13:49:50.293Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/a7/a7/037b53b2885b0d8447064432491a4d5a1014cd9f97a594d53acd0c04541a/coverage-7.15.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:2413074a5ecbb61a01a7888fc72db0ca324d13588c5b38bc0dd8564cdcdfea26", size = 223630, upload-time = "2026-08-06T13:49:52.637Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/80/4f/152b8a4779ae90da11bb24f7467df8a59f0be48a5c52acb856325ca48289/coverage-7.15.4-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4e6f6f632b7b2f714bf7a1346e8f97b650ee71f3c298aaad42a2ab60f0f07645", size = 264489, upload-time = "2026-08-06T13:49:54.52Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/10/2d/84b4b9e0e1dd6528a51920ff7031f35b789382e467a28ec6a5a578cb8812/coverage-7.15.4-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8df457da2249d3c75ca2e5e835d59c725abfe92d27fdff6cd99eed85b51d5e9a", size = 267567, upload-time = "2026-08-06T13:49:56.721Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/53/fc/ba01cc25299f9f8a2c8b02d3b28c53f3543d9fbfbe4e74fa2760b48f163e/coverage-7.15.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:050f66a08805acb5b8a23c6d4a517b1ecf82c08e81ed0e4bd727df065e5c6624", size = 270123, upload-time = "2026-08-06T13:49:58.736Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/cf/d0/db2647cbf40b14f8c308f94ff7bf89c06d564e59f396906edf50086ec788/coverage-7.15.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1587fb771d1ccceef708fdde1e5af8c7ed24b486b61d13a321acb7d8145390aa", size = 271107, upload-time = "2026-08-06T13:50:00.811Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/70/ff/4d2d17924552c458bb4f77dd631f0e3bc92fbbdf2d2d916cd4b33bbfd5b1/coverage-7.15.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8b4f1c3a69ca580f3fbd6b2046915f536d7f586874f25c1bb23add2a3c88d50f", size = 264955, upload-time = "2026-08-06T13:50:03.023Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/ee/de/dc010c7a3691f396d93bbc26bfcafa1c2a3a351cd520470f15faf5795bd5/coverage-7.15.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:ffb58d7eff5b7f6ecc6fa21d6288ab7f968a212cb67d682c269c09b9eba3b66f", size = 267949, upload-time = "2026-08-06T13:50:05.557Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/78/ea/dc96a11375e83c045c2f7c61fb6918277cfe9401db7c0f7b1d111a84b2e5/coverage-7.15.4-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:d9df165544774574ee004b953023d1bebada1894a80b1052a43d798b0f676e67", size = 264421, upload-time = "2026-08-06T13:50:07.612Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/c8/86/b77131a0f9503ce461cd577076147d7a9040f0c5dda772686f729e2cc9cb/coverage-7.15.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:f9de0a24a4079b53e523b5c5e2c5945ec251ab486652659955187cf255a259bc", size = 269121, upload-time = "2026-08-06T13:50:09.58Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/24/24/944bc35007862955e7ebf05754e645419dcf5d7526c52735cfa2715e8ebf/coverage-7.15.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:150089274bdc9f940628552cb92844e0223c987f1902ab8efe9f45a2ec758d88", size = 264565, upload-time = "2026-08-06T13:50:11.722Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/c7/cc/a3bb9f93e7e740659163e2ea584f8196ddcd2c456a5dbe15f6c50105fec1/coverage-7.15.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:a58a94fed5da6997d258e8f7668c1e195fbd04a691d781b7558f1e468f9e68bc", size = 266522, upload-time = "2026-08-06T13:50:13.786Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/49/dd/e0e40f3560d878d888c580698ff5ad1179f5e1c3ac949684ef66b41a3817/coverage-7.15.4-cp315-cp315t-win32.whl", hash = "sha256:ebd5a6d8466ff30836572f3ba2cae8a5e8f85029b1c6d5e2ed338dc472a5166a", size = 225068, upload-time = "2026-08-06T13:50:15.825Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/c6/7e/37732ea80eebc30e976e4cdab15c190bc42d96959a42e38ddf6f8c60468f/coverage-7.15.4-cp315-cp315t-win_amd64.whl", hash = "sha256:288bde2a2d7ab6b6c2d7252fcde8b524387f2d970bdba9658fc6f8bbcaef0f9b", size = 225895, upload-time = "2026-08-06T13:50:17.928Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/c6/08/1e00f7923eaaba45fb3d51dd794125fc766304b1df264f3a9c6557bfb30e/coverage-7.15.4-cp315-cp315t-win_arm64.whl", hash = "sha256:68be5e1de60ff13c9095bbec0e5a7fa45b33b101752215b91345ea1f61c4a278", size = 225213, upload-time = "2026-08-06T13:50:19.981Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/b4/d9/e70c286c979378f061d8266e279b686ab0b0b688e1fe0af864684f23a77d/coverage-7.15.4-py3-none-any.whl", hash = "sha256:964730a1e9de9c0cf11be6a1a3c79ce419c34882842abd256086ba4698705e84", size = 214332, upload-time = "2026-08-06T13:50:22.192Z"}
|
||||
{url = "https://files.pythonhosted.org/packages/54/c5/e62c87f4799d1e3647d5b2ae16ea1d12205d72fde1ea8529e13fe050f678/coverage-7.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1545c52ce756b8a97007f439a220297f1cd72a2cbbcdffccdf1c1f70e74f9a42", size = 223215, upload-time = "2026-08-28T21:51:55.628Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/89/e9/5e62fda9397175fb206f75368b6e85da06d831c181b6d0f67ca073cd2f89/coverage-7.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0598aadae641f30a0796b75b45c0b9c5de8619bd5cfb251bb0cc254e86e6dd13", size = 223585, upload-time = "2026-08-28T21:51:57.355Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/b9/40/bede08621b1ba67e88c4d3336c22b52cb7911ff1fa4ef055344b6670e58a/coverage-7.16.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4080ad6bad9f14690e6b2104f5e8d137ccc65a4b5427a36662090637d4bd16d5", size = 254575, upload-time = "2026-08-28T21:51:59.233Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/12/d8/ab0bdaa45dfd6b8cbf1a3ec548fdf827684b1997f9724375c5b3e89144fb/coverage-7.16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e9883a2f8206ce3af59117dc278e5d043fea06912bca3f199816129e5e2de354", size = 257172, upload-time = "2026-08-28T21:52:01.015Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/1d/bb/135de81784bbd7dfedcab2b92b03d71d75b09b0815b42d6dabb052def5a6/coverage-7.16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:984e5430fc6f858385009e92549955157d79335b1f3e13e1031e0f89d1284261", size = 258410, upload-time = "2026-08-28T21:52:02.76Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/ad/72/ce44ecc062fb2e43d9447bb76154d091c2139232f20c125297c4b58f4c6a/coverage-7.16.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b1374099dd1ad0d31fbb6c95d00a56a3c5e85fb3343dca14fc12f78323a2b42a", size = 260539, upload-time = "2026-08-28T21:52:04.821Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/e7/c4/9389c36a41e59406ca2bba493807c2294d2e5186a7e9ebcc2e63a0f2a711/coverage-7.16.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34d8686bce035c8465b318a8c2890e69ba14a00801a27f4eb6bdc97c23944d87", size = 254756, upload-time = "2026-08-28T21:52:06.68Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/ad/0f/7762447b15e01fb84263608540123c4d9941f06303265ee74d801ccbec0e/coverage-7.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:857fceba6ff4b507ee0ad98798a33d544a8473df0c542bf04251ee4ed5ee6292", size = 256540, upload-time = "2026-08-28T21:52:08.529Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/e6/fa/c60dc75a8346c1dbebebc7279b19971c88f70dd575f0bc10bc0cb16f92d5/coverage-7.16.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bbf08d951abaa1ce89e28c998361d56b952413846b459cd017f116ad4c9adbfa", size = 254508, upload-time = "2026-08-28T21:52:10.323Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/c3/f0/4e0834f3a1fccaa8bf625a2a1d73bde0fa32577dc3249853c0dd0e7f2b20/coverage-7.16.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1a03e78f53e4d2ab13adac19958a89322d1829913e5623d642627bf60b35da21", size = 258659, upload-time = "2026-08-28T21:52:12.124Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/b4/ec/fe712d3a11fd6e874565a5fa5497c48b8ece561d9611da040b44cdcf8386/coverage-7.16.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:dcd3dafcdd78305d27c59a1006b53a4990acb89e68d8fbe0992f4f83503c827f", size = 254326, upload-time = "2026-08-28T21:52:14.181Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/e7/78/093e12072e01034c65ff380f76c74b79dd83e44fa92b689a2154389be734/coverage-7.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c1bcfe470a796fbea6234accd81d258a31574dc0b7bf569e16be757572c4de17", size = 256102, upload-time = "2026-08-28T21:52:16.003Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/9b/c0/265176117ca5d06e3f65575842884cdda96cf213350a31e9d41c80d65854/coverage-7.16.0-cp313-cp313-win32.whl", hash = "sha256:1420370276f1694b663207b8245c3628aafb9624fe3cebf313a13d860e55ee67", size = 225250, upload-time = "2026-08-28T21:52:17.82Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/1f/01/8a87f2c04fde322430b45d16d8f543693e9894c5b2d2ca238a287c00beca/coverage-7.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:496277c8d7beed695e02c7be53516a0152e4caef8738a0feab6a638546cce449", size = 225790, upload-time = "2026-08-28T21:52:19.641Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/23/40/c21feacd9edfe7063195bf9cc84d650e9938fc6a23063e4f027199b160e1/coverage-7.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:181c2906b9b3759955c1c33c51fbb91c754fbd0b82ea49e2c81061f5a052082c", size = 225180, upload-time = "2026-08-28T21:52:21.613Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/ea/73/850675f262391b322c4c988b6cdc32cdc6629288f0fb158687b587a393a8/coverage-7.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:54b7fba6a74d010de34319a0419d5b65af8c00f539ad0b6f39fc6f342ab99697", size = 223258, upload-time = "2026-08-28T21:52:23.558Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/61/c1/4f54c6d47c80d1cc58ef8fe6b74e6eb50f9e2c0f6e2de6cf38dbca2937b8/coverage-7.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fa4ff0b3dd52208d2b30903022d5087f82000507b504753dfeee83e4f32d6883", size = 223587, upload-time = "2026-08-28T21:52:25.627Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/3c/be/298f2456230fb44e272a4e53a41b3f3c39f0821c242d7b7daa9787b4d6f7/coverage-7.16.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:35a9676bf86097f790113ebd9fb67681804ef54d40941d2f10ba68c02239e575", size = 254632, upload-time = "2026-08-28T21:52:27.689Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/a3/9c/a1bda6439c19c4783d50df896142b67b9e7d432db36675d339a32778669d/coverage-7.16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f98d438add63546745e5e847192e3e9ab897ed6f2ca96f8281e2f5a15958ae62", size = 257139, upload-time = "2026-08-28T21:52:29.741Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/f8/cd/cd735c9be757f97237c305f36897a5e5b348bdbc12ebed3b2b80060dd8a9/coverage-7.16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:151855767480be14db595cbc2040f6a4db965cdfeebd354d79b0256742b029e0", size = 258484, upload-time = "2026-08-28T21:52:31.68Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/e4/04/84b2e1e8aae9db3f549782f28ce25bba5fd6a9c7bfba3782ffe8b4cd2559/coverage-7.16.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:183613f664718b340589d7f005c7e92b4b601cffd20a8a4117cfda3e983b080f", size = 260798, upload-time = "2026-08-28T21:52:33.642Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/8a/4f/e04cf52483619a4dc5dd6367b30c9a8ac52243567fdfacec9b11a441565c/coverage-7.16.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:785b114356c99c0dd5b3f57b9696cfd57b7704f4c53847df8dc88c6cc0d9bcb6", size = 254612, upload-time = "2026-08-28T21:52:35.543Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/da/33/627c4113f66bfffd43807f54dbf080c4632ecf12e4ef7a3bdd4ec38e46a2/coverage-7.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:30f5aee6d1d517abcdfd4f9cad027969ff79a1440a22da263f9514e31b5b66e9", size = 256495, upload-time = "2026-08-28T21:52:37.485Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/3c/38/aaca432f4e008a88f2bc4d1459aa7016d8d1bbbe801f7e4fa3cf2746557b/coverage-7.16.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:190ffa0f5af966254c249fb3aeaca2cef389785e3e287fd577d39e134d20f8a3", size = 254454, upload-time = "2026-08-28T21:52:39.425Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/cc/db/8430aa87ef0a508f4c17c1b8fa7e0cf80231988d9081aa36c194036592d6/coverage-7.16.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0ccc37c00e1a5d30840902c54557e104d04aead872cedf6d2281c8725a467e06", size = 258728, upload-time = "2026-08-28T21:52:41.32Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/76/88/cd8aa8c82493ffbd291d3ef5554452fffc634c6c6098a04ac848c79c98f3/coverage-7.16.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6c60cde430c0e7e3be612973af39b4cff90ec2e2defe7b2b701daea3a0ffff04", size = 254271, upload-time = "2026-08-28T21:52:43.278Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/a8/49/fe16c811ea9314a84b48f34e4bf5a3d9013091093b285a74b2272fc863d7/coverage-7.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c5297028c8df849a61b29129cadfe682f90b5b396f528eb319a57d7678eefdad", size = 255927, upload-time = "2026-08-28T21:52:45.461Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/d1/45/d0bd410e78cfbf768acc8099b335e1d5c0d5c26103c796d2bebdee001715/coverage-7.16.0-cp314-cp314-win32.whl", hash = "sha256:136988df5bc5a48795d9c42c75c4bbda5d9a78e750a080c1233010edff93a1af", size = 225424, upload-time = "2026-08-28T21:52:47.658Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/17/78/1ce6ce4646822e9308dcdb1942eaf31bfd7da43247b8886338b0d6fe3767/coverage-7.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:ce2ba5e9f1842fe09165825abfb3bc6b527c71a27bc2eb3a10f2284ced64506d", size = 225918, upload-time = "2026-08-28T21:52:49.692Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/f9/cd/e1323fe3a7dfcdd709451a43fe708ca1dfd36a7fc07b34eb7bd1dfdfb52d/coverage-7.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:a89d07e48d9baead9a15599923a02f62c6df6c3d85aa84ef34be3c9fd6aeb91f", size = 225344, upload-time = "2026-08-28T21:52:51.665Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/39/fb/1c15460d4cf915f09ae3ad3862fef4f901838991c5641b0cec545050d810/coverage-7.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6e2854b62601c89a63814ad5def3b90d99c6724cc4cb977f75b725e5fca4b1e3", size = 223986, upload-time = "2026-08-28T21:52:53.572Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/9f/73/347d2d0009ac211f79ee2a2364fd2aa19d6b9628dc22ed13a9b9386097ab/coverage-7.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f093faf23df888518d273be6da65f0ec5a25b5d8b670231e4d87de07361042e7", size = 224254, upload-time = "2026-08-28T21:52:55.59Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/5a/2f/51442e6ad9d705369596f08496021647e276d5b57311818fd4312d93509b/coverage-7.16.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b7dbbbf6551eb94618e7bc76ab61cc2740a5b3d13294171bd6adb36e12346c3c", size = 265619, upload-time = "2026-08-28T21:52:57.645Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/ea/8e/0f752276f6d13efbd019ab6d90792e20d6272c44cda039dc5c6d27b91e7f/coverage-7.16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51e7d0e311d2fba3915f971236cbdd4ad821fc7a23988221c0b33c964b0eba22", size = 267734, upload-time = "2026-08-28T21:52:59.611Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/fa/02/4df3baef8029881c9d1a380859f2be73f90080d430def567d182e8566a35/coverage-7.16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0bb04ee77e557d7476471969d35fbbfb5fc8a4152e9409aa5811780c36d9b23e", size = 270156, upload-time = "2026-08-28T21:53:01.658Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/9f/30/ce10fdb74055ebbfb5c8a025d8845dc19c76e4b2c42bb5c755b56678990c/coverage-7.16.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c72c9b201dc0e8c2c8821d49858fd865010d08181bf877d2320971b6464ebfd5", size = 271279, upload-time = "2026-08-28T21:53:03.698Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/71/19/c7e1fc9504d90da848493bad4018dd235c713a80633e48c5f0a41b63d45e/coverage-7.16.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0fca700cae4635656668ba6e2b66a85aac9f2622d7b2bcf82e844c409eaa1313", size = 264677, upload-time = "2026-08-28T21:53:05.741Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/a4/f3/4021519dd41583ab396c81955387f927779641f6bac26818b6918a45aafc/coverage-7.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:584896fb8b650e999e24ef57e9513e482c12f8e15a73ee9d4584e23c99465867", size = 267610, upload-time = "2026-08-28T21:53:07.763Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/55/fc/df65aac93938d8f506434c8e96440c1d696f6be0a6a01d3c6bfe5d49403e/coverage-7.16.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:949eae7e0f562b1518355aaef4b03523e49a6d3fea12aa3542d9e36c863f8267", size = 265217, upload-time = "2026-08-28T21:53:09.786Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/32/2d/dc9a5e62715165fcb4c715f965f411e324917c9daeddde16536e9d36ce3f/coverage-7.16.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:64f0611ee05364fc85cc3e5bc371804117a76fd337720e6017332fc7c534257a", size = 268948, upload-time = "2026-08-28T21:53:11.866Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/8b/4e/fe73a5560f25fca52acda76fc1554f30de081793ae4de97e920f8ab161d7/coverage-7.16.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:050a291b3cfe5e0df5999ef2fa5a7aff6e2db329f069d47eb63f02bde2e7e96b", size = 264061, upload-time = "2026-08-28T21:53:13.996Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/b3/f7/bb78cc4b97085ebbd77fa18cbc25abfab462814efa3e2363b4e50885c775/coverage-7.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a336b1e2990a64f5c356a9b8380fb9c029d56c832b801255250c44d603271bfd", size = 266371, upload-time = "2026-08-28T21:53:16.233Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/aa/ec/84b4af5cd4ad498477b3bfb2217e47b048da919451053790efda66f7383c/coverage-7.16.0-cp314-cp314t-win32.whl", hash = "sha256:058631257350b31784ed43ceb808298b6f074edf4ebca4c7ce5082e6bf873a61", size = 225736, upload-time = "2026-08-28T21:53:18.632Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/7e/43/50fc0e6c675c3ef14895a74bab2d6120cb5d6f4b562a3d3f5046797758dc/coverage-7.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:ed35097438dfa980c1ec75bc83edf8acbe7a374d7007e571957a257fbd0e2fb3", size = 226570, upload-time = "2026-08-28T21:53:20.754Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/fc/24/9effce7bcd3c6eeb4da3561905837509e582dcdde7a7f07d6ef2c8512f76/coverage-7.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0466f4a5c0370461b7d8c7eb259d7d1db0b5756f13d66230b04d22a1d380ee11", size = 225879, upload-time = "2026-08-28T21:53:22.747Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/4a/2c/318e4379106bc8047ba235e3732ddc87d1b393ac3db9776f5405ff14f322/coverage-7.16.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:80d7d5d744a041f08637df743ac086204ec5acbcd8432a42b00b49e607358024", size = 223257, upload-time = "2026-08-28T21:53:25.376Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/81/4d/a5c54d9144e9db6505749758ba50a28be624148873751728a59cbb72d27a/coverage-7.16.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:c5feffce90c3d602e149de1c477578efc34dee5f069f9764cc15808ce01ee15c", size = 223596, upload-time = "2026-08-28T21:53:27.461Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/bc/97/38e93a10899c9315964c0a4e729b3e5867f8f46e977808f9c6fbda52525a/coverage-7.16.0-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:acadbf2f2a18d7f9c7f119ac798c00c540d7c79c93abd71ed648c87891303633", size = 254699, upload-time = "2026-08-28T21:53:29.715Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/fa/7a/acddda030b4630f68167f3daa94b41d22071847822a70d8178d43dcf678e/coverage-7.16.0-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4212cec9b42fd9929e70b462732fefd8b13406371871c82f3c14397499d6550b", size = 257614, upload-time = "2026-08-28T21:53:31.948Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/15/7e/225b182497c1ce6d3f0d76a3074a4dbc9f272300e92bb100df53b03de0aa/coverage-7.16.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c5a43cc0ef101637ae920a9eed24cf0549ef815621eae68b3ad577ec5a7ad2f", size = 259236, upload-time = "2026-08-28T21:53:34.291Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/2e/19/76641ddc50cb2410ebbd0ed7fe1052614d0e5612e802a2817521adb9febb/coverage-7.16.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c76a9b50a344261fe4a9bd20c322b48d3913cc48e8c37f78c21a596008296e68", size = 261433, upload-time = "2026-08-28T21:53:36.401Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/12/9e/5f89de8b7c2017f36b68b4e4a25940723a748b21474820bf61e8bce0891c/coverage-7.16.0-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80cf547379ad6b1878fd03b033b51188beab4b41824c96e7839e014a4cb947be", size = 255182, upload-time = "2026-08-28T21:53:38.496Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/1a/c1/ce94b2ec502e79775efb5efa22c741ebb0bd2be10bdd29650825ff57bdcb/coverage-7.16.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:4b1d09cb5d8dc2c7164450f5217e6f0717497de9c588806a0780d352abef904a", size = 257329, upload-time = "2026-08-28T21:53:40.87Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/86/8d/3f5374df3a6ca19ee5f98a6bd21dbb05f1e9d399bd9978e9821d260eab5e/coverage-7.16.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:cd1e85abed2d2499c16664137ac802356316f92b4e2bf3c150bdf0c45f5dd9ae", size = 255210, upload-time = "2026-08-28T21:53:43.393Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/8e/b8/1bc5751496d0be6fd9dde8ca547d9a8a9f07847856aba3f3ae5ac594cd81/coverage-7.16.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:360967a6fd77794c167529eec2d16ff8e38216110619d23acc3fd466a1648bee", size = 259442, upload-time = "2026-08-28T21:53:45.725Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/7a/dc/8aca78e47e1e6fcc761cd28a20daf4a84bd847a7369e2701a93ccfc3d1fd/coverage-7.16.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:92cbc2bf4f7f67c79f1d3ca4fe8c50faddf48e852a3d07eaaf02dc014889832f", size = 254618, upload-time = "2026-08-28T21:53:48.292Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/73/fd/787842cdf6ce16ac5c1bd8a26549bab3b3f27b02500075bc540dc7853bca/coverage-7.16.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cce4dc8528453128c6fae523b15f3887fbea1d4d7c9eb9639d3d4fdcbe570c73", size = 256541, upload-time = "2026-08-28T21:53:50.805Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/ef/79/8df302cbef373dd1f3401044cdb94dfc74517e5af2af27b4d0e721557e0e/coverage-7.16.0-cp315-cp315-win32.whl", hash = "sha256:5205baea687133613dced668a3d0168ea1479349615bfc255849a7944988c889", size = 225429, upload-time = "2026-08-28T21:53:53.177Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/85/87/5bad7ac45f76b3728ca211028ee561c2ede3ba44da401129e28bb8737291/coverage-7.16.0-cp315-cp315-win_amd64.whl", hash = "sha256:4fcb5f07a9b7083bfb715115d27ce263ba2b5b89dddeee536b295ba0e3c2c627", size = 225903, upload-time = "2026-08-28T21:53:55.535Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/cc/ea/67d84b11caf240f059ec313f616d82212df5004e8bc85802c1edfc50bb3d/coverage-7.16.0-cp315-cp315-win_arm64.whl", hash = "sha256:d568a8adcec0eda42ec23e5e65dfb8c184fc255120f9e99b484f7c869d923fb9", size = 225334, upload-time = "2026-08-28T21:53:57.769Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/65/21/a88349cce3ff720729b754916ac47e2e3646a8137552e4fa7cdd5967cc7f/coverage-7.16.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:3e8037e8213adf882e9d7eedd2c5c557933ab0b9632c42d98fe98ec9bcdb4025", size = 223980, upload-time = "2026-08-28T21:54:00.082Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/fd/02/4d54abf3e6a4d8b7675921b20e91163b1064a5a9dbefebb71c05065dd136/coverage-7.16.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:289f2ed4d56eebf029b649e7dfc3c1153b111962a75e294cdd8e4a1598a04cc3", size = 224276, upload-time = "2026-08-28T21:54:02.381Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/f6/39/10dbc96d95d20b9b041045d293480bd49e536180e93af62dd7662376284d/coverage-7.16.0-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b83f6ac575530783771c8dcf05284f7c8b5b12f1e7cb226d63445aac4497a3a", size = 265135, upload-time = "2026-08-28T21:54:04.558Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/e7/3b/6b326544afd1a8aef3a495bbae109a7ab5baf23e04a2741d8d64e2df2ba2/coverage-7.16.0-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c3ff6580f2dfc5bec34717b85b2e6cf5ec993b721e7bb58a794babd525a8178", size = 268216, upload-time = "2026-08-28T21:54:06.97Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/54/34/1dc8265f3ed990690e24d5f31ff79bc9fb9b25d54f9f89bebad5a6a8b7a1/coverage-7.16.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:507596cee23e9968b1934fe86d799b76166541af0a293930918b1b48a5c84bd2", size = 270772, upload-time = "2026-08-28T21:54:09.234Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/66/a7/3a8463713a402b44044ec832f4a76e442ce4b3a207804303f4d1dc1a9bb4/coverage-7.16.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edc2be98e6c55ccc5ff7832bb64f023a4b03dba39dfa84b850046cf08a8249b0", size = 271752, upload-time = "2026-08-28T21:54:11.701Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/25/3b/dd5e795cfbe1842f69899189089ae289a96d6a68de312960ea668542e33c/coverage-7.16.0-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9c0690994b84a15a53bdd39e0b2fdb539b22533820623eb86ba75b93760c645b", size = 265589, upload-time = "2026-08-28T21:54:14.12Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/1b/b6/fd90636cbd95cb018312f6ca1ca2bbd70fbe8e4ee6f3992fc36a4230364e/coverage-7.16.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:de24c62bf798940a14674a47489a81b79915ec4134f556d5199830e065225dd0", size = 268596, upload-time = "2026-08-28T21:54:16.303Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/91/10/ef2d59264f3b3b358cc5885ca375e6cdbda7c195e78304d5aae800a72d9d/coverage-7.16.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:69474d81f198774c9d2937599ca5da04c9e1c5de5032da23c607ce4960ce360e", size = 265072, upload-time = "2026-08-28T21:54:18.597Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/3f/5b/400891c364c0170408d172501b340b18611800f4c42d8fbb16f9f5497c24/coverage-7.16.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:72a0795cc6d34acc2b03dfeabdc82b61b72087f2737018b56ac92c1cf5446c54", size = 269768, upload-time = "2026-08-28T21:54:20.985Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/98/93/9792c80271df04d287d21ed5d662fd8fa58b1737888d817679b1ce5d2fab/coverage-7.16.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:d9a218d3f9c7d6916684ed5ba94f620661117a730e733cd6ef5e87accc5872eb", size = 265211, upload-time = "2026-08-28T21:54:23.344Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/81/67/5b8f827cfa6616e6bd7ba9397acfe7e3c4fd5b9fca4125511d5089f55d5a/coverage-7.16.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:49fa72ead28c8216f8916398a4f3c4669acb30a061822810ee20a727a1be2897", size = 267170, upload-time = "2026-08-28T21:54:25.85Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/5c/ee/c135d2d2cb617d744bc3e13c922f2fae66964494176ddef225dc4656bd2c/coverage-7.16.0-cp315-cp315t-win32.whl", hash = "sha256:27461af9f3ed7d2cf2411eb083784f87055ebf42211789ae3a216c48609bc743", size = 225731, upload-time = "2026-08-28T21:54:28.151Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/8a/4d/dc3d53eadf155916e183bf5dfacbfc4aa5bfb7f13b7da11c01caa7a05cbc/coverage-7.16.0-cp315-cp315t-win_amd64.whl", hash = "sha256:c5612cc20ca76abc883e50269af47c1494b42958bb63dbb9aa79729a1ab5f7d3", size = 226562, upload-time = "2026-08-28T21:54:30.42Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/2f/00/ac9da1a60a4e84c3ad0f7db4723fd327154a8f9add210c0dcd2db3ec5156/coverage-7.16.0-cp315-cp315t-win_arm64.whl", hash = "sha256:2ddaa9e2af4760a329d80008b7a3b4762fbb0dbcb169199360f9a5179c32f2dc", size = 225872, upload-time = "2026-08-28T21:54:32.806Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/b1/5a/234e8fadf85c3cc48cb31c247b9e8e0c7f06ece80f5b29f9b8c241f9da4c/coverage-7.16.0-py3-none-any.whl", hash = "sha256:245f7de6d023a5bba375dbec9f2e0869bfa26ac0cc639bbb7b4c814884000b73", size = 214977, upload-time = "2026-08-28T21:54:35.189Z"}
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -394,11 +394,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
sdist = {url = "https://files.pythonhosted.org/packages/6d/30/03b03951873a1a0ffc7e8ca0e10c15597b59e8d0e39260704cd2ea087bc4/filelock-3.32.4.tar.gz", hash = "sha256:2bde2e4cf732e0153406d8a7bc80620ecf5e621fe0d25e41143c4e3b4733ff30", size = 222126, upload-time = "2026-08-23T17:37:55.363Z"}
|
||||
sdist = {url = "https://files.pythonhosted.org/packages/0a/a0/50c2c0ce5e74d7721bbb1b19a26ebd339aac5878553a6e35308c2f31f935/filelock-3.32.5.tar.gz", hash = "sha256:f6a6a28f743f9b95ce19db5abe0f376f75eb56517dff21e1a4751e2657d3e83d", size = 222838, upload-time = "2026-08-31T18:56:34.729Z"}
|
||||
source = {registry = "https://pypi.org/simple"}
|
||||
version = "3.32.4"
|
||||
version = "3.32.5"
|
||||
wheels = [
|
||||
{url = "https://files.pythonhosted.org/packages/01/a4/9b63d595d748e3aff8812b65eacc1a2c4bd90b7c2012e08e72373b4835eb/filelock-3.32.4-py3-none-any.whl", hash = "sha256:22e58ca3b1ae3b98993b762d7338367ae64fe50252bf78d59da3bfebcdf1cedd", size = 99864, upload-time = "2026-08-23T17:37:53.913Z"}
|
||||
{url = "https://files.pythonhosted.org/packages/36/d2/b70a31e13d04456d28493f31d2aa087e99eeb2767ef0293b2625727ccb8c/filelock-3.32.5-py3-none-any.whl", hash = "sha256:142cd9fa77a872c5e78c62329a0d15278fadc686eb89e760017968961a4fd6b2", size = 100003, upload-time = "2026-08-31T18:56:33.078Z"}
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -411,7 +411,7 @@ dependencies = [
|
||||
]
|
||||
name = "id-configurator"
|
||||
source = {editable = "."}
|
||||
version = "0.2.0"
|
||||
version = "2026.9.7"
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
@@ -571,11 +571,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
sdist = {url = "https://files.pythonhosted.org/packages/ea/06/cf1564dcc2e2261c8c8c6c05628dc8b418943bdae2a4e58640ceb2f770fa/platformdirs-4.11.5.tar.gz", hash = "sha256:e8b31f4f8bcbbedef91a6b57a706255e4f148d2a4e01648382a0a47342539173", size = 34823, upload-time = "2026-08-27T21:36:37.46Z"}
|
||||
sdist = {url = "https://files.pythonhosted.org/packages/69/b7/802a56eca9f2fac455b8bab5375a2647b0f0e14a2cd63ef077de3c4a7658/platformdirs-4.11.7.tar.gz", hash = "sha256:4f41487eeeeeb07f3a6625e61d9bc0ae6809f92d3386dbd74392fbb76108104d", size = 35127, upload-time = "2026-09-01T13:35:10.502Z"}
|
||||
source = {registry = "https://pypi.org/simple"}
|
||||
version = "4.11.5"
|
||||
version = "4.11.7"
|
||||
wheels = [
|
||||
{url = "https://files.pythonhosted.org/packages/c7/12/6f3fcd5067a9cbf4f8664b32957973498da8b083455203c8d9cab83a725c/platformdirs-4.11.5-py3-none-any.whl", hash = "sha256:89f8d42695853b89c7170bd49bc3dc593f98a71e695ede88e06a3b247bc4563b", size = 23900, upload-time = "2026-08-27T21:36:36.227Z"}
|
||||
{url = "https://files.pythonhosted.org/packages/27/6e/80993e10a0482f630cef528635789233224f36b1ffd11592aa15d13ff9ce/platformdirs-4.11.7-py3-none-any.whl", hash = "sha256:8a02cb259042c79d1cd0450facc2fe6dc9d303ae7901afbe33bf8ea0b188cef6", size = 23938, upload-time = "2026-09-01T13:35:09.02Z"}
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -655,11 +655,11 @@ dependencies = [
|
||||
{name = "typing-inspection"}
|
||||
]
|
||||
name = "pydantic"
|
||||
sdist = {url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z"}
|
||||
sdist = {url = "https://files.pythonhosted.org/packages/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750, upload-time = "2026-08-28T14:04:00.916Z"}
|
||||
source = {registry = "https://pypi.org/simple"}
|
||||
version = "2.13.4"
|
||||
version = "2.13.5"
|
||||
wheels = [
|
||||
{url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z"}
|
||||
{url = "https://files.pythonhosted.org/packages/eb/47/c95ffc2009878c7aac0c5e08528022dcb885933252a88b5f170058014464/pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73", size = 472589, upload-time = "2026-08-28T14:03:59.136Z"}
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -667,55 +667,55 @@ dependencies = [
|
||||
{name = "typing-extensions"}
|
||||
]
|
||||
name = "pydantic-core"
|
||||
sdist = {url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z"}
|
||||
sdist = {url = "https://files.pythonhosted.org/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262, upload-time = "2026-08-28T10:01:31.677Z"}
|
||||
source = {registry = "https://pypi.org/simple"}
|
||||
version = "2.46.4"
|
||||
version = "2.46.5"
|
||||
wheels = [
|
||||
{url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z"}
|
||||
{url = "https://files.pythonhosted.org/packages/f5/37/5abe39a8372a61d3dc3c1338fc504281c01b32fdb3169cd7187153b56d3e/pydantic_core-2.46.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0", size = 2075885, upload-time = "2026-08-28T09:58:47.856Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/21/43/6323b1f8b217780454c61304bcd2b38ae4762f50754414124603ccc90bb2/pydantic_core-2.46.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff", size = 1922768, upload-time = "2026-08-28T09:58:49.58Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/0f/a3/c05ca796e1197618a774b01e596aeedfefc2f7d8c01ae3054e910b120e8a/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931", size = 1951241, upload-time = "2026-08-28T09:58:51.511Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/68/32/33bc39ac705c52cffc908e8389f9754fdb208aea5c69cceddf4eb3ce99af/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f", size = 2031975, upload-time = "2026-08-28T09:58:53.166Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/b0/70/2333e885c0f6a67bc105c5916965dac9b57f2718ee20d81d1a06a4ebdc13/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038", size = 2208542, upload-time = "2026-08-28T09:58:55.017Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/f7/ea/296debfb4264207bbda5936133892e027c0a58875ad53ebd512fba8ec3a2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f", size = 2264692, upload-time = "2026-08-28T09:58:56.767Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/d3/f2/9e4de77a6271e07a76d2d58b11c091a979c191ed2939bf80067568b369d2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1", size = 2066633, upload-time = "2026-08-28T09:58:58.531Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/8d/db/f9e9d0c97445987b2084823d5c240de88087338f04fc2cfaa2df186b8049/pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761", size = 2105235, upload-time = "2026-08-28T09:59:00.421Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/07/c5/79169b047b3b2c3e99e04bc76372af9637e0bf6db638274fa927df96369e/pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5", size = 2157367, upload-time = "2026-08-28T09:59:02.442Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/26/b5/ba6057afb7c291bd449f51b867f95aef2072941c4ce4e5c31d6ffd132d3b/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e", size = 2158420, upload-time = "2026-08-28T09:59:04.2Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/6e/28/2057abecaafdc22912afa819603a51f0a62d40643b7c4871c51721fea9be/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed", size = 2309588, upload-time = "2026-08-28T09:59:06.048Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/71/9d/881156dc404e27479c4246128d73538464cab4a239bec61995e227644c30/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519", size = 2341866, upload-time = "2026-08-28T09:59:08.539Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/5a/38/d66f443a259f84d13babdceae568e572b0ed26da17ca5d0a649ebb110a67/pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea", size = 1938580, upload-time = "2026-08-28T09:59:10.402Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/2c/1e/1d5371213f4cc9a7ed70c0bfcc7911de22311ee99a662a56077d7292d2ac/pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5", size = 2041980, upload-time = "2026-08-28T09:59:12.396Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/5a/48/4222d90b1c67568bace4dec6dca6271449c66de3595d72b6d098f5fde597/pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575", size = 1997213, upload-time = "2026-08-28T09:59:14.245Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/8e/8a/14596f2a8367da50cf7cbac48169ee5d9c8e11d486a3b527082384630c72/pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355", size = 2074081, upload-time = "2026-08-28T09:59:16.141Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/ae/d5/d8a4eb6d6c7f66b91dd37c576d76e9e60fba900caf5372c17bcf949febc2/pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e", size = 1920497, upload-time = "2026-08-28T09:59:18.065Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/8e/26/092079428f86e927e030b2c0ced87df69dbb1c875cdeaa67bf42ea2be746/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3", size = 1952130, upload-time = "2026-08-28T09:59:20.476Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/08/c3/8ec0e290a9ebaebd64047bf5fda94be835c6b1551b02437e4b76778fbcd7/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c", size = 2026371, upload-time = "2026-08-28T09:59:22.227Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/01/72/4fd20ad520fb8da0157f95b27a7eb05a72790ef08138e7701ac972c342ea/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21", size = 2202822, upload-time = "2026-08-28T09:59:24.277Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/31/b0/d16e0771206b29314f0d52198b720be21e8a99ab2bf11e3bc0d7c9cebdff/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f", size = 2262756, upload-time = "2026-08-28T09:59:26.608Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/2c/9b/59634b7ac631c63b2a37760eb6943af3e29573d6b59a4abc5e7f019d4cee/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f", size = 2068352, upload-time = "2026-08-28T09:59:29.044Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/08/7c/570abb1ad2155348dc754ea91be22e5aaa18eb6d69a6068f7c6f2679a6ed/pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a", size = 2104777, upload-time = "2026-08-28T09:59:30.95Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/8e/25/5bf74adc65a1ac5b7be3f6cb0bcb5433615c1598a801c19d830d84c98ded/pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821", size = 2156312, upload-time = "2026-08-28T09:59:32.604Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/90/6a/2ef38830675e050121040618135564ed56b860b45433b02d9b4ebece46f3/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2", size = 2150067, upload-time = "2026-08-28T09:59:34.453Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/90/ef/a7dbb03a14a64c2a4621f989c615ed9a892535a6cad938fc27079f919d80/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47", size = 2304516, upload-time = "2026-08-28T09:59:36.194Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/68/f8/6bb4c4b80e8a6fde1904c64a51c62a1d04fcdfa3ea521a66b2ddefa1d885/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a", size = 2335223, upload-time = "2026-08-28T09:59:37.931Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/2a/80/f46b8c681195190b2c1f1c7c0a81abce60663e987613e09ef64d433dd96b/pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074", size = 1934827, upload-time = "2026-08-28T09:59:39.836Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/f7/3c/60674207246bc0a4009d2391b7c7251c7159f279c8d2ab8aae8ef46f3dee/pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0", size = 2042648, upload-time = "2026-08-28T09:59:41.792Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/69/0c/117c562c7c1babdf44576b72a5e496906506c93690387ecfbca7c729ae2e/pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5", size = 1989652, upload-time = "2026-08-28T09:59:43.702Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/e8/66/9336ae58f9eb68c41d121894e52c4c89eccb07eb8f602a04ee9c3f37736a/pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7", size = 2065829, upload-time = "2026-08-28T09:59:45.364Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/c5/02/bc19b47a96c2d3109760711acf22369e56bd7e405ca52f7ade164d2ead57/pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3", size = 1905716, upload-time = "2026-08-28T09:59:47.18Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/52/a4/70b47c0509923dd98ccfed04fb3e32ea3849c82a0ff2205bb41009b43c00/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f", size = 1934216, upload-time = "2026-08-28T09:59:49.241Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/52/ab/aa03b65f7bb198585edf806b906c3223ecf1795543e39e23aec4cce27ad2/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7", size = 2010635, upload-time = "2026-08-28T09:59:51.692Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/3c/8b/0da06343f30b84ec549aafd309c6456223d5dc8bd36af504c573faad561d/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c", size = 2209369, upload-time = "2026-08-28T09:59:53.582Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/d6/5b/844c4defaa34a3df66eb9257087d121d70c201298b96abdf9f492fc2f1bf/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111", size = 2253238, upload-time = "2026-08-28T09:59:55.484Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/f4/64/a4e536cb16d7f61a7fd3120b46c577fc7fa7325992f69c4f52bc786d77d8/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829", size = 2065740, upload-time = "2026-08-28T09:59:58.038Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/5f/75/aaa38c6bc2d085f6605b34eabdc6a8a4e0b2e61fc9c8e6e52b28e97b3125/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa", size = 2087425, upload-time = "2026-08-28T09:59:59.898Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/55/ae/fcab4cfc39aba3689e1d20c8b5250ad280957022c09af2ed9cd585602a5e/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034", size = 2139306, upload-time = "2026-08-28T10:00:03.057Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/2d/f4/f1d03a4bc9d9acbc62f4d742b8a319af52f71885079868b2ff8e48a651ee/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184", size = 2144589, upload-time = "2026-08-28T10:00:05.645Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/83/f3/7a53bb1356de514a4cd295f25b6ac39237895620c0462d2592b76c16e114/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38", size = 2288882, upload-time = "2026-08-28T10:00:07.931Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/cd/94/5a81583660c175c59d49ffb09f4b3a44debeaf86a19fca664ae1cdd9ee32/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9", size = 2335210, upload-time = "2026-08-28T10:00:10.177Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/5a/9f/5d685c2693b972d1a59c998586e8823712b66603aeff47ee60a4bdaafd37/pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9", size = 1921180, upload-time = "2026-08-28T10:00:12.35Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/70/12/5c94ee16d65a37a15f9e869f5e6256df111154491173801a4c5e800ab548/pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290", size = 2020515, upload-time = "2026-08-28T10:00:14.774Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/63/19/67830dda664e6bdf9285ee2e40f355d0d7d6b92aa0c42e8d217bb8d33d36/pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f", size = 1989276, upload-time = "2026-08-28T10:00:16.984Z"}
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -790,11 +790,11 @@ dependencies = [
|
||||
{name = "filelock"}
|
||||
]
|
||||
name = "python-discovery"
|
||||
sdist = {url = "https://files.pythonhosted.org/packages/b2/8f/3c92c45737f654f2488ab3662b7604a55d3d35146d37c9ce80f5c95b95a6/python_discovery-1.5.3.tar.gz", hash = "sha256:e500eb24025fb7c4876c1fdcfbafd9028a10c71b661aee38cb6fb0de594518c1", size = 82477, upload-time = "2026-08-24T14:48:46.396Z"}
|
||||
sdist = {url = "https://files.pythonhosted.org/packages/91/96/0f93e27c9f60a650838f2118159aa115fd5732c0716247917b7ba7ede665/python_discovery-1.6.0.tar.gz", hash = "sha256:6393b4eae1be8b2182670635e7baff89ac21cb9f8e86fd1ff40c7b1144febb4c", size = 82849, upload-time = "2026-08-28T17:30:02.366Z"}
|
||||
source = {registry = "https://pypi.org/simple"}
|
||||
version = "1.5.3"
|
||||
version = "1.6.0"
|
||||
wheels = [
|
||||
{url = "https://files.pythonhosted.org/packages/30/12/823d9a321904ccfd2969a24b84fdfd1e6614c707ec569c62879bf1dbc6c5/python_discovery-1.5.3-py3-none-any.whl", hash = "sha256:8305296358f1aa2ed302a25b84be7df84fef8ca47c7dce2da63cb7325333044e", size = 38290, upload-time = "2026-08-24T14:48:45.305Z"}
|
||||
{url = "https://files.pythonhosted.org/packages/43/5e/21abf578182fb15006a57faf3711a1e659e29d600d19b6e557eae908c81d/python_discovery-1.6.0-py3-none-any.whl", hash = "sha256:d4e244cf17b8b29819ed78003d55fbacf86eda23425b075454fff9271b79377a", size = 38451, upload-time = "2026-08-28T17:30:01.236Z"}
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -909,11 +909,11 @@ dependencies = [
|
||||
{name = "virtualenv"}
|
||||
]
|
||||
name = "tox"
|
||||
sdist = {url = "https://files.pythonhosted.org/packages/ec/ae/05210c695efa7349af65aeb988604b15520eade3b6e2d43bdf44ce69b4b3/tox-4.60.1.tar.gz", hash = "sha256:2611b77ebe06fa571ae2f4176eaf644115d08891befb9c7cb705cb038d5ab38e", size = 303833, upload-time = "2026-08-25T05:39:00.934Z"}
|
||||
sdist = {url = "https://files.pythonhosted.org/packages/c3/8b/2e25d11c05e006438f8a1f20f8814aeb576ec8b3bf157f68454d2dcc09bf/tox-4.61.2.tar.gz", hash = "sha256:463b6eeaaf0929d60b4f7a87b26f4a7a764fe9832a74bf50b4e9d259b6a8e905", size = 306825, upload-time = "2026-08-31T20:19:53.614Z"}
|
||||
source = {registry = "https://pypi.org/simple"}
|
||||
version = "4.60.1"
|
||||
version = "4.61.2"
|
||||
wheels = [
|
||||
{url = "https://files.pythonhosted.org/packages/83/d8/5ca808e8b2d98ac55e9141309c63b273caec25eb5bdaf3d7bf6d809903bc/tox-4.60.1-py3-none-any.whl", hash = "sha256:6f23a6417ad761b17c047e14b6e278968c09eb9b1fc00025269f82ec8327bc75", size = 226880, upload-time = "2026-08-25T05:38:59.194Z"}
|
||||
{url = "https://files.pythonhosted.org/packages/50/c9/3e78716c0b994a75f064d36ae56d707435dd147d6945914aaa6372fe3ccf/tox-4.61.2-py3-none-any.whl", hash = "sha256:262efa52b735004dcef04ae4c934614ec4e5c61319a14d1e762c18db7fdb82c3", size = 227984, upload-time = "2026-08-31T20:19:51.709Z"}
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -968,11 +968,11 @@ dependencies = [
|
||||
{name = "shellingham"}
|
||||
]
|
||||
name = "typer"
|
||||
sdist = {url = "https://files.pythonhosted.org/packages/ae/40/4a3db7990d1f62a53182aa96eaef57aeb2886a27f90a195bc66713565d31/typer-0.27.1.tar.gz", hash = "sha256:a79bef8469a79c45498e7b814ecf8d603cc7644e9acbd9e19cac0334240b18df", size = 203994, upload-time = "2026-08-03T14:41:03.438Z"}
|
||||
sdist = {url = "https://files.pythonhosted.org/packages/16/f7/57713ba479fd405eb76de31404b2c744c289e336b2d999511ebf51e496f7/typer-0.27.2.tar.gz", hash = "sha256:269b7eb9d3c202ca84b4bc9618cb04ebb43d3d4d1e567e4c768607232c05f945", size = 204045, upload-time = "2026-08-28T10:26:55.046Z"}
|
||||
source = {registry = "https://pypi.org/simple"}
|
||||
version = "0.27.1"
|
||||
version = "0.27.2"
|
||||
wheels = [
|
||||
{url = "https://files.pythonhosted.org/packages/43/89/9518bc0c3929bee36b3a4a8e3daddd6e03f92f9961c66d4983b837160543/typer-0.27.1-py3-none-any.whl", hash = "sha256:53150287edd11baeb4e4722c8e394fcdf8181c0ae89485cba8d25c778d5edd56", size = 122874, upload-time = "2026-08-03T14:41:04.391Z"}
|
||||
{url = "https://files.pythonhosted.org/packages/dc/bf/205d0004930ede8f542fb58f601526fccf4ae7626075ca1e6c4de5d3d652/typer-0.27.2-py3-none-any.whl", hash = "sha256:b3a5fc4342d5fc8fda8fc3010b1cf117e9249aab7fae800c2eff62fd3842d97d", size = 123130, upload-time = "2026-08-28T10:26:53.752Z"}
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1007,28 +1007,28 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "uv"
|
||||
sdist = {url = "https://files.pythonhosted.org/packages/f7/ab/790e25d2c685f5a41b9567001a43fbe049a595c1b08b8a93bf82d5fc704b/uv-0.12.7.tar.gz", hash = "sha256:4b320f84763a80308fd830ecf5c4c44505a8ed910fe265c5977d0a3727cfcd55", size = 7107999, upload-time = "2026-08-27T22:13:22.978Z"}
|
||||
sdist = {url = "https://files.pythonhosted.org/packages/48/92/167ad4b0795530383a84560dc8bc8c0afab6b6fb10baba34b2a257fa6894/uv-0.12.10.tar.gz", hash = "sha256:27e8350faffb35d8ecaa5ed39236709809bb9a883f1d11d13c0964d6051b2384", size = 7144864, upload-time = "2026-09-04T23:14:21.301Z"}
|
||||
source = {registry = "https://pypi.org/simple"}
|
||||
version = "0.12.7"
|
||||
version = "0.12.10"
|
||||
wheels = [
|
||||
{url = "https://files.pythonhosted.org/packages/91/63/cc236f80a868b28525e0f7aa61f7ee998a0540ccba8cc4d01b2d42fc7838/uv-0.12.7-py3-none-linux_armv6l.whl", hash = "sha256:d568fd3448c24354753fd8333c978aeeeb6b51db4b100e234461a7eb50882fae", size = 22215689, upload-time = "2026-08-27T22:12:36.485Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/ba/c5/40f4a5736d2f795688977b3b8bf841daca1e9c93b358778b7ed3dcd747fe/uv-0.12.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ec5b437aa60e8c94da263ad709d0bf6c8f268ac81f305d89c6115badd7d1cbe7", size = 20471214, upload-time = "2026-08-27T22:12:39.147Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/e5/3e/5946d33de437467190b51d7d9f9e74a53628782565d50b64f6051050f482/uv-0.12.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:016fe4b9a2e0d2a35b17b6c3efbb45b929189c5b4b37aa921265265ccfe42cc1", size = 17317478, upload-time = "2026-08-27T22:12:41.58Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/2e/5b/0e42dd88d9928b51a4e85d89d8aed46933951187b508ae68c145b06e3334/uv-0.12.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:fe9a871bd638ee6d2fd73bf40c2ee98153e44d06f796a03fcecf9d12b36d42d8", size = 21379352, upload-time = "2026-08-27T22:12:44.123Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/8f/5c/b6417812ef87fc713ec368b1001b41c78eaedbfa521789db7d103b56c500/uv-0.12.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:1014a13854c45eb1daa9a32602e0f4d07f3edd298826d3e8d22740eed60a7c95", size = 21686985, upload-time = "2026-08-27T22:12:47.686Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/69/23/a6284df1dc4233fea564227f77c402969b4f6aebcb647826ce9e82a413de/uv-0.12.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0ad3e91cc911596bb54197057853b64b36a066462d8f2fc4d4f60b61b707ffa8", size = 21744177, upload-time = "2026-08-27T22:12:50.572Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/fb/e1/96526f3ea72667352e1a83dff3cbb8701dbfc830aafb3ac9dd532d4fb827/uv-0.12.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6d4bd67b488ef2766cfa885947c1093c18caf5d665ba9156963ad0241f196e9", size = 22527940, upload-time = "2026-08-27T22:12:53.479Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/39/7b/816e7c4ef1b59faf295d4eec133b15d6d3a227dc4bc341dceccc31a3ba15/uv-0.12.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:debeccc5eca0063cd922bc67caa4a8c0df5f69090179866ed17fa7264905bda2", size = 23630281, upload-time = "2026-08-27T22:12:55.941Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/ac/63/03d00b74c5582163b8c544b2dc39396388d904a13118303fb2c32ece2811/uv-0.12.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36c8f93d182b766b9ed4a9c1da5ec0f7dc9f934887df3404d996f66321fd18d5", size = 23345465, upload-time = "2026-08-27T22:12:58.726Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/8a/01/616cc5f80952eef6fa48c5dbd6ea8aeed04a494e7dfbe9f5f65cfcf85ad9/uv-0.12.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4545e87c7ac64af317d8daffd279e23e93b0e05035662363033d3525923339d2", size = 20058968, upload-time = "2026-08-27T22:13:01.239Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/66/23/051acc24b21bbf0e67bc00e2b4239892e37cf39fdd6428fc246a2ae62b79/uv-0.12.7-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:56a5730f8eff477501b3276a0059c2c2843302d5d4a6cc10f993a5cd66ddace8", size = 19450815, upload-time = "2026-08-27T22:13:03.636Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/42/a4/a84adae83d6891401284ec4929ea7892c7efc5484cb53283147fbb4881b6/uv-0.12.7-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:d83419298e202f56e381cef6406b519b9336e58fc90a559617d86576a3d8a4e8", size = 22402981, upload-time = "2026-08-27T22:13:06.247Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/8a/35/6fcdff1292ba1cc027d1d741531dfc4d19f92e1312262833beacf3b7fbe8/uv-0.12.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:fc57436f2f012b885454f465dbf077f573745f0d4275a6a38194203b67e94ea4", size = 22535567, upload-time = "2026-08-27T22:13:08.802Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/2b/ea/7ac04384642a4ba808b20a94cb09e0f4b4a6aef6cf9671858be0567c153a/uv-0.12.7-py3-none-musllinux_1_1_i686.whl", hash = "sha256:b2bd0f25f17f0000a2415347471e713cd1597f4525cd3412d17875b131f4b1ac", size = 21667314, upload-time = "2026-08-27T22:13:11.197Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/e1/c8/348089faf45a3c24c1d5a5ee3957cd1c7fb4a6aef1d9646114a4b7cc9c7a/uv-0.12.7-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:ff33305718665c6fba25efdd260c67a6bd500c665e3d5d61059612791ca10c90", size = 22919105, upload-time = "2026-08-27T22:13:13.596Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/3e/7f/5fb8ed68270666ed675daead854bde3b14d7ab42c2b7c0d234b5ea8ea928/uv-0.12.7-py3-none-win32.whl", hash = "sha256:3ac3321ccd6097dbef154d27044e0762a67b2f6eb017dcc65be6574f4671fb0d", size = 19880506, upload-time = "2026-08-27T22:13:16.072Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/e7/97/40a91354862028e8f8e547ac27c6acd7b7cc67b9003acc28596d9e90cd6b/uv-0.12.7-py3-none-win_amd64.whl", hash = "sha256:277d326d7e63b912f3425c6e6d7d5d49f21b43d080d21859ff3c6819353f1847", size = 18085016, upload-time = "2026-08-27T22:13:18.352Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/52/69/6cdd022fff6a8d8f5c9af2686f61f2605ddd03b5709efb73df6e08f40249/uv-0.12.7-py3-none-win_arm64.whl", hash = "sha256:95c3a4fa65e72bab3ca1b4c8ce18fbe784cf3137e9f9234588b69d09b341a4a7", size = 19522843, upload-time = "2026-08-27T22:13:20.633Z"}
|
||||
{url = "https://files.pythonhosted.org/packages/94/d1/b3752f00e67e5bf141adec0225c62a17c1cf3fb524db463c541c080f1237/uv-0.12.10-py3-none-linux_armv6l.whl", hash = "sha256:efbbc586d40014b9dd3bc37a2ad5803d12919458f0f9d16f122db5d91626f5ab", size = 22182923, upload-time = "2026-09-04T23:13:31.502Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/ab/4d/ef846ec0a4dfa8d4f47b96a87a1e331143be5a045c28c8b4b0374946455f/uv-0.12.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7e3a70d9dff95481afccd7d41e7ec42fdc230d53487b3e7e633f9b35610401b5", size = 20494379, upload-time = "2026-09-04T23:13:34.339Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/04/2e/5614e9a940fe1bf291342410b2cde9fe35a1430135ae7434ea8fd67033f8/uv-0.12.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5dc26c73826d2119292d49d71c5a9d5ba9dd97dd02459d816b8226f47f3dc6bd", size = 17238363, upload-time = "2026-09-04T23:13:36.905Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/d1/03/e71936935b8ac26390b280e102b9db823e90adb1134f7e226538054fdece/uv-0.12.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:bd0afae6918795c6a61a649e64d735e339f3d62f8310235da6f19eb3f66f323b", size = 21590025, upload-time = "2026-09-04T23:13:39.588Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/b2/c0/c6388a722fb9db8adf8ec8eca762547b5a06ccbd1176571dc584636af5f1/uv-0.12.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:c1fd0fcabc18dccaf541ca44a061c245045c937bec75c0491e94d59cd33ca97f", size = 21677222, upload-time = "2026-09-04T23:13:42.218Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/9d/c6/96698aed29f25ff289b28b6312c72e260c56f7e1de2e3f690788bddea19e/uv-0.12.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de4b06934bd025ceeeb48af5c6f9a82407a2fe2b5b35e456cfb3198ef5029de0", size = 21712683, upload-time = "2026-09-04T23:13:44.927Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/df/98/5871a2bff93bd6118ae877b874a09202d39e2831af37f56e2070d5c4bc87/uv-0.12.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b610e8cf7c6d51dc119d352e699b674534d0de327b4d2a67eb5d71ec364b8970", size = 22331081, upload-time = "2026-09-04T23:13:47.833Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/3d/b6/d690f34901bdc5094645cf75a00381cfb8f1f28ee87b7089af761094bb6f/uv-0.12.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f43c06b19137bb05877b0ec59c4932878db5d1aeb6f7b5c33259c22e0359c4fe", size = 23785643, upload-time = "2026-09-04T23:13:51.111Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/90/da/168a8c69f60b9a2c90afb0b9211911ca42a81300c098d545da911bd4ef8f/uv-0.12.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0334577671a65b621faddfdf5f9e9dd4eb08eaa7231c4405c4a7f4f17d224d9f", size = 23337759, upload-time = "2026-09-04T23:13:54.163Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/f0/41/f0e14ba1f881f7126152dcaf04119b3a2bd7566ba1cac1d22834bd134cbf/uv-0.12.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7d6248ad9f2d282fea795f248da8fa666ab382df50d8385bd98e01049440a0c", size = 19978237, upload-time = "2026-09-04T23:13:56.994Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/7a/20/acebfb5fc01b29cce5e68b4824fa451d5fb1e569bb2579bcc7233583a894/uv-0.12.10-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:5c0bcf0598742eb417f1aa41ec82f0d89bd7b7f42d0a519bea495a9419d8d8ee", size = 19402684, upload-time = "2026-09-04T23:13:59.579Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/49/ee/c40dc8b7b143a6f7843994b1b8a13aa8859d8404973704cd6bad7a26ac8d/uv-0.12.10-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:0ac914e9750bb1de2518d444cc1cff084f4eff3eefe9cdd5814b775443a16df1", size = 22450726, upload-time = "2026-09-04T23:14:02.316Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/c0/ec/b5df49f571b58e694ca8c3ddadd89939036ac0fe7ddaadc7600225ea8585/uv-0.12.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:46ca83d33bf87a67d0c5aa5b64f5576fc5c8e700a0ecf4900d409f8a1a1c9810", size = 22571602, upload-time = "2026-09-04T23:14:05.088Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/4a/3e/f0335142e8a4c6ddc5117dcc9b5fd55c83cdd8e20a7f3be9584282dbfb64/uv-0.12.10-py3-none-musllinux_1_1_i686.whl", hash = "sha256:c49cd1f6e5ded1bd844119da8b184d5c044ae51441c476af2382fdb8af99edb8", size = 21499938, upload-time = "2026-09-04T23:14:08.109Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/98/3e/991f08fcab1706175ae30eda6ef483223511679c56fe1032b919ca3ea391/uv-0.12.10-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:a34e1be296e6e30fdbfee9b84a45d082ce7df4dd4e588ebcfc44249505c10f02", size = 22879435, upload-time = "2026-09-04T23:14:11.1Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/c4/80/8db01aabf9937cbc228003392ee9fe542c4dad5c3c5b26f2e1f136d8e463/uv-0.12.10-py3-none-win32.whl", hash = "sha256:f7f2bc56fbd367b1066e8461a82e51e8886bbab52b1f8d1793af8ce52232348c", size = 19873629, upload-time = "2026-09-04T23:14:13.755Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/a6/88/e980985d93283c546374b6b9dfa486a28091c4c056226711e31bb0f722d1/uv-0.12.10-py3-none-win_amd64.whl", hash = "sha256:2ad71395c31b5db20c56327f62ee0299f75833308d4413284aba87324d092afa", size = 18084435, upload-time = "2026-09-04T23:14:16.323Z"},
|
||||
{url = "https://files.pythonhosted.org/packages/84/10/fa542546044f783060d09a624f9964b595c0a0713cf2d89ade69c6667d8b/uv-0.12.10-py3-none-win_arm64.whl", hash = "sha256:4f097c6b7f63eceb3faf5102009f9a8cc22fd7d8a341858d2fd540f46a592f7a", size = 19598033, upload-time = "2026-09-04T23:14:18.829Z"}
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1039,9 +1039,9 @@ dependencies = [
|
||||
{name = "python-discovery"}
|
||||
]
|
||||
name = "virtualenv"
|
||||
sdist = {url = "https://files.pythonhosted.org/packages/1d/60/fc54e876e34f94dd0cf0185aaecfd4bfa906653f003d9b2fb21428642fca/virtualenv-21.7.5.tar.gz", hash = "sha256:a73c4246fba3c8901ff9717399f466e00eeca5a3834981f1a6ebb4f1e94de2f8", size = 5346743, upload-time = "2026-08-25T05:39:16.14Z"}
|
||||
sdist = {url = "https://files.pythonhosted.org/packages/08/1c/69faa2e6a83484e2a8227bce5cfaa183941c5720f99c48f204931d286b07/virtualenv-21.7.8.tar.gz", hash = "sha256:1dc49c790072a9072cb1803f9bd62aa69cd583077cada32390f75505cdc64c9b", size = 5347580, upload-time = "2026-09-01T13:36:13.894Z"}
|
||||
source = {registry = "https://pypi.org/simple"}
|
||||
version = "21.7.5"
|
||||
version = "21.7.8"
|
||||
wheels = [
|
||||
{url = "https://files.pythonhosted.org/packages/ca/d8/401141bf45637be916c86d325bd821c5838c7eff83294b934cd94e774e4f/virtualenv-21.7.5-py3-none-any.whl", hash = "sha256:e36ca889510ab6cb0b1dca93c59e5431dd4422a3c88f487358d470c90af8c07a", size = 5324697, upload-time = "2026-08-25T05:39:14.229Z"}
|
||||
{url = "https://files.pythonhosted.org/packages/9f/34/88d507d4a4030fa559788de9c690a214f9a4053aa1d91cfb60e9b36127c2/virtualenv-21.7.8-py3-none-any.whl", hash = "sha256:3040eb3cbf5d32b10ffd57d167e6a162237ad82ba7d8cf1400a1efed593d85ac", size = 5324617, upload-time = "2026-09-01T13:36:11.248Z"}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user