# Poetry Repositories

> Use a CloudRepo Python repository with Poetry: an explicit source, a credential from the environment, pinning private packages, publishing with poetry publish, and what a 401, 403, 404 or 409 means.

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.

## Before you start

- A Python repository. If you have none, see [Creating a Repository](/docs/manage/repositories.html#creating-a-repository).
- A repository token that reaches it, with **Read + write** if you will publish. See [Repository Tokens: Create One and Authenticate](/docs/authenticate/repository-tokens.html). The username is the email address of the account that created the token, and the password is the token.
- [Poetry](https://python-poetry.org/docs/#installation), installed from Poetry’s own instructions.

> **Note:** Use a repository token, not a password. An organization owner’s portal password is not accepted for Poetry, pip or twine. A token the owner creates works like anyone else’s.

## Add CloudRepo as an explicit source

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`

```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**

```bash
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.

## Give Poetry the credential

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**

```bash
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.

## Publish

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**

```bash
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](https://admin.cloudrepo.io) lists them.

> **Note:** Each file you publish to a Python repository, a wheel or a source distribution, can be up to 5 GiB (5,368,709,120 bytes). A larger upload is refused with `413`.

## When Poetry answers 401, 403, 404 or 409

- **`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](/docs/formats/python.html#overwrite-protection).

More: [Repository tokens](/docs/authenticate/repository-tokens.html), for every client’s credential; [Python Repositories](/docs/formats/python.html), for pip and twine and the Python repository settings; [uv Repositories](/docs/formats/uv.html), for uv.

---

The page: https://www.cloudrepo.io/docs/formats/poetry.html
