Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
af7baaa
update typing
dholth Mar 11, 2026
dce47ed
ability to generate "packages.whl" section
dholth Mar 11, 2026
4f19f29
ignore some files for ruff; remove bare except: in conftest
dholth Mar 12, 2026
d0ae208
draft repodata v3 implementation
dholth Mar 12, 2026
5a12faf
fix naming issue (#272)
ForgottenProgramme Mar 27, 2026
a083728
Update conda_index/cli/__init__.py
dholth Mar 29, 2026
da5a3d6
Merge remote-tracking branch 'origin/main' into 262-wheel
dholth Mar 30, 2026
573da94
Merge remote-tracking branch 'origin/main' into 262-wheel
dholth Mar 31, 2026
9dec51e
type hint
dholth Mar 31, 2026
237264d
adjust for no top-level "packages.whl" key
dholth Mar 31, 2026
f054c61
add news
dholth Mar 31, 2026
6c72e94
Merge branch 'main' into 262-wheel
dholth Apr 1, 2026
5331b50
Apply suggestion from @dholth
dholth Apr 2, 2026
602963b
add typed index_shards_2 method
dholth Apr 2, 2026
96f0b03
add indexed_shards_2 to sqlitecache
dholth Apr 2, 2026
9cfec13
consolidate v3 handling in ChannelIndex
dholth Apr 2, 2026
9426ae3
rename cli option
dholth Apr 2, 2026
5855072
move indexed_shards() to superclass
dholth Apr 2, 2026
d9e6b72
update news
dholth Apr 2, 2026
7275205
Merge remote-tracking branch 'origin/main' into 262-wheel
dholth Apr 2, 2026
8f37fcd
remove unused import
dholth Apr 2, 2026
5cfe9d7
tidy v3 logic and add typing
dholth Apr 2, 2026
58bd3f2
remove unused import
dholth Apr 2, 2026
d148cb4
no .whl for indexed_shards()
dholth Apr 2, 2026
160196e
fix test
dholth Apr 2, 2026
1f0b44d
Update news
dholth Apr 6, 2026
136b07c
show repodata-with-wheels API
dholth Apr 6, 2026
9c98ee6
Fix key format for wheel records
danyeaw Apr 7, 2026
e0d9c18
Update conda_index/index/__init__.py with comment
dholth Apr 7, 2026
ea7a884
wrap lines
dholth Apr 7, 2026
e2ca1e8
Update conda_index/index/cache.py
dholth Apr 7, 2026
cc28205
Apply suggestion from @danyeaw
dholth Apr 7, 2026
40d0054
Apply suggestion from @danyeaw
dholth Apr 7, 2026
722d6c5
package_section_for_path() replaces endswith()
dholth Apr 7, 2026
1db53cd
compare whl section from output
dholth Apr 7, 2026
f51a29f
Support CEP 21 run exports in shards (#275)
dholth Apr 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions conda_index/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def update_index(
progress=False,
current_index_versions=None,
write_run_exports=False,
repodata_v3=False,
):
import os

Expand Down Expand Up @@ -49,4 +50,5 @@ def update_index(
subdirs=ensure_list(subdir),
current_index_versions=current_index_versions,
write_run_exports=write_run_exports,
repodata_v3=repodata_v3,
)
11 changes: 11 additions & 0 deletions conda_index/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,15 @@
default=False,
show_default=True,
)
@click.option(
"--repodata-next/--no-repodata-next",
help="""
EXPERIMENTAL. Write CEP-XXXX v3 repodata layout with all package
records under a top-level v3 key.
""",
default=False,
show_default=True,
)
Comment thread
dholth marked this conversation as resolved.
def cli(
dir,
patch_generator=None,
Expand All @@ -210,6 +219,7 @@ def cli(
db_url="",
html_dependencies=False,
update_only=False,
repodata_next=False,
):
logutil.configure()
if verbose:
Expand Down Expand Up @@ -269,6 +279,7 @@ def cli(
cache_kwargs=cache_kwargs,
html_dependencies=html_dependencies,
update_only=update_only,
repodata_v3=repodata_next,
)

if update_cache is False:
Expand Down
164 changes: 152 additions & 12 deletions conda_index/index/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,10 @@
import sys
import time
from concurrent.futures import Executor, ProcessPoolExecutor, ThreadPoolExecutor
from contextlib import nullcontext
from datetime import datetime, timezone
from os.path import basename, getmtime, getsize, isfile, join
from pathlib import Path
from typing import Iterable
from typing import TYPE_CHECKING, Iterable
from uuid import uuid4

import msgpack
Expand All @@ -36,6 +35,27 @@
from . import rss, sqlitecache
from .fs import FileInfo, MinimalFS

if TYPE_CHECKING:
from typing import Any, NotRequired, TypedDict

from .cache import IndexedPackages, IndexedShard

V3Section = TypedDict(
"V3Section",
{"tar.bz2": dict, "conda": dict, "whl": dict},
)

# in this style because "packages.conda" is not a Python identifier
ShardDict = TypedDict(
"ShardDict",
{
"packages": dict[str, dict[str, Any]],
"packages.conda": dict[str, dict[str, Any]],
"v3": NotRequired[V3Section],
},
)


log = logging.getLogger(__name__)

# zstd -T0 -b15 -e17 repodata.json
Expand Down Expand Up @@ -111,6 +131,7 @@ def update_index(
write_zst=False,
write_run_exports=False,
html_dependencies=False,
repodata_v3=False,
):
"""
High-level interface to ``ChannelIndex``. Index all subdirs under
Expand Down Expand Up @@ -147,6 +168,7 @@ def update_index(
write_zst=write_zst,
write_run_exports=write_run_exports,
html_dependencies=html_dependencies,
repodata_v3=repodata_v3,
)

channel_index.index(
Expand Down Expand Up @@ -177,6 +199,7 @@ def _make_seconds(timestamp):
)
CHANNELDATA_VERSION = 1
RUN_EXPORTS_VERSION = 1
REPODATA_REVISION_V3 = 3
Comment thread
dholth marked this conversation as resolved.
REPODATA_JSON_FN = "repodata.json"
REPODATA_FROM_PKGS_JSON_FN = "repodata_from_packages.json"
REPODATA_SHARDS_FN = "repodata_shards.msgpack.zst"
Expand Down Expand Up @@ -252,15 +275,17 @@ def _apply_instructions(subdir, repodata, instructions, new_pkg_fixes=None):
for key in ("packages", "packages.conda"):
if key == "packages.conda" and fn.endswith(CONDA_PACKAGE_EXTENSION_V1):
fn = fn.replace(CONDA_PACKAGE_EXTENSION_V1, CONDA_PACKAGE_EXTENSION_V2)
if fn in repodata[key]:
repodata[key][fn]["revoked"] = True
repodata[key][fn]["depends"].append("package_has_been_revoked")
records = repodata.get(key, {})

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

repodata patch doesn't work with wheel or v3

if fn in records:
records[fn]["revoked"] = True
records[fn]["depends"].append("package_has_been_revoked")

for fn in instructions.get("remove", ()):
for key in ("packages", "packages.conda"):
if key == "packages.conda" and fn.endswith(CONDA_PACKAGE_EXTENSION_V1):
fn = fn.replace(CONDA_PACKAGE_EXTENSION_V1, CONDA_PACKAGE_EXTENSION_V2)
popped = repodata[key].pop(fn, None)
records = repodata.get(key, {})
popped = records.pop(fn, None)
if popped:
repodata["removed"].append(fn)
repodata["removed"].sort()
Expand Down Expand Up @@ -397,6 +422,7 @@ def __init__(
upstream_stage: str = "fs",
cache_kwargs=None,
update_only=False,
repodata_v3=False,
):
if threads is None:
threads = MAX_THREADS_DEFAULT
Expand Down Expand Up @@ -430,6 +456,7 @@ def __init__(
self.write_current_repodata = write_current_repodata
self.upstream_stage = upstream_stage
self.update_only = update_only
self.repodata_v3 = repodata_v3

self.cache_kwargs = cache_kwargs

Expand Down Expand Up @@ -717,13 +744,29 @@ def index_subdir_shards(self, subdir, verbose=False, progress=False):

(self.output_root / subdir).mkdir(parents=True, exist_ok=True)

for name, shard in cache.indexed_shards():
shard_data = compressor.compress(sqlitecache.packb_typed(shard))
shard_hash = hashlib.sha256(shard_data).digest()
v3_data = {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

collecting all packages to produce statistics for the repodata_revisions dict; would rather skip and lie about numbers and timestamps.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the concern processing time? If so, could this be an elective post processing function?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see. It is part of the spec. I am not sure what the intent of this spec provides.

"tar.bz2": {},
"conda": {},
"whl": {},
}

for shard in cache.indexed_shards_2():
repodata_shard = self._indexed_shard_to_repodata(shard)
shard_bytes = compressor.compress(sqlitecache.packb_typed(repodata_shard))
shard_hash = hashlib.sha256(shard_bytes).digest()
output_path = self.output_root / subdir / f"{shard_hash.hex()}.msgpack.zst"
if not output_path.exists():
output_path.write_bytes(shard_data)
shards[name] = shard_hash
output_path.write_bytes(shard_bytes)
shards[shard.name] = shard_hash

if self.repodata_v3:
for section, records in repodata_shard["v3"].items():
v3_data[section].update(records)

if self.repodata_v3:
shards_index["info"]["repodata_revisions"] = [
self._make_repodata_revision_data(v3_data)
]

return shards_index

Expand All @@ -750,13 +793,108 @@ def index_subdir(self, subdir, verbose=False, progress=False):
"removed": [], # can be added by patch/hotfix process
}

if self.repodata_v3:
v3_packages = self._extract_indexed_packages_v3(indexed_packages)
new_repodata["v3"] = v3_packages
new_repodata["packages"] = {}
new_repodata["packages.conda"] = {}
new_repodata["info"]["repodata_revisions"] = [
self._make_repodata_revision_data(v3_packages)
]

if self.base_url:
# per https://github.com/conda-incubator/ceps/blob/main/cep-15.md
new_repodata["info"]["base_url"] = f"{self.base_url.rstrip('/')}/{subdir}/"
new_repodata["repodata_version"] = 2

return new_repodata

@staticmethod
def _v3_key_for_path(path: str) -> str | None:
for extension in (".tar.bz2", ".conda", ".whl"):
if path.endswith(extension):
return path[: -len(extension)]
return None

def _extract_indexed_packages_v3(
self, indexed_packages: IndexedPackages
) -> V3Section:
"""
Return all packages from IndexedPackages as the "v3": {...} section.
"""
v3: V3Section = {
"tar.bz2": {},
"conda": {},
"whl": {},
}
for section, records in (
("tar.bz2", indexed_packages.packages),
("conda", indexed_packages.packages_conda),
("whl", indexed_packages.packages_whl),
):
for filename, record in records.items():
Comment thread
dholth marked this conversation as resolved.
# Per draft wheel-in-conda work, key is conda-like so that some
# conda-like parsing can occur on the key only. So we derive the
# key here. `record["fn"]` contains the filename or URL.
if section == "whl":
name = record.get("name")
version = record.get("version")
build = record.get("build")
if name is None or version is None or build is None:
log.warning(
"%s: v3 whl records require name, version, and build; skipping",
filename,
)
continue
key = f"{name}-{version}-{build}"
else:
key = self._v3_key_for_path(filename)
if key is None:
log.warning("%s has unsupported package extension", filename)
continue

v3[section][key] = record

return v3

def _indexed_shard_to_repodata(self, indexed_shard: IndexedShard) -> ShardDict:
if self.repodata_v3:
shard_data: ShardDict = {
"packages": {},
"packages.conda": {},
"v3": self._extract_indexed_packages_v3(indexed_shard),
}
else:
shard_data = {
"packages": indexed_shard.packages,
"packages.conda": indexed_shard.packages_conda,
}
return shard_data

@staticmethod
def _make_repodata_revision_data(
revision_data: dict[str, dict[str, dict]],
) -> dict[str, int | None]:
"""
Return { "revision": 3, ... } dict with package statistics derived from
revision_data, which is similar to monolithic repodata.
"""
timestamps = []
n_packages = 0
for section_records in revision_data.values():
n_packages += len(section_records)
for record in section_records.values():
timestamp = record.get("timestamp")
if isinstance(timestamp, (int, float)):
timestamps.append(int(timestamp))

return {
"revision": REPODATA_REVISION_V3,
"n_packages": n_packages,
"oldest": min(timestamps) if timestamps else None,
"newest": max(timestamps) if timestamps else None,
}

def extract_subdir_to_cache(
self,
subdir: str,
Expand Down Expand Up @@ -826,7 +964,7 @@ def extract_subdir_to_cache(

return subdir

####
# region: channeldata

def channeldata_path(self):
channeldata_file = os.path.join(self.output_root, "channeldata.json")
Expand Down Expand Up @@ -869,6 +1007,8 @@ def update_channeldata(self, rss=False):
log.debug("write channeldata")
self._write_channeldata(channel_data)

# endregion

def detect_subdirs(self):
if not self._subdirs:
detected_subdirs = {
Expand Down
Loading
Loading