Skip to content

Commit

Permalink
Add static index generation (#136)
Browse files Browse the repository at this point in the history
Adds the ability to generate a static index we can then host on GitHub
pages e.g. https://astral-sh.github.io/packse/8abfdb3/simple-html/

```
❯ uv pip install example-a --extra-index-url https://astral-sh.github.io/packse/8abfdb3/simple-html/
Resolved 2 packages in 1.07s
Downloaded 2 packages in 24ms
Installed 2 packages in 5ms
 + example-a==1.0.0
 + example-b==2.0.0
```

Available as `packse index build`, we don't re-use existing
distributions since we need the metadata from the scenarios. I think
this could replace CodeArtifact and Test PyPI. Only supports the HTML
index right now but we could probably generate a static JSON index too.
  • Loading branch information
zanieb authored Mar 6, 2024
1 parent fbe6643 commit 4e2b423
Show file tree
Hide file tree
Showing 17 changed files with 556 additions and 381 deletions.
4 changes: 2 additions & 2 deletions src/packse/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,13 +395,13 @@ def build_package(
)
module_name = package_name.replace("-", "_")

template_config = load_template_config("simple")
template_config = load_template_config("package")

logger.debug("Generating project for '%s-%s'", package_name, version)

package_destination = create_from_template(
build_destination,
template_name="simple",
template_name="package",
variables={
"package-name": package_name,
"module-name": module_name,
Expand Down
39 changes: 36 additions & 3 deletions src/packse/cli.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import argparse
import json
import logging
import sys
from pathlib import Path
Expand All @@ -14,7 +15,7 @@
UserError,
)
from packse.fetch import fetch
from packse.index import index_down, index_up
from packse.index import build_index, index_down, index_up
from packse.inspect import inspect
from packse.list import list
from packse.publish import publish
Expand Down Expand Up @@ -127,6 +128,14 @@ def _call_index_down(args):
exit(1)


def _call_index_build(args):
build_index(
args.targets,
short_names=args.short_names,
no_hash=args.no_hash,
)


def _call_publish(args):
publish(
args.targets,
Expand Down Expand Up @@ -183,7 +192,11 @@ def _call_inspect(args):
else:
targets.append(target)

inspect(targets, skip_invalid, short_names=args.short_names)
print(
json.dumps(
inspect(targets, skip_invalid, short_names=args.short_names), indent=2
)
)


def _root_parser():
Expand Down Expand Up @@ -365,7 +378,7 @@ def _add_serve_parser(subparsers):
"--build-dir",
type=Path,
default="./build",
help="The direcotry to store intermediate build artifacts in.",
help="The directory to store intermediate build artifacts in.",
)
parser.add_argument(
"--short-names",
Expand Down Expand Up @@ -430,9 +443,29 @@ def _add_index_parser(subparsers):
down = subparsers.add_parser("down", help="Stop a running package index server.")
down.set_defaults(call=_call_index_down)

build = subparsers.add_parser("build", help="Build a static package index server.")
build.add_argument(
"targets",
type=Path,
nargs="*",
help="The scenario files to load",
)
build.add_argument(
"--short-names",
action="store_true",
help="Exclude scenario names from generated packages.",
)
build.add_argument(
"--no-hash",
action="store_true",
help="Exclude scenario version hashes from generated packages.",
)
build.set_defaults(call=_call_index_build)

_add_shared_arguments(parser)
_add_shared_arguments(up)
_add_shared_arguments(down)
_add_shared_arguments(build)


def _add_list_parser(subparsers):
Expand Down
86 changes: 86 additions & 0 deletions src/packse/index.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
import errno
import hashlib
import importlib
import logging
import os
import shutil
import signal
import subprocess
import sys
import tempfile
import time
from contextlib import contextmanager, nullcontext
from pathlib import Path
from typing import Generator

from packse import __development_base_path__
from packse.build import build
from packse.error import (
RequiresExtra,
ServeAddressInUse,
ServeAlreadyRunning,
ServeCommandError,
)
from packse.inspect import inspect
from packse.template import create_from_template

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -291,3 +296,84 @@ def is_running(pid):
if err.errno == errno.ESRCH:
return False
return True


def sha256_file(path: Path):
h = hashlib.sha256()

with open(path, "rb") as file:
while True:
# Reading is buffered, so we can read smaller chunks.
chunk = file.read(h.block_size)
if not chunk:
break
h.update(chunk)

return h.hexdigest()


def build_index(
targets: list[Path],
no_hash: bool,
short_names: bool,
):
out_path = Path("./index")
if out_path.exists():
shutil.rmtree(out_path)

with tempfile.TemporaryDirectory() as tmpdir:
tmpdir = Path(tmpdir)
logger.info("Building distributions...")
build(
targets,
rm_destination=True,
skip_root=False,
short_names=short_names,
no_hash=no_hash,
dist_dir=tmpdir / "dist",
build_dir=tmpdir / "build",
)

out_path.mkdir()
(out_path / "files").mkdir()

variables = inspect(targets, short_names=short_names, no_hash=no_hash)
for scenario in variables["scenarios"]:
for package in scenario["packages"]:
# Create a new distributions section
package["dists"] = []
for version in package["versions"]:
for file in Path(
tmpdir
/ "dist"
/ (
scenario["name"]
if no_hash
else f"{scenario['name']}-{scenario['version']}"
)
).iterdir():
if package["name"].replace("-", "_") + "-" + version[
"version"
] not in str(file):
continue

# Include all the version information to start
dist = version.copy()
# Then add a sha256
dist["sha256"] = sha256_file(file)
dist["file"] = file.name
package["dists"].append(dist)
logger.info(
"Found distribution %s",
file.name,
)
file.rename(out_path / "files" / file.name)

logger.info("Populating template...")
create_from_template(
out_path,
"index",
variables=variables,
)

logger.info("Built static index at ./%s", out_path)
3 changes: 1 addition & 2 deletions src/packse/inspect.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""
Get details for all scenarios.
"""
import json
import logging
from pathlib import Path
from typing import cast
Expand Down Expand Up @@ -136,4 +135,4 @@ def inspect(

raw["module_name"] = raw["name"].replace("-", "_")

print(json.dumps(result, indent=2))
return result
2 changes: 1 addition & 1 deletion src/packse/scenario.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ class Scenario(msgspec.Struct, forbid_unknown_fields=True):
Additional options for the package resolver
"""

template: str = "simple"
template: str = "package"
"""
The template to use for scenario packages.
"""
Expand Down
69 changes: 48 additions & 21 deletions src/packse/template.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
import re
from pathlib import Path
from typing import Any

Expand Down Expand Up @@ -33,32 +34,58 @@ def create_from_template(
template_path = __templates_path__ / template_name
first_root = None
for root, _, files in template_path.walk():
# Determine the new directory path in the destination
new_root = destination / Path(
chevron_blue.render(str(root), variables, no_escape=True)
).relative_to(template_path)
for loop_root, scope in parse_loop(root, variables):
# Determine the new directory path in the destination
new_root = destination / Path(
chevron_blue.render(str(loop_root), scope, no_escape=True)
).relative_to(template_path)

if new_root == destination:
continue
if new_root == destination:
continue

if not first_root:
first_root = new_root
if not first_root:
first_root = new_root

# Create the new directory
logger.debug("Creating %s", new_root.relative_to(destination))
new_root.mkdir()
# Create the new directory
logger.debug("Creating %s", new_root.relative_to(destination))
new_root.mkdir()

for file in files:
file_path = root / file
for file in files:
file_path = root / file

# Determine the new file path
new_file_path = new_root / chevron_blue.render(
file, variables, no_escape=True
)
logger.debug("Creating %s", new_file_path.relative_to(destination))
# Determine the new file path
new_file_path = new_root / chevron_blue.render(
file, scope, no_escape=True
)
logger.debug("Creating %s", new_file_path.relative_to(destination))

new_file_path.write_text(
chevron_blue.render(file_path.read_text(), variables, no_escape=True)
)
new_file_path.write_text(
chevron_blue.render(file_path.read_text(), scope, no_escape=True)
)

return first_root


def parse_loop(path: Path, variables: dict[str, Any]):
# This implementatation is pretty dubious and certain to fail on edge-cases
scope = variables
scopes = []
matches = re.findall(r"\[\[(.*?)\]\]", str(path))
if not matches:
return [(path, variables)]

scopes = variables[matches[0].strip()]
for match in matches[1:]:
match: str = match.strip()
scopes = [scope[match] for scope in scopes if match in scope]

results = []
for scope in scopes:
for inner in scope:
results.append(
(
Path(re.sub(r"\[\[(.*?)\]\]", "", str(path))),
inner,
)
)
return results
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<meta name="pypi:repository-version" content="1.1" />
</head>
<body>
<h1>Links for {{ name }}</h1>
{{#dists}}
<a
href="../../files/{{file}}#sha256={{sha256}}"
{{#yanked}}
data-yanked="Yanked for testing"
{{/yanked}}
>
{{file}}
</a>
<br />
{{/dists}}
</body>
</html>
10 changes: 10 additions & 0 deletions src/packse/templates/index/simple-html/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<body>
{{#scenarios}}
<!-- scenario: {{ name }} -->
{{#packages}}
<a href="{{name}}/">{{name}}</a><br />
{{/packages}} {{/scenarios}}
</body>
</html>
File renamed without changes.
Loading

0 comments on commit 4e2b423

Please sign in to comment.