Set up pip, uv, and Poetry

Make your PackageMaze Feed the default index for a Python project, so every package — yours and the public ones you depend on — resolves through one Feed Base URL. Publishing goes to the same Feed, with twine, uv, or Poetry.

install — PyPI Simple API
https://pkg.packagemaze.com/<organization>/<feed>/simple/

Why one index, not two

#

Python clients make it easy to add PackageMaze alongside public PyPI. Do not. An additional index is not a weaker version of the same setup; it is a different, worse one:

  • Dependency confusion. With two indexes in the resolver path, the client may pick either. Someone who registers your internal package name on public PyPI can win that race. pip's own documentation warns about this.
  • Supply-chain rules that hold. Blocked names, delisted versions, and Reserved Package Namespaces stop nothing if the client can still reach public PyPI directly.
  • Minimum Age Policy. Your Feed withholds brand-new upstream versions — 24 hours by default — long enough for a bad release to be caught. A fallback index hands the client that release anyway.
  • A record of what you actually use. Usage history can only report the requests that reach PackageMaze.

So: no --extra-index-url, no Poetry supplemental or explicit source, no second entry left in pyproject.toml pointing at pypi.org. A Unified Feed already proxies its linked External Feeds, so public packages arrive over the same endpoint as yours.

With your agent

#

"Make PackageMaze the only Python package index for this project."

  • Finds every index, source, and publish target the project configures.
  • Replaces them with one PackageMaze index, set as the default.
  • Removes extra-index-url and fallback sources instead of layering on top of them.
  • Keeps Token Secrets out of committed files — credentials stay in the environment.

Requires a connected agent. See Connect your agent.

Two URLs under one Feed

#

Installing and publishing are different Artifact Protocol endpoints beneath the same Feed Base URL. Point install configuration at the Simple API path and publish configuration at the Legacy Upload API path; neither works in the other's place.

install — PyPI Simple API
https://pkg.packagemaze.com/<organization>/<feed>/simple/
publish — PyPI Legacy Upload API
https://pkg.packagemaze.com/<organization>/<feed>/legacy/

Machine setup

#

Once per machine. Create a Token with Read access for installing, or Read-and-publish if you will also publish, and put its Secret in your shell environment. PackageMaze shows a Token Secret once, at creation.

shell
export MAZE_TOKEN="<Token Secret>"

Every Python client below authenticates as the fixed username __token__ with the Token Secret as the password. Prefer your operating system's secure storage — Keychain, Secret Service, or Credential Manager — over a long-lived exported variable in a shell profile.

Project setup

#

Once per project, committed. Each of these files names the Feed and refers to MAZE_TOKEN; none contains a literal Token Secret.

pip

#

Put the index on the first line of requirements.txt. pip expands environment variables in requirements files, so the credential stays out of Git.

requirements.txt
--index-url https://__token__:${MAZE_TOKEN}@pkg.packagemaze.com/<organization>/<feed>/simple/

--index-url replaces the default index. --extra-index-url adds one next to public PyPI — that is the anti-pattern this page exists to prevent.

uv

#

Declare a named index in pyproject.toml. default = true replaces uv's built-in PyPI index rather than adding to it, and authenticate = "always" sends credentials on the first request instead of waiting for a challenge.

pyproject.toml
[[tool.uv.index]]
name = "packagemaze"
url = "https://pkg.packagemaze.com/<organization>/<feed>/simple/"
publish-url = "https://pkg.packagemaze.com/<organization>/<feed>/legacy/"
default = true
authenticate = "always"

Poetry

#

Declare the Feed as a primary source. That is the whole trick: a primary source disables Poetry's implicit PyPI source for the project, which is exactly the all-packages behaviour you want.

pyproject.toml
[[tool.poetry.source]]
name = "packagemaze"
url = "https://pkg.packagemaze.com/<organization>/<feed>/simple/"
priority = "primary"

Do not use supplemental (searched only after PyPI) or explicit (searched only for packages pinned to it). And once the source is primary, ordinary poetry add is correct — poetry add --source packagemaze is for pinning one package in a multi-source project and says the opposite of what this setup means.

Install

#

pip

#

For commands outside a requirements file, set PIP_INDEX_URL in the environment.

shell
export PIP_INDEX_URL="https://__token__:${MAZE_TOKEN}@pkg.packagemaze.com/<organization>/<feed>/simple/"
pip install <package-name>

uv

#

uv reads credentials for the named index from environment variables built from the index name.

shell
export UV_INDEX_PACKAGEMAZE_USERNAME="__token__"
export UV_INDEX_PACKAGEMAZE_PASSWORD="${MAZE_TOKEN}"
uv sync
uv add <package-name>

Poetry

#

Store the credential against the source name once, then use ordinary Poetry commands.

