Skip to content

Commit

Permalink
Add strict and offline index support (#36)
Browse files Browse the repository at this point in the history
Creates two separate indexes, `packages/local` and `packages/all`
instead of just `packages/all`. `packages/all` inherits from `root/pypi`
and `packages/local`. `packages/local` does not allow packages to be
downloaded from PyPI.

All of the "build" dependencies must be published to `packages/local`
for it to be used to install source distributions. This pull request
vendors all of the needed dependencies.

Additionally, there is an `--offline` flag for `packse index up` which
does not allow any PyPI access. This is kind of nice when you are not
planning on using fallback to PyPI as it prevents the devpi server from
doing any additional indexing of the real PyPI.

This was tested by installing a scenario published to the strict and
offline indexes.
  • Loading branch information
zanieb authored Dec 18, 2023
1 parent f6cd711 commit 2eb94c7
Show file tree
Hide file tree
Showing 20 changed files with 146 additions and 40 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ jobs:
- name: Publish scenarios [local]
if: github.ref != 'refs/heads/main'
run: |
# Start the local index server
index_url=$(poetry run packse index up --bg)
# Start the local index server, do not allow packages from PyPI
index_url=$(poetry run packse index up --bg --offline)
# Publish the packages
poetry run packse publish --anonymous --index-url "$index_url" dist/*
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,14 @@ packse index up

The `--bg` flag can be passed to run the index in the background.

Two package indexes are created:

- `packages/local`: which only allows locally published packages to be installed
- `packages/all`: which includes local packages but allows missing packages to be pulled from PyPI

When publishing scenario packages, you should always use the `packages/local` package index; or they will not be
available when installing from `packages/all`.

When running an index in the background, state will be stored in the `~/.packse` directory. The `PACKSE_STORAGE_PATH`
environment variable can be used to override the storage directory. Additionally, `--storage-path` can be passed
to the `index` command to change the storage path of server state.
Expand Down
12 changes: 12 additions & 0 deletions src/packse/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ def _call_index_up(args):
reset=args.reset,
storage_path=args.storage_path,
background=args.bg,
offline=args.offline,
all=args.all,
)


Expand Down Expand Up @@ -238,6 +240,16 @@ def _add_index_parser(subparsers):
action="store_true",
help="Run the index server in the background, exiting after it is ready.",
)
up.add_argument(
"--offline",
action="store_true",
help="Run the all index servers without acccess to the real PyPI.",
)
up.add_argument(
"--all",
action="store_true",
help="Output the index server URL that allows fallback to the real PyPI.",
)
up.set_defaults(call=_call_index_up)

down = subparsers.add_parser("down", help="Stop a running package index server.")
Expand Down
132 changes: 103 additions & 29 deletions src/packse/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from pathlib import Path
from tempfile import TemporaryDirectory

from packse import __development_base_path__
from packse.error import ServeAddressInUse, ServeAlreadyRunning

logger = logging.getLogger(__name__)
Expand All @@ -21,6 +22,8 @@ def index_up(
background: bool = False,
storage_path: Path | None = None,
reset: bool = False,
offline: bool = False,
all: bool = False,
):
server_url = f"http://{host}:{port}"

Expand Down Expand Up @@ -60,7 +63,12 @@ def index_up(

logger.info("Starting server at %s...", server_url)
with start_index_server(
storage_path, server_storage, host, port, server_log_path
storage_path,
server_storage,
host,
port,
server_log_path,
offline=offline,
) as server_process:
init_client(client_storage, server_url)

Expand All @@ -77,16 +85,43 @@ def index_up(
logger.debug("Logging in...")
login_user(username, password, client_storage)

index = "packages/all"
create_index(index, client_storage, exists_ok=background)
all_index = "packages/all"
local_index = "packages/local"
pypi_index = "root/pypi"

# First, create the "local" index which does not allow fallback to PyPI
create_index(local_index, client_storage, exists_ok=background, bases=[])

# Then, create the "all" index which pulls from the "local" index or PyPI
create_index(
all_index,
client_storage,
exists_ok=background,
bases=[local_index, pypi_index],
)

logger.info("Ensuring local index has build dependencies...")
add_build_requirements(local_index, client_storage)

all_index_url = f"{server_url}/{all_index}"
local_index_url = f"{server_url}/{local_index}"
logger.info(
"Indexes available at %s and %s", all_index_url, local_index_url
)

index_url = f"{server_url}/{index}"
logger.info("Index available at %s", index_url)
logger.debug(
"To use `devpi` commands, include `--clientdir %s`", client_storage
)

if background:
logger.info("Running in background with pid %s", server_process.pid)
logger.info("Stop index server with `packse index down`.")
print(index_url)

if all:
print(all_index_url)
else:
print(local_index_url)

else:
logger.info("Ready! [Stop with Ctrl-C]")

Expand Down Expand Up @@ -147,21 +182,26 @@ def start_index_server(
host: str,
port: int,
server_log_path: Path,
offline: bool,
) -> subprocess.Popen:
server_url = f"http://{host}:{port}"
command = [
"devpi-server",
"--serverdir",
str(server_storage),
# Future default, let's just opt-in now for better output
"--absolute-urls",
"--host",
host,
"--port",
str(port),
]

if offline:
command.append("--offline")

server_process = subprocess.Popen(
[
"devpi-server",
"--serverdir",
str(server_storage),
# Future default, let's just opt-in now for better output
"--absolute-urls",
"--host",
host,
"--port",
str(port),
],
command,
stderr=subprocess.STDOUT,
stdout=server_log_path.open("wb") if server_log_path else subprocess.PIPE,
)
Expand All @@ -187,6 +227,10 @@ def start_index_server(
except BaseException as exc:
server_process.kill()

stdout, _ = server_process.communicate(timeout=2)
if logger.getEffectiveLevel() <= logging.DEBUG:
print(stdout.decode())

# Do not reset the pidfile when a server was already running!
if not isinstance(exc, ServeAddressInUse):
reset_pidfile(storage_path)
Expand Down Expand Up @@ -222,21 +266,51 @@ def init_client(client_storage: Path, server_url: str):
)


def create_index(name: str, client_storage: Path, exists_ok: bool = False):
def add_build_requirements(
to_index: str,
client_storage: Path,
):
"""
Pushes package build requirements to an index that does not allow fallback to PyPI.
"""
build_directory = __development_base_path__ / "vendor" / "build"
logger.debug("Uploading build packages to index %s", to_index)
subprocess.check_output(
[
"devpi",
"upload",
"--clientdir",
str(client_storage),
"--from-dir",
str(build_directory.resolve()),
"--index",
to_index,
]
)


def create_index(
name: str,
client_storage: Path,
bases: list[str],
exists_ok: bool = False,
):
logger.info("Creating package index %r...", name)
command = [
"devpi",
"index",
"--clientdir",
str(client_storage),
"-c",
name,
"volatile=False",
"acl_upload=:ANONYMOUS:", # Do not require auth to upload packages
]
if bases:
command.append("bases={}".format(",".join(bases)))
try:
subprocess.check_output(
[
"devpi",
"index",
"--clientdir",
str(client_storage),
"-c",
name,
"bases=root/pypi", # TODO(zanieb): Allow users to disable pull from real PyPI
"volatile=False",
"acl_upload=:ANONYMOUS:", # Do not require auth to upload packages
],
command,
stderr=subprocess.STDOUT,
)
except subprocess.CalledProcessError as exc:
Expand Down
16 changes: 11 additions & 5 deletions tests/__snapshots__/test_index.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,16 @@
Initializing server...
Starting server at http://localhost:3141...
Configuring client...
Creating package index 'packages/local'...
Creating package index 'packages/all'...
Index available at http://localhost:3141/packages/all
Ensuring local index has build dependencies...
Indexes available at http://localhost:3141/packages/all and http://localhost:3141/packages/local
Running in background with pid [PID]
Stop index server with `packse index down`.

''',
'stdout': '''
http://localhost:3141/packages/all
http://localhost:3141/packages/local

''',
})
Expand All @@ -67,8 +69,10 @@
Initializing server...
Starting server at http://localhost:3141...
Configuring client...
Creating package index 'packages/local'...
Creating package index 'packages/all'...
Index available at http://localhost:3141/packages/all
Ensuring local index has build dependencies...
Indexes available at http://localhost:3141/packages/all and http://localhost:3141/packages/local
Ready! [Stop with Ctrl-C]
Interrupted!

Expand All @@ -83,14 +87,16 @@
Initializing server...
Starting server at http://localhost:3141...
Configuring client...
Creating package index 'packages/local'...
Creating package index 'packages/all'...
Index available at http://localhost:3141/packages/all
Ensuring local index has build dependencies...
Indexes available at http://localhost:3141/packages/all and http://localhost:3141/packages/local
Running in background with pid [PID]
Stop index server with `packse index down`.

''',
'stdout': '''
http://localhost:3141/packages/all
http://localhost:3141/packages/local

''',
})
Expand Down
4 changes: 2 additions & 2 deletions tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ def snapshot_command(
(re.escape(str(Path(sys.executable).parent)), "[PYTHON_BINDIR]"),
(re.escape(str((working_directory or Path.cwd()).absolute())), "[PWD]"),
(
r"(\d+\.)?\d+(ms|s)",
"[TIME]",
r"in (\d+\.)?\d+(ms|s)",
"in [TIME]",
),
(re.escape(str(__development_base_path__.absolute())), "[PROJECT_ROOT]"),
]
Expand Down
6 changes: 4 additions & 2 deletions tests/test_index.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
import subprocess

import psutil
Expand Down Expand Up @@ -44,7 +45,7 @@ def test_index_up_background(snapshot, tmpcwd, tmpenviron):
== snapshot
)
finally:
subprocess.call(["packse", "index", "down"])
subprocess.call(["packse", "index", "down"], env=tmpenviron)


def test_index_up_foreground(snapshot, tmpcwd, tmpenviron):
Expand All @@ -54,7 +55,8 @@ def test_index_up_foreground(snapshot, tmpcwd, tmpenviron):
snapshot_command(
["index", "up"],
extra_filters=FILTERS,
interrupt_after=5,
# Send a keyboard interrupt after a bit — use a longer delay for slow CI machines
interrupt_after=10 if os.environ.get("CI") else 5,
)
== snapshot
)
Expand Down
4 changes: 4 additions & 0 deletions vendor/build/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Vendored build dependencies

This directoy contains vendored build dependencies for running a package index in offline mode while allowing
package scenarios to be built and installed.
Binary file added vendor/build/calver-2022.6.26-py3-none-any.whl
Binary file not shown.
Binary file added vendor/build/editables-0.5.tar.gz
Binary file not shown.
Binary file added vendor/build/flit_core-3.9.0-py3-none-any.whl
Binary file not shown.
Binary file added vendor/build/hatchling-1.20.0.tar.gz
Binary file not shown.
Binary file added vendor/build/packaging-23.2.tar.gz
Binary file not shown.
Binary file added vendor/build/pathspec-0.12.1.tar.gz
Binary file not shown.
Binary file added vendor/build/pluggy-1.3.0.tar.gz
Binary file not shown.
Binary file added vendor/build/setuptools-69.0.2.tar.gz
Binary file not shown.
Binary file not shown.
Binary file added vendor/build/trove-classifiers-2023.11.29.tar.gz
Binary file not shown.
Binary file not shown.
Binary file added vendor/build/wheel-0.42.0-py3-none-any.whl
Binary file not shown.

0 comments on commit 2eb94c7

Please sign in to comment.