From pypicloud
pypicloud was the closest thing to peryx in Python: a
Pyramid application offering private hosting on S3/GCS/Azure/local
storage with a fallback = cache mode that downloaded misses from PyPI, stored them, and served them. Its repository
was archived on August 27, 2023 ("Pypicloud has transitioned to maintenance mode"), with the last release in December
2022. It runs today only under Python 3.10 with SQLAlchemy pinned below 2.
Comparison against peryx
Overlap
- Read-through cache-on-miss. pypicloud's
fallback = cacheis peryx's default cached-index behavior: fetch a miss, store it, serve it. - Private hosting of your own packages, private names taking precedence over public ones.
- Token or user-authenticated uploads.
pypicloud-only behavior
- GCS and Azure storage backends. pypicloud supports them directly. peryx supports the local filesystem and S3-compatible object storage.
- Pluggable cache and access backends. pypicloud keeps its package index in SQLAlchemy, Redis, or DynamoDB, and drives access through config, SQL, or LDAP user/group systems. peryx embeds its metadata store (redb, nothing to provision). Access tokens carry scoped grants per index.
- Shared metadata backend. Several stateless pypicloud web servers can share one cache database. Each peryx node
keeps local metadata and coordinates through the selected
dcorhaavailability contract.
peryx-only behavior
- It is maintained. pypicloud is archived and pinned to a pre-2.0 SQLAlchemy stack.
- A streaming cold path. pypicloud buffers a missed wheel fully into a
TemporaryFile, writes it to storage and a cache row, and only then serves it, so the client waits for the whole download plus the disk write plus the DB commit. peryx streams the bytes to the client and into the store at once. - Concurrency correctness. A cold burst of clients asking pypicloud for the same wheel each download it and race to
insert the same primary key into single-writer SQLite; the losers surface as
HTTP 500. peryx single-flights the fetch, so all waiters tail one download. - Content-addressed dedup and PEP 658 metadata, neither of which pypicloud
offers (it stores files by
name/version/filenameand serves no.metadatasibling).
Performance vs peryx
The benchmark suite runs both from their published packages. Cold and warm installs through uv:
| peryx | pypicloud | |
|---|---|---|
| cold cache net | 4.4 s ±6% (1.18x) | 7.0 s ±3% (1.88x) |
| warm cache | 3.3 s ±1% (0.90x) | 3.9 s ±2% (1.08x) |
| server CPU | 1.8 s ±4% (1.00x) | 3.5 s ±6% (1.99x) |
| server peak memory | 699 MB ±2% (1.00x) | 376 MB ±0% (0.54x) |
The throughput workload includes the cold burst that pypicloud answers with HTTP 500: four clients ask for one large
wheel the instant it lands.
| peryx | pypicloud | |
|---|---|---|
| cold cache: 4 clients, one wheel net | 1.0 s ±1% (0.24x) | 4.6 s (1.05x) |
| hot cache: single download | 5,526 MB/s ±4% (48.67x) | 2,716 MB/s (23.92x) |
| hot cache: 8 parallel downloads | 7,273 MB/s ±2% (64.08x) | 3,441 MB/s (30.32x) |
| server CPU | 412 ms ±2% (1.00x) | 1.8 s ±48% (4.43x) |
| server peak memory | 44 MB ±1% (1.00x) | 279 MB ±34% (6.34x) |
Migration procedure
Feature-wise this is the most direct migration: peryx's read-through cached index is pypicloud's fallback = cache made
the default. Your cached-index state refills on first use; only hosted uploads need to move. Map the config across:
| pypicloud | peryx |
|---|---|
ppc-make-config + pserve config.ini | a TOML file + peryx serve |
pypi.fallback = cache | the default cached-index behavior |
pypi.fallback = redirect / none | not offered; misses serve through the cache or 404 on hosted-only indexes |
storage = s3 | [blob] backend = "s3" |
storage = gcs / azure | no native backend |
db = sqlalchemy / redis / dynamo cache | embedded (redb), nothing to provision |
| access backends (config / SQL / LDAP) | one write-granting [[index.access_token]] per hosted index |
/simple/ and /pypi/ routes | /{route}/simple/ |
Gotchas
- S3 settings differ. Configure the bucket under
[blob]; the AWS SDK provider chain supplies credentials. The object-storage guide lists durability requirements and migration limits. - Permissions move to grants. Set
anonymous_read = falseand addread,write, ordeleteactions to each[[index.access_token]]as needed. - Metadata stays node-local. Shared S3 stores blobs, not redb metadata. Use
dcorhafor coordinated nodes.