shell
poetry config http-basic.packagemaze __token__ "${MAZE_TOKEN}"
poetry install
poetry add <package-name>

Publish

#

Publishing from CI is the recommended path — the credential is short-lived and the upload is attributed to a verified job. Publish locally when you must; the commands below are the fallback, not the default. All three need a Token with Read-and-publish access and target the Legacy Upload API path.

twine

#
shell
python -m build
TWINE_REPOSITORY_URL="https://pkg.packagemaze.com/<organization>/<feed>/legacy/" \
  TWINE_USERNAME="__token__" \
  TWINE_PASSWORD="${MAZE_TOKEN}" \
  TWINE_NON_INTERACTIVE=1 \
  twine upload dist/*

A machine-local .pypirc works too, but it holds a literal Token Secret. Keep it out of the repository.

uv

#

The publish target is the publish-url already in pyproject.toml, so the command names the index rather than a URL.

shell
export UV_PUBLISH_TOKEN="${MAZE_TOKEN}"
uv publish --index packagemaze --no-attestations

Poetry

#

Poetry keeps publish targets separate from install sources, so the Feed is registered a second time — as a repository this time, with the Legacy Upload API URL.

shell
poetry config repositories.packagemaze https://pkg.packagemaze.com/<organization>/<feed>/legacy/
poetry config http-basic.packagemaze __token__ "${MAZE_TOKEN}"
poetry publish --build --repository packagemaze

First-party versions you publish are installable immediately; Minimum Age Policy applies only to upstream versions. Once the CI publish job works, turning on Only allow publishing from approved CI jobs in Feed settings makes PackageMaze reject these local publishes outright.

CI

#

CI never stores a PackageMaze Token. Each job mints a short-lived one for the step that needs it. See Set up CI for the access rules and permissions this depends on.

GitHub Actions, pip

#
pip install job
- id: packagemaze-token
  uses: packagemaze/[email protected]
  with:
    feed: <organization>/<feed>
    purpose: install
- uses: actions/setup-python@v6
  with:
    python-version: "3.13"
- run: pip install -r requirements.txt
  env:
    PIP_INDEX_URL: https://__token__:${{ steps.packagemaze-token.outputs.token }}@pkg.packagemaze.com/<organization>/<feed>/simple/
    MAZE_TOKEN: ${{ steps.packagemaze-token.outputs.token }}

GitHub Actions, uv

#
uv install job
- id: packagemaze-token
  uses: packagemaze/[email protected]
  with:
    feed: <organization>/<feed>
    purpose: install
- uses: astral-sh/[email protected]
- run: uv sync
  env:
    UV_INDEX_PACKAGEMAZE_USERNAME: "__token__"
    UV_INDEX_PACKAGEMAZE_PASSWORD: ${{ steps.packagemaze-token.outputs.token }}

GitHub Actions, twine

#

Publish jobs use a second setup step with purpose: publish, which carries the Package name PackageMaze authorizes the upload against.

twine publish job
- id: packagemaze-publish-token
  uses: packagemaze/[email protected]
  with:
    feed: <organization>/<feed>
    purpose: publish
    package: "<package-name>"
- run: twine upload dist/*
  env:
    TWINE_REPOSITORY_URL: "https://pkg.packagemaze.com/<organization>/<feed>/legacy/"
    TWINE_USERNAME: "__token__"
    TWINE_PASSWORD: ${{ steps.packagemaze-publish-token.outputs.token }}
    TWINE_NON_INTERACTIVE: "1"

CircleCI

#

CircleCI exchanges its OIDC token through the maze CLI inside the step that needs the Token. Publish steps use --purpose publish and pass the Package name.

uv CircleCI install step
export MAZE_TOKEN="$(maze auth exchange-oidc --feed "<organization>/<feed>" --purpose install)"
UV_INDEX_PACKAGEMAZE_USERNAME="__token__" UV_INDEX_PACKAGEMAZE_PASSWORD="${MAZE_TOKEN}" uv sync

For Poetry, install Poetry in an earlier step and commit the project source shown above. Configure the fresh Token in the step that installs dependencies.

Poetry CircleCI install step
export MAZE_TOKEN="$(maze auth exchange-oidc --feed "<organization>/<feed>" --purpose install)"
poetry config http-basic.packagemaze __token__ "${MAZE_TOKEN}"
poetry install

Feed Configuration provides complete CircleCI install and publish jobs for Poetry. The publish step configures the upload repository and exchanges a separate Token for the Package it publishes.

Checking it worked

#

Feed Doctor checks a Feed's setup — Feed Base URL, Token access, and committed pip, uv, or Poetry configuration — and names the problem rather than returning a generic failure. It never asks for a Token Secret in committed files. If an install is already failing, Fix a failing install maps the symptom to a cause.

PDM and Hatch have Setup Instructions on the Feed Configuration page in the app, but no guide here; Pipenv has neither.