Publish and download (private) python packages using an OCI registry for storage.
To not have to rely on yet-another-cloud-provider for private Python packages, PyOCI, makes ghcr.io act like a python index.
In addition, this completely removes the need for separate access management as GitHub Packages access control applies.
Most subscriptions with cloud providers include an OCI (docker image) registry where private containers can be published and distributed from.
PyOCI allows using any (private) OCI registry as a python package index, as long as it implements the OCI distribution specification. It acts as a proxy between pip and the OCI registry.
An instance of PyOCI is available at https://pyoci.com, to use this proxy, please see the Getting started.
Tested registries:
Published packages will show up in the OCI registry UI:
To install a package with pip using PyOCI:
pip install --index-url="http://<username>:<password>@<pyoci-url>/<OCI-registry-url>/<namespace>/" <package-name>
<pyoci-url>: https://pyoci.com<OCI-registry-url>: URL of the OCI registry to use.<namespace>: namespace within the registry, for most registries this is the username or organization name.
Example installing package hello-world from organization allexveldman using ghcr.io as the registry:
pip install --index-url="https://$GITHUB_USER:$GITHUB_TOKEN@pyoci.com/ghcr.io/allexveldman/" hello-world
Warning
If the package contains dependencies from regular pypi, these will not resolve.
Pip does not have a proper way of indicating you only want to resolve <package-name> through PyOCI and it's dependencies through pypi.
Poetry does provide you with a way to do this.
As does uv.
For more examples, including how to publish a package, see the examples.
If you don't want, or can't, use https://pyoci.com, you can host your own using the docker container.
docker run ghcr.io/allexveldman/pyoci:latest
Note that only HTTP is supported at this moment, PyOCI is expected to run behind a reverse proxy that handles TLS termination, or a trusted environment.
PORT: port to listen on, defaults to8080.PYOCI_PATH: Host PyOCI on a subpath, for example:PYOCI_PATH="/acme-corp".PYOCI_MAX_BODY: Limit the maximum accepted body size in bytes when publishing packages, defaults to 50MB.PYOCI_MAX_VERSIONS: Limit how many versions (in reverse alphabetical order) to fetch filenames for when listing a package. By default PyOCI will only include the last100versions. To not limit the versions, set this value to0.OTLP_ENDPOINT: If set, forward logs, traces, and metrics to this OTLP collector endpoint every 30s.OTLP_AUTH: Full Authorization header value to use when sending OTLP requests.RUST_LOG: Log filter, defaults toinfo.
The following environment variables will be added as attributes to the OTLP resources:
DEPLOYMENT_ENVIRONMENT->deployment.environment
Set by Azure Container App, can change if I every decide to move host:
CONTAINER_APP_NAME->k8s.container.nameCONTAINER_APP_REVISION->k8s.pod.nameCONTAINER_APP_REPLICA_NAME->k8s.replicaset.name
PyOCI exposes the /health endpoint that returns HTTP 200 if the server is up and processing requests.
Note
This endpoint is always /health and does not change with PYOCI_PATH.
Labels can be added to your package by including them as a PyOCI :: Label :: <Key> :: <Value> classifier of the package.
If the classifiers are found in the package upload request, the key-value pairs will be added as annotations (aka labels in docker terms) to the OCI image.
Note that these classifiers are case-sensitive and non-standard.
For example, to associate a package
on ghcr.io with a repo on github.com, add the
PyOCI :: Label :: org.opencontainers.image.source :: https://github.com/<org>/<repo>
classifier to your project.
See the examples for how to add classifiers to your project.
OCI allows for images to contain paths, for example python/team1/hello-world.
Python does not allow for such a prefix.
To publish/use a package with a path prefix, append the path to the index url.
So to install hello-world as part of the python/team1/ path in the allexveldman organisation on ghcr.io,
use pip install --index-url="https://$GITHUB_USER:$GITHUB_TOKEN@pyoci.com/ghcr.io/allexveldman/python/team1/" hello-world
Note that this prefix is only reflected in the OCI registry, the package itself will be installed in your python environment as just hello-world.
Pip's Basic authentication is forwarded as-is to the target registry as part of the token authentication flow.
PyOCI will refuse to upload a package file if the package name, version and architecture already exist. To update an existing file, delete it first and re-publish it.
There is no formal specification for deleting python packages, instead you can use the OCI registry provided methods to delete your package.
PyOCI also supports deleting a package file using DELETE /<registry>/<namespace>/<package-name>/<filename>, support depends on the
underlying registry's support for the content management
section of the OCI Distribution specification.
As PyOCI acts as a private pypi index, Renovate needs to be configured to use credentials for your private packages (https://docs.renovatebot.com/getting-started/private-packages/).
To prevent having to check-in encrypted secrets you can:
- Self-host renovate as a github workflow
- Set
package: readpermissions for the workflow - Pass the
GITHUB_TOKENas an environment variable to Renovate - Add a hostRule for the Renovate runner to apply basic auth for pyoci using the environment variable
- In the package settings of the private package give the repository running renovate
readaccess.
Note that at the time of writing, GitHub App Tokens can't be granted read:package permissions,
this is why you'll need to use the GITHUB_TOKEN.
.github/workflows/renovate.yaml
...
concurrency:
group: Renovate
# Allow the GITHUB_TOKEN to read packages
permissions:
contents: read
packages: read
jobs:
renovate:
...
- name: Self-hosted Renovate
uses: renovatebot/github-action@v40.2.4
with:
configurationFile: config.js
token: '${{ steps.get_token.outputs.token }}'
env:
RENOVATE_PYOCI_USER: pyocibot
RENOVATE_PYOCI_TOKEN: ${{ secrets.GITHUB_TOKEN }}config.js
module.exports = {
...
hostRules: [
{
matchHost: "pyoci.com",
hostType: "pypi",
username: process.env.RENOVATE_PYOCI_USER,
password: process.env.RENOVATE_PYOCI_TOKEN
},
],
};See the contributing