Skip to content

Poetry Repositories

View as Markdown

Poetry installs from a CloudRepo Python repository and publishes to it. CloudRepo serves the same simple index and the same upload API that pip and twine use, so Poetry needs the repository’s address and a credential.

Name CloudRepo as an explicit source, and pin each private package to it. Poetry asks an explicit source only for the packages pinned to it, so each private package comes from CloudRepo and nothing else, and every other package comes from PyPI. List every package in [project] dependencies, the table that poetry init writes, and pin each private one under [tool.poetry.dependencies]. The source URL ends in /simple/:

pyproject.toml
[project]
name = "my-service"
version = "0.1.0"
requires-python = ">=3.9"
dependencies = [
"requests (>=2.31.0,<3.0.0)", # public: from PyPI
"acme-shared-lib (>=1.2.0,<2.0.0)", # private: from CloudRepo alone
"acme-utils (>=1.0.0,<2.0.0)", # a private dependency of acme-shared-lib
]
[tool.poetry.dependencies]
acme-shared-lib = { source = "cloudrepo" }
acme-utils = { source = "cloudrepo" }
[[tool.poetry.source]]
name = "cloudrepo"
url = "https://your-org.mycloudrepo.io/repositories/your-repo/simple/"
priority = "explicit"

List and pin every private package, including each private package that your private packages depend on. A pin alone is ignored: once [project] dependencies lists any package, Poetry reads a [tool.poetry.dependencies] entry only for a package in that list. It prints no warning, and poetry check still prints All set!. When another package needs a private package that is pinned but not listed, Poetry looks it up on PyPI, as it does any dependency that names no source, and anyone can publish a package on PyPI under your private package’s name. Do not use priority = "supplemental" for private packages either: Poetry asks a supplemental source only when no primary source has a version that satisfies the constraint, and whoever publishes a package on PyPI under your private package’s name can choose a version that does.

poetry add installs at once. A private dependency that it meets before you have named its source is installed from PyPI. So to add a private package from the command line, name every private package it needs in one command, each with the source:

Terminal
poetry add acme-utils acme-shared-lib --source cloudrepo

If you instead add only acme-shared-lib, with poetry add acme-shared-lib --source cloudrepo, while acme-utils has no source yet, Poetry installs whatever package PyPI holds under the name acme-utils. CloudRepo is never asked for it.

When a private package starts to need another private package, add that one too, with poetry add and --source cloudrepo or by listing and pinning it by hand, before you run poetry update or poetry lock --regenerate. Otherwise Poetry takes it from PyPI.

Poetry reads the credential for a source from two environment variables. Their names carry the source’s name in upper case, so the source called cloudrepo takes these. Set both from your secret store, here as CLOUDREPO_USERNAME and CLOUDREPO_TOKEN:

Terminal
export POETRY_HTTP_BASIC_CLOUDREPO_USERNAME="$CLOUDREPO_USERNAME"
export POETRY_HTTP_BASIC_CLOUDREPO_PASSWORD="$CLOUDREPO_TOKEN"
poetry install

Do not use poetry config http-basic.cloudrepo. Given the token as an argument, it sits on the command line where a process listing shows it. Without the argument, Poetry prompts for the token, and on a machine with no keyring it prints Using a plaintext file to store credentials and writes the token to auth.toml in Poetry’s configuration directory.

Build, then publish to the repository URL without /simple/. Poetry reads the address from a third variable named for the repository, so nothing is written to Poetry’s configuration, and the credential is the same pair as above:

Terminal
export POETRY_REPOSITORIES_CLOUDREPO_URL=https://your-org.mycloudrepo.io/repositories/your-repo
export POETRY_HTTP_BASIC_CLOUDREPO_USERNAME="$CLOUDREPO_USERNAME"
export POETRY_HTTP_BASIC_CLOUDREPO_PASSWORD="$CLOUDREPO_TOKEN"
poetry build
poetry publish --repository cloudrepo

Expected: Poetry prints Publishing and the project name, then Uploading and the file name for the source distribution and the wheel, and exits without an error. The files are then in the repository, and the admin portal lists them.

  • Authorization error accessing. Poetry prints Source (cloudrepo): Authorization error accessing and the index URL, for a 401 and for a 403 alike. For a 401 the credential was refused or never sent: the username must be the email address of the account that created the token (__token__ is refused: that is the convention on pypi.org, and CloudRepo does not use it), the password must be the token itself, and a token that is expired or revoked fails the same way. Check that the variable names match the source’s name in upper case. For a 403 the token does not reach this repository: a token reaches only the repositories ticked when it was created.
  • doesn't match any versions. Poetry prints Because ... depends on <package> (<version>) which doesn't match any versions, version solving failed. The repository does not hold that name and version, or the URL is wrong, or the dependency names no source and Poetry looked on PyPI. A proxy repository that the token does not reach also answers 404, not 403.
  • HTTP Error 409: Conflict, on publish. The repository has Overwrite Protection on, and a file at that path already exists. Poetry prints the explanation CloudRepo sends with it after the status. Upload a new version, or turn Overwrite Protection off for that repository: see Python Repositories.

More: Repository tokens, for every client’s credential; Python Repositories, for pip and twine and the Python repository settings; uv Repositories, for uv.