diff --git a/peps/pep-0694.rst b/peps/pep-0694.rst
index 2a9ac30bdbb..2c38fb9370d 100644
--- a/peps/pep-0694.rst
+++ b/peps/pep-0694.rst
@@ -21,8 +21,8 @@ Abstract
This PEP proposes an extensible API for uploading files to a Python package index such as PyPI.
Along with standardization, the upload API provides additional useful features such as support for:
-* a publishing session, which can be used to simultaneously publish
- all wheels in a package release;
+* a publishing session, which can be used to simultaneously and atomically publish
+ all artifacts (wheels, sdists) in a package release;
* "staging" a release, which can be used to test uploads before publicly publishing them,
without the need for `test.pypi.org `__;
@@ -31,13 +31,12 @@ Along with standardization, the upload API provides additional useful features s
* detailed status on the state of artifact uploads;
-* new project creation without requiring the uploading of an artifact.
+* new project creation without requiring the uploading of an artifact;
* a protocol to extend the supported upload mechanisms in the future without requiring a full PEP;
- these can be standardized and recommended for all indexes, or be index-specific;
+ these can be standardized and recommended for all indexes, or be index-specific.
-Once this new upload API is adopted, the existing legacy API can be deprecated, however this PEP
-does not propose a deprecation schedule for the legacy API.
+This PEP does not propose a deprecation schedule for the legacy API.
Rationale
@@ -56,7 +55,7 @@ In addition, there are a number of major issues with the legacy API:
while the index processes the uploaded file to determine success or failure.
* It does not support any mechanism for parallelizing or resuming an upload. With the largest
- default file size on PyPI being around 1GB in size, requiring the entire upload to complete
+ default file size on PyPI being around 1GiB in size, requiring the entire upload to complete
successfully means bandwidth is wasted when such uploads experience a network interruption while
the request is in progress.
@@ -74,18 +73,18 @@ In addition, there are a number of major issues with the legacy API:
unreliable, most installers instead choose to download the entire file and read the metadata from
there.
-* There is no mechanism for allowing an index to do any sort of sanity checks before bandwidth gets
- expended on an upload. Many cases of invalid metadata or incorrect permissions could be checked
+* There is no mechanism for allowing an index to do any sort of sanity checks before bandwidth gets expended
+ on an upload. Many error conditions, such as incorrect permissions or quota exhaustion could be checked
prior to uploading files.
* There is no support for "staging" a release prior to publishing it to the index.
* Creation of new projects requires the uploading of at least one file, leading to "stub" uploads
- to claim a project namespace.
+ to claim a project name, wasting space.
The new upload API proposed in this PEP provides ways to solve all of these problems, either directly or
through an extensible approach, allowing servers to implement features such as resumable and parallel uploads.
-This upload API this PEP proposes provides better and more standardized error reporting, a more robust release
+The upload API this PEP proposes provides better and more standardized error reporting, a more robust release
testing experience, and atomic and simultaneous publishing of all release artifacts.
Legacy API
@@ -150,16 +149,7 @@ that are used in the same way.
Upload 2.0 API Specification
============================
-This PEP traces the root cause of most of the issues with the existing API to be roughly two things:
-
-- The metadata is submitted alongside the file, rather than being parsed from the
- file itself. [#fn-metadata]_
-
-- It supports only a single request, using only form data, that either succeeds or fails, and all
- actions are atomic within that single request.
-
-To address these issues, this PEP proposes a multi-request workflow, which at a high level involves
-these steps:
+This PEP proposes a multi-request workflow, which at a high level involves these steps:
#. Initiate a :ref:`publishing session `, creating a release stage.
#. Initiate :ref:`file upload session(s) ` to that stage
@@ -176,19 +166,15 @@ these steps:
Versioning
----------
-This PEP uses the same ``MAJOR.MINOR`` versioning system as used in :pep:`691`,
-but it is otherwise independently versioned.
-The legacy API is considered by this PEP to be version ``1.0``,
-but this PEP does not modify the legacy API in any way.
+This PEP uses the same ``MAJOR.MINOR`` versioning system as used in :pep:`691`, but it is otherwise
+independently versioned. The legacy API is considered by this PEP to be version ``1.0``, but this PEP does
+not modify the legacy API in any way.
The API proposed in this PEP therefore has the version number ``2.0``.
-Both major and minor version numbers of the Upload API
-**MUST** only be changed through the PEP process.
-Index operators and implementers **MUST NOT** advertise or implement
-new API versions without an approved PEP.
-This ensures consistency across all implementations
-and prevents fragmentation of the ecosystem.
+Both major and minor version numbers of the Upload API **MUST** only be changed through the PEP process.
+Index operators and implementers **MUST NOT** advertise or implement new API versions without an approved PEP.
+This ensures consistency across all implementations and prevents fragmentation of the ecosystem.
Content Types
-------------
@@ -212,7 +198,7 @@ in the content type; the version number is prefixed with a ``v``.
The major API version specified in the ``.meta.api-version`` JSON key of client requests
**MUST** match the ``Content-Type`` header for major version.
-Unlike :pep:`691`, this PEP does not change the existing *legacy* ``1.0`` upload API in any way,
+Unlike :pep:`691`, this PEP does not change the existing legacy ``1.0`` upload API in any way,
so servers are required to host the new API described in this PEP at a different endpoint than the
existing upload API.
@@ -221,16 +207,13 @@ defined in this PEP **MUST** include a ``Content-Type`` header value of:
- ``application/vnd.pypi.upload.v2+json``.
-Similar to :pep:`691`, this PEP also standardizes on using server-driven content negotiation to
-allow clients to request different versions or serialization formats,
-which includes the ``format`` part of the content type.
-However, since this PEP expects the existing legacy ``1.0`` upload API
-to exist at a different endpoint,
-and this PEP currently only provides for JSON serialization,
-this mechanism is not particularly useful.
-Clients only have a single version and serialization they can request.
-However clients **SHOULD** be prepared to handle content negotiation gracefully
-in the case that additional formats or versions are added in the future.
+Similar to :pep:`691`, this PEP also standardizes on using server-driven content negotiation to allow clients
+to request different versions or serialization formats, which includes the ``format`` part of the content
+type. However, since this PEP expects the existing legacy ``1.0`` upload API to exist at a different
+endpoint, and this PEP currently only provides for JSON serialization, this mechanism is not particularly
+useful. Clients only have a single version and serialization they can request. However clients **SHOULD** be
+prepared to handle content negotiation gracefully in the case that additional formats or versions are added in
+the future.
Servers **MUST NOT** advertise support for API versions beyond those defined in approved PEPs.
Any new versions or formats require standardization through a new PEP.
@@ -253,8 +236,10 @@ the url structure of a domain. For example, the root endpoint could be
The choice of the root endpoint is left up to the index operator.
-Authentication for Upload 2.0 API
-----------------------------------
+.. _authentication:
+
+Authentication and Authorization
+--------------------------------
All endpoints in this specification **MUST** use standard HTTP authentication
mechanisms as defined in :rfc:`7235`.
@@ -269,6 +254,35 @@ Authentication follows the standard HTTP pattern:
The specific authentication schemes (e.g., Bearer, Basic, Digest)
are determined by the index operator.
+Authentication establishes the principal making a request. Authorization determines whether that principal
+may act on a particular session. All session endpoints defined in this specification (i.e. the URLs returned
+under the ``links`` key when a :ref:`publishing session ` or :ref:`file upload
+session ` is created) **MUST** be authorized against the project's upload
+permissions. Specifically, a server **MUST** verify, contemporaneously on each request, that the
+authenticated principal is currently authorized to upload to the project named by the session, and **MUST**
+respond with ``403 Forbidden`` if it is not.
+
+Because this check is performed independently on each request, a session is **not** tied to the exact
+credentials that created it:
+
+- A principal that is granted upload permission after a session is opened may immediately participate in that
+ session.
+- A principal whose upload permission is revoked while a session is open **MUST** be denied with a
+ ``403 Forbidden`` on any subsequent request, even if that principal created the session.
+
+This denial is evaluated per request and is not "sticky": if a principal's permission is later restored, its
+subsequent requests are authorized again. An index **MAY** apply a stricter policy, but this specification
+does not require one.
+
+Servers **MUST** perform this authorization check on at least every request that creates, modifies, completes,
+extends, cancels, or publishes a publishing session or file upload session. For upload mechanisms that
+transfer a file across more than one request (for example, chunked or multipart mechanisms), servers
+**SHOULD** authorize each such request.
+
+The unguessable :ref:`stage preview URL ` is a separate capability and is deliberately **not**
+governed by this authorization check; it grants read-only preview access to any client that holds the token,
+so that (for example) a CI job can install-test a staged release without project upload credentials.
+
.. _session-errors:
@@ -345,7 +359,7 @@ A release starts by creating a new publishing session. To create the session, a
The request includes the following top-level keys:
``meta`` (**required**)
- Describes information about the payload itself. Currently, the only defined sub-key is
+ Describes information about the payload itself. Currently, the only required sub-key is
``api-version`` the value of which must be the string ``"2.0"``. Optional sub-keys can define
:ref:`index-specific behavior `.
@@ -364,24 +378,28 @@ Upon successful session creation, the server returns a ``201 Created`` response.
include a ``Location`` header containing the same URL as the :ref:`links.session `
key in the :ref:`response body `.
-If a session is created for a project which has no previous release,
-then the index **MAY** reserve the project name before the session is published,
-however it **MUST NOT** be possible to navigate to that project using
-the "regular" (i.e. :ref:`unstaged `) access protocols,
-*until* the stage is published.
-If this first-release stage gets canceled,
-then the index **SHOULD** delete the project record, as if it were never uploaded.
+If a session is created for a project which has no previous release, then the index **MAY** reserve the
+project name before the session is published, however it **MUST NOT** be possible to navigate to that project
+using the "regular" (i.e. :ref:`unstaged `) access protocols, *until* the stage is published.
+If this first-release stage gets canceled, then the index **SHOULD** delete the project record, as if it were
+never uploaded.
-The session is owned by the user that created it,
-and all subsequent requests **MUST** be performed with the same credentials,
-otherwise a ``403 Forbidden`` will be returned on those subsequent requests.
+A publishing session is **not** bound to the specific credentials that created it. Instead, every request
+against the session **MUST** be performed by an authenticated principal that is authorized to upload to the
+project at the time of that request, as described in :ref:`authentication`. A request from a principal that
+is not, or is no longer, so authorized **MUST** receive a ``403 Forbidden``.
+
+For a first-release session on a project that does not yet exist, there are no existing project upload
+permissions to evaluate; the index instead authorizes the request according to its own name-registration
+policy, and **SHOULD** treat the creating principal (and, where applicable, an organization it acts on behalf
+of) as authorized for the lifetime of the session.
.. _index-specific-metadata:
Optional Index-specific Metadata
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Index can optionally define their own metadata for index-specific behavior. The metadata key
+Indexes can optionally define their own metadata for index-specific behavior. The metadata key
**MUST** begin with an underscore, with the following value easily and uniquely identifying the
index. For example, PyPI could allow for projects to be created in an `organization account
`__ of which the publisher is a member by using the
@@ -393,7 +411,7 @@ following index-specific metadata section:
"meta": {
"api-version": "2.0",
"_pypi.org": {
- "organization": "my-main-org"
+ "organization": "my-org"
}
},
"name": "foo",
@@ -401,7 +419,7 @@ following index-specific metadata section:
}
This is only an example. This PEP does not define or reserve any index-specific keys or metadata;
-that is left up to the index to specify and document. The semantics (e.g. whether bogus keys or
+that is left up to the index to specify and document. The semantics (e.g. whether invalid keys or
values result in an error or are ignored) of the index-specific metadata is also undefined here.
.. _publishing-session-response:
@@ -421,11 +439,13 @@ The successful response includes the following content:
"stage": "...",
"upload": "...",
"session": "...",
+ "publish": "...",
+ "extend": "...",
},
"mechanisms": ["http-post-bytes"],
"session-token": "",
"expires-at": "2025-08-01T12:00:00Z",
- "status": "pending",
+ "status": "open",
"files": {},
"notices": [
"a notice to display to the user"
@@ -451,18 +471,18 @@ the following keys:
index does *not* support stage previewing, this key **MUST** be omitted.
``expires-at``
- An :rfc:`3339` formatted timestamp string; this string **MUST** represent a UTC timestamp using the
- "Zulu" (i.e. ``Z``) marker, and use only whole seconds (i.e. no fractional seconds). This
- timestamp represents when the server will expire this session, and thus all of its content,
- including any uploaded files and the URL links related to the session. The session **SHOULD**
- remain active until at least this time unless the client itself has canceled or published the
- session. Servers **MAY** choose to extend this expiration time, but should never move it
- earlier. Clients can query the :ref:`session status ` to get the current
- expiration time of the session.
+ An :rfc:`3339` formatted timestamp string; this string **MUST** represent a UTC timestamp using the "Zulu"
+ (i.e. ``Z``) marker, and use only whole seconds (i.e. no fractional seconds). This timestamp represents
+ when the server will expire this session, and thus all of its content, including any uploaded files and
+ the URL links related to the session. The session **SHOULD** remain active until at least this time unless
+ the client itself has canceled or published the session. Servers **MAY** choose to extend this expiration
+ time, but should never move it earlier. Clients can query the :ref:`session status
+ ` to get the current expiration time of the session, and may request an
+ :ref:`extension `.
``status``
- A string that contains one of ``pending``, ``published``, ``error``, or ``canceled``,
- representing the overall :ref:`status of the session `.
+ A string that contains one of ``open``, ``processing``, ``published``, ``error``, or ``canceled``,
+ representing the overall :ref:`status of the session `.
``files``
A mapping containing the filenames that have been uploaded to this session, to a mapping
@@ -478,14 +498,23 @@ the following keys:
Multiple Session Creation Requests
++++++++++++++++++++++++++++++++++
-If a second attempt to create a session is received for the same name-version pair while a session for that
-pair is in the ``pending``, ``processing``, or ``complete`` state, then a new session is *not* created.
+If a second attempt to create a session is received for the same name-version pair while an existing session
+for that pair is in a non-terminal state -- that is, ``open``, ``processing``, or ``error`` (see
+:ref:`publishing session states `) -- then a new session is *not* created.
Instead, the server **MUST** respond with a ``409 Conflict`` and **MUST** include a ``Location`` header that
-points to the :ref:`session status URL `.
-
-For sessions in the ``error`` or ``canceled`` state, a new session is created with same ``201 Created``
-response and payload, except that the :ref:`publishing session status URL `,
-``session-token``, and ``links.stage`` values **MUST** be different.
+points to the :ref:`session status URL `. Like every other session request, such a
+request **MUST** be performed by a principal authorized to upload to the project (see :ref:`authentication`);
+a request from an unauthorized principal **MUST** receive a ``403 Forbidden`` instead, which takes precedence
+over the ``409 Conflict`` so that the existence of the in-progress session is not disclosed. An authorized
+principal receives the ``409 Conflict`` and ``Location`` header and may use the referenced session; this is
+how multiple authorized publishers (for example, distinct Trusted Publishing workflows) can contribute to the
+same session.
+
+Otherwise -- for example, when the name-version pair has no session in a non-terminal state, either because
+the previous session for that pair has reached a terminal ``published`` or ``canceled`` state, or because no
+session has ever been created for it -- a new session is created with the same ``201 Created`` response and
+payload, except that the :ref:`publishing session status URL `, ``session-token``,
+and ``links.stage`` values **MUST** be different.
.. _publishing-session-links:
@@ -495,12 +524,16 @@ Publishing Session Links
For the ``links`` key in the success JSON, the following sub-keys are valid:
``session``
- The endpoint where actions for this session can be performed,
- including :ref:`publishing this session `,
- :ref:`canceling and discarding the session `,
- :ref:`querying the current session status `,
- and :ref:`requesting an extension of the session lifetime `
- (*if* the server supports it).
+ The endpoint where the session resource can be accessed for
+ :ref:`querying the current session status ` (via ``GET``)
+ and :ref:`canceling and discarding the session ` (via ``DELETE``).
+
+``publish``
+ The endpoint for :ref:`publishing this session ` (via ``POST``).
+
+``extend``
+ The endpoint for :ref:`requesting an extension of the session lifetime `
+ (via ``POST``). If the server does not support session extensions, this key **MUST** be omitted.
``upload``
The endpoint session clients will use to initiate a :ref:`file upload session `
@@ -544,13 +577,106 @@ sub-mapping with the following keys:
these notices are specific to the referenced file.
+.. _publishing-session-states:
+
+Publishing Session States
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+A publishing session is always in exactly one of the following states, reported by the ``status`` key of the
+:ref:`session status response `:
+
+``open``
+ The session is accepting changes. Files can be :ref:`uploaded `, replaced,
+ and :ref:`deleted `; the session can be :ref:`previewed
+ ` and :ref:`extended `; and it can be
+ :ref:`published ` or :ref:`canceled
+ `. A newly created session starts in this state.
+
+``processing``
+ The client has requested publication and the server accepted the request for deferred processing,
+ returning a ``202 Accepted`` (see :ref:`publishing-session-completion`). The session is no longer
+ accepting changes while the server validates and processes it. This is a transitional state; the client
+ polls the :ref:`session status ` until it resolves to ``published`` or
+ ``error``.
+
+``published`` (**terminal**)
+ The session's files have been published and are publicly available. No further changes are possible.
+
+``error``
+ The most recent deferred publish attempt failed. The session is **fully editable again** -- it permits
+ exactly the same operations as ``open``, and differs from ``open`` *only* in that it records that the last
+ publish attempt failed. The human-readable reason **MUST** be reported in the session's ``notices`` (and,
+ where the failure is attributable to a particular file, in that file's ``notices``). From this state the
+ client can address the problem and :ref:`publish ` again, or :ref:`cancel
+ ` the session.
+
+``canceled`` (**terminal**)
+ The session was canceled and its staged data discarded. No further changes are possible.
+
+Both ``open`` and ``error`` are editable states that permit the identical set of operations; a client **MUST
+NOT** treat an ``error`` session as closed or read-only. The only difference between them is that ``error``
+additionally records that the previous deferred publish request failed.
+
+Because ``published`` and ``canceled`` are terminal, reaching either one frees the name-version pair so that a
+:ref:`subsequent session ` may be created for it, for example, to add wheels
+for additional platforms to an already-published release.
+
+The transitions between these states are:
+
+.. list-table::
+ :header-rows: 1
+
+ * - From
+ - Event
+ - To
+ * - *(none)*
+ - Session created
+ - ``open``
+ * - ``open``
+ - A file is uploaded, replaced, or deleted
+ - ``open``
+ * - ``open``
+ - Publish request completed immediately (``201 Created``)
+ - ``published``
+ * - ``open``
+ - Publish request accepted for deferred processing (``202 Accepted``)
+ - ``processing``
+ * - ``open`` or ``error``
+ - Publish request fails synchronously
+ - unchanged (the error is returned to the caller)
+ * - ``open`` or ``error``
+ - Session canceled (``DELETE``)
+ - ``canceled``
+ * - ``processing``
+ - Deferred processing succeeds
+ - ``published``
+ * - ``processing``
+ - Deferred processing fails
+ - ``error``
+ * - ``processing``
+ - Cancellation requested
+ - rejected with ``409 Conflict`` (see :ref:`publishing-session-cancellation`)
+ * - ``error``
+ - A file is uploaded, replaced, or deleted
+ - ``error``
+ * - ``error``
+ - Publish retried
+ - ``processing`` or ``published``
+
+A *synchronous* publish failure (i.e. one the server determines within the publish request itself) is returned
+to the caller as an :ref:`error response ` and leaves the session in its current editable
+state (``open`` stays ``open``; ``error`` stays ``error``). The ``error`` *state* is reached only when a
+publish that was accepted for deferred processing subsequently fails, because in that case the failure cannot
+be returned to the caller directly and the client discovers it by polling.
+
+
.. _publishing-session-completion:
Complete a Publishing Session
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To complete a session and publish the files that have been included in it, a client issues a
-``POST`` request to the ``session`` :ref:`link `
+``POST`` request to the ``publish`` :ref:`link `
given in the :ref:`session creation response body `.
The request looks like:
@@ -560,32 +686,43 @@ The request looks like:
{
"meta": {
"api-version": "2.0"
- },
- "action": "publish",
+ }
}
-If the server is able to immediately complete the publishing session, it may do so and return a
-``201 Created`` response. If it is unable to immediately complete the publishing session
-(for instance, if it needs to do validation that may take longer than reasonable in a single HTTP
-request), then it may return a ``202 Accepted`` response.
+If the server is able to immediately complete the publishing session, it may do so and return a ``201
+Created`` response, moving the session to the terminal :ref:`status `
+``published``. If it is unable to immediately complete the publishing session (for instance, if it needs to
+do validation that may take longer than reasonable in a single HTTP request), then it may return a ``202
+Accepted`` response and move the session to the ``processing`` state.
The server **MUST** include a ``Location`` header in the response pointing back to the :ref:`Publishing
-Session status ` URL, which can be used to query the current session status. If the server
-returned a ``202 Accepted``, polling that URL can be used to watch for session status changes.
+Session status ` URL, which can be used to query the current session status. If
+the server returned a ``202 Accepted``, polling that URL can be used to watch for the session status to
+change: deferred processing resolves to either ``published`` on success or ``error`` on failure. When it
+resolves to ``error``, the session remains editable and the reason is reported in the session's ``notices``,
+as described in :ref:`publishing-session-states`.
+
+A publish attempt that fails *synchronously* (i.e. within the publish request itself) is returned to the
+client as an :ref:`error response ` and leaves the session in its current editable state; it
+does **not** move the session to ``error``.
.. _publishing-session-cancellation:
Publishing Session Cancellation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-To cancel a publishing session, a client issues a ``DELETE`` request to
-the ``session`` :ref:`link `
-given in the :ref:`session creation response body `.
-The server then marks the session as canceled, and **SHOULD** purge any data that was uploaded
-as part of that session.
-Future attempts to access that session URL or any of the publishing session URLs
-**MUST** return a ``404 Not Found``.
+To cancel a publishing session, a client issues a ``DELETE`` request to the ``session`` :ref:`link
+` given in the :ref:`session creation response body `.
+The server then marks the session as ``canceled``, and **SHOULD** purge any data that was uploaded as part of
+that session. Future attempts to access that session URL or any of the publishing session URLs **MUST**
+return a ``404 Not Found``.
+
+Cancellation is only permitted while the session is :ref:`open or in the error state
+`. If the session is in the ``processing`` state (i.e. because a deferred
+publishing request is already being processed) the server **MUST** reject the cancellation with a ``409
+Conflict``, since publication may already be in progress. The client can instead wait for processing to
+resolve; if it resolves to ``error``, the session can then be canceled.
To prevent dangling sessions, servers may also choose to cancel timed-out sessions on their own
accord. It is recommended that servers expunge their sessions after no less than a week, but each
@@ -614,7 +751,8 @@ Publishing Session Extension
Servers **MAY** allow clients to extend sessions, but the overall lifetime and number of extensions
allowed is left to the server. To extend a session, a client issues a ``POST`` request to the
-:ref:`links.session ` URL (same as above, also the ``Location`` header).
+:ref:`links.extend ` URL. If the server does not support session extensions,
+the ``links.extend`` key will not be present in the response.
The request looks like:
@@ -624,7 +762,6 @@ The request looks like:
"meta": {
"api-version": "2.0"
},
- "action": "extend",
"extend-for": 3600
}
@@ -653,8 +790,8 @@ Indexes advertise their support for staged previews by returning two key pieces
staged previews **MUST NOT** include these in their responses.
The ``session-token`` is a short token which could be used as a convenience for installation tool UX, if they
-want to support staged previews via a command line switch, e.g. ``$TOOL install --staging $SESSION_TOKEN``.
-The ``links.stage`` key gives the full URL to the stage, which could be used in the CLI, e.g. ``pip
+want to support staged previews via a command line switch, e.g. ``pip install --staged $SESSION_TOKEN``. The
+``links.stage`` key gives the full URL to the stage, which could be used in the CLI, e.g. ``pip
install --extra-index-url $STAGE_URL``. Both the session token and URL **MUST** be cryptographically
unguessable, but the algorithm for generating the token is left to the index. The stage URL **MUST** be
calculable from the session token, using a format documented by the index, but the exact format of the URL is
@@ -686,7 +823,6 @@ The request looks like:
"filename": "foo-1.0.tar.gz",
"size": 1000,
"hashes": {"sha256": "...", "blake2b": "..."},
- "metadata": "...",
"mechanism": "http-post-bytes"
}
@@ -694,17 +830,15 @@ The request looks like:
Besides the standard ``meta`` key, the request JSON has the following additional keys:
``filename`` (**required**)
- The name of the file being uploaded. The filename **MUST** conform to either the `source distribution
- file name specification
- `_
- or the `binary distribution file name convention
- `_.
+ The name of the file being uploaded. The filename **MUST** conform to either the
+ `source distribution file name specification `_
+ or the `binary distribution file name convention `_.
Indexes **SHOULD** validate these file names at the time of the request, returning a ``400 Bad Request``
error code and an RFC 9457 style error body, as described in the :ref:`session-errors` section when the
file names do not conform.
``size`` (**required**)
- The size in bytes of the file being uploaded.
+ The final total size in bytes of the file being uploaded.
``hashes`` (**required**)
A mapping of hash names to hex-encoded digests. Each of these digests are the checksums of the
@@ -724,16 +858,17 @@ Besides the standard ``meta`` key, the request JSON has the following additional
A client **MAY** send a mechanism that is not advertised in cases where server operators have
documented a new or upcoming mechanism that is available for use on a "pre-release" basis.
-``metadata`` (**optional**)
- If given, this is a string value containing the file's `core metadata
- `_.
-
Servers **MAY** use the data provided in this request to do some sanity checking prior to allowing
the file to be uploaded. These checks may include, but are not limited to:
- checking if the ``filename`` already exists in a published release;
-- checking if the ``size`` would exceed any project or file quota;
-- checking if the contents of the ``metadata``, if provided, are valid.
+- checking if the ``size`` would exceed any project or file quota.
+
+A publishing session **MAY** be created for a ``name`` and ``version`` that has already been published, for
+example to add wheels for additional platforms to an existing release. However, because published artifacts
+are immutable, if the ``filename`` in this request matches a file that has already been published for this
+release, the server **MUST** reject the request with a ``409 Conflict`` and **MUST NOT** overwrite the
+published file.
If the server determines that upload should proceed, it will return a ``202 Accepted`` response, with the
response body below. The :ref:`status ` of the publishing session will also
@@ -756,7 +891,9 @@ The successful response includes the following:
"api-version": "2.0"
},
"links": {
- "file-upload-session": "..."
+ "file-upload-session": "...",
+ "complete": "...",
+ "extend": "..."
},
"status": "pending",
"expires-at": "2025-08-01T13:00:00Z",
@@ -779,7 +916,7 @@ the following keys:
``status``
A string with valid values ``pending``, ``processing``, ``complete``, ``error``, and ``canceled``
- indicating the current state of the file upload session.
+ indicating the current :ref:`state of the file upload session `.
``expires-at``
An :rfc:`3339` formatted timestamp string representing when the server will expire this file upload
@@ -801,11 +938,111 @@ File Upload Session Links
For the ``links`` key in the response payload, the following sub-keys are valid:
``file-upload-session``
- The endpoint where actions for this file-upload-session can be performed. including :ref:`completing a
- file upload session `, :ref:`canceling and discarding the file upload
- session `, :ref:`querying the current file upload session status
- `, and :ref:`requesting an extension of the file upload session lifetime
- ` (*if* the server supports it).
+ The endpoint where the file upload session resource can be accessed for :ref:`querying the
+ current file upload session status ` (via ``GET``) and
+ :ref:`canceling and discarding the file upload session ` (via
+ ``DELETE``).
+
+``complete``
+ The endpoint for :ref:`completing a file upload session ` (via ``POST``).
+
+``extend``
+ The endpoint for :ref:`requesting an extension of the file upload session lifetime
+ ` (via ``POST``). If the server does not support file upload session
+ extensions, this key **MUST** be omitted.
+
+.. _file-upload-session-states:
+
+File Upload Session States
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+A file upload session is always in exactly one of the following states, reported by the ``status``
+key of the :ref:`file upload session status response `. The same
+value is reflected for the file in the ``files`` mapping of the :ref:`publishing session status
+`.
+
+``pending``
+ The file upload session has been created and the negotiated :ref:`upload mechanism
+ ` is being executed; the file's bytes are in transit or not yet fully transferred.
+ A newly created session starts in this state and remains in it until the client :ref:`completes
+ ` or :ref:`cancels ` the upload. A file
+ whose upload is still ``pending`` cannot be :ref:`replaced `.
+
+``processing``
+ The client has requested completion and the server accepted the request for deferred processing, returning
+ a ``202 Accepted`` (see :ref:`file-upload-session-completion`). This is a transitional state; the client
+ polls the :ref:`file upload session status `, respecting the ``Retry-After``
+ header, until it resolves to ``complete`` or ``error``.
+
+``complete``
+ The file has been fully uploaded, validated, and accepted into the publishing session. The file can still
+ be :ref:`deleted `, which removes it from the publishing session and
+ moves this session to ``canceled``.
+
+``error``
+ The upload failed and the file is **not** in a usable state. Unlike a :ref:`publishing session in the
+ error state `, a file upload session cannot be repaired in place: the client
+ **MUST** :ref:`cancel or delete ` the file and, if it still wants to
+ upload it, begin an entirely new file upload session. A file upload session enters ``error`` whenever a
+ completion attempt fails -- whether the server detects the failure synchronously within the ``complete``
+ request, or asynchronously while the session is ``processing``.
+
+``canceled`` (**terminal**)
+ The session was canceled (an in-progress upload) or its completed file was deleted. The session resource
+ and its associated upload mechanisms **MUST NOT** be assumed reusable; recovering or replacing the file
+ requires a new file upload session.
+
+Only ``canceled`` is terminal. Both ``complete`` and ``error`` still permit a ``DELETE`` (which moves the
+session to ``canceled``); from ``error``, deletion is the only forward action.
+
+The transitions between these states are:
+
+.. list-table::
+ :header-rows: 1
+
+ * - From
+ - Event
+ - To
+ * - *(none)*
+ - File upload session created (``202 Accepted``)
+ - ``pending``
+ * - ``pending``
+ - Upload mechanism executes (bytes transferred)
+ - ``pending``
+ * - ``pending``
+ - Completion request completed immediately (``201 Created``)
+ - ``complete``
+ * - ``pending``
+ - Completion request accepted for deferred processing (``202 Accepted``)
+ - ``processing``
+ * - ``pending``
+ - Completion request fails synchronously
+ - ``error``
+ * - ``pending``
+ - Cancellation requested (``DELETE``)
+ - ``canceled``
+ * - ``processing``
+ - Deferred processing succeeds
+ - ``complete``
+ * - ``processing``
+ - Deferred processing fails
+ - ``error``
+ * - ``processing``
+ - Cancellation requested
+ - rejected with ``409 Conflict`` (see :ref:`file-upload-session-cancellation`)
+ * - ``complete``
+ - File deleted (``DELETE``)
+ - ``canceled``
+ * - ``error``
+ - File deleted (``DELETE``)
+ - ``canceled``
+
+Unlike a publishing session, where a synchronous publish failure leaves the session editable and only a
+*deferred* failure reaches the ``error`` state, a file upload session treats *any* completion failure as fatal
+to the file, because a partially or incorrectly uploaded file cannot be edited in place. Both a synchronous
+and a deferred completion failure therefore move the session to ``error``, from which the client deletes the
+file and starts over.
+
.. _file-upload-session-completion:
@@ -813,18 +1050,17 @@ Complete a File Upload Session
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To complete a file upload session, which indicates that the file upload mechanism has been executed
-and did not produce an error, a client issues a ``POST`` to the ``file-upload-session`` link in the
-file upload session creation response body.
+and did not produce an error, a client issues a ``POST`` to the ``complete`` :ref:`link
+` in the file upload session creation response body.
-The requests looks like:
+The request looks like:
.. code-block:: json
{
"meta": {
"api-version": "2.0"
- },
- "action": "complete",
+ }
}
If the server is able to immediately complete the file upload session, it may do so and return a ``201
@@ -841,6 +1077,11 @@ If the server responds with a ``202 Accepted``, clients may poll the file upload
for the status to change. Clients **SHOULD** respect the ``Retry-After`` header value of the file upload
session status response.
+If a completion attempt fails -- synchronously (in which case the server also returns an :ref:`error response
+`) or asynchronously while the session is ``processing`` -- the session moves to the
+:ref:`error state `, from which the client must :ref:`cancel or delete
+` the file and start a new file upload session to retry.
+
.. _file-upload-session-cancellation:
@@ -854,15 +1095,24 @@ to delete.
A successful deletion request **MUST** respond with a ``204 No Content``.
+A ``DELETE`` is permitted while the session is ``pending`` (canceling an in-progress upload), ``complete``
+(deleting an uploaded file), or ``error`` (discarding a failed upload). If the session is in the
+``processing`` state -- that is, a deferred completion is already underway -- the server **MUST** reject the
+``DELETE`` with a ``409 Conflict``, since the outcome is already being decided. The client can instead wait
+for processing to resolve and then delete the file if needed.
+
Once canceled or deleted, a client **MUST NOT** assume that the previous file upload session resource or
associated file upload mechanisms can be reused.
+.. _replacing-files:
+
Replacing a Partially or Fully Uploaded File
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To replace a session file, the file upload **MUST** have been previously completed, canceled, or
-deleted. It is not possible to replace a file if the upload for that file is in-progress.
+deleted. A file whose upload is still in-progress cannot be replaced; if a client attempts to do so,
+the server **MUST** return a ``409 Conflict``.
To replace a session file, clients should :ref:`cancel and delete the in-progress upload
` first. After this, the new file upload can be initiated by beginning the
@@ -887,8 +1137,9 @@ File Upload Session Extension
Servers **MAY** allow clients to extend file upload sessions, but the overall lifetime and number of
extensions allowed is left to the server. To extend a file upload session, a client issues a ``POST`` request
-to the ``links.file-upload-session`` URL from the :ref:`file upload session creation response
-`.
+to the ``extend`` :ref:`link ` from the :ref:`file upload session creation response
+`. If the server does not support file upload session extensions,
+the ``links.extend`` key will not be present in the response.
The request looks like:
@@ -898,7 +1149,6 @@ The request looks like:
"meta": {
"api-version": "2.0"
},
- "action": "extend",
"extend-for": 3600
}
@@ -914,8 +1164,8 @@ success response, and the ``expires-at`` key will simply reflect the current exp
.. _staged-preview:
-Stage Previews
---------------
+Staged Previews
+---------------
The ability to preview staged releases before they are published is an important feature of this PEP, enabling
an additional level of last-mile testing before the release is available to the public. Indexes **MAY**
@@ -958,12 +1208,11 @@ Required File Upload Mechanisms
Upload API version 2.0 compliant servers **MUST** support the ``http-post-bytes`` mechanism.
-This mechanism **MUST** use the same authentication scheme as
-the rest of the Upload 2.0 protocol endpoints.
+This mechanism **MUST** use the same authentication scheme as the rest of the Upload 2.0 protocol endpoints.
-A client executes this mechanism by submitting a ``POST`` request to the ``file_url``
-returned in the ``http-post-bytes`` map of the ``mechanism`` map of the
-:ref:`file upload session creation response body ` like:
+A client executes this mechanism by submitting a ``POST`` request to the ``file_url`` returned in the
+``http-post-bytes`` map of the ``mechanism`` map of the :ref:`file upload session creation response body
+` like:
.. code-block:: text
@@ -971,15 +1220,14 @@ returned in the ``http-post-bytes`` map of the ``mechanism`` map of the
-Servers **MAY** support uploading of digital attestations for files (see :pep:`740`).
-This support will be indicated by inclusion of an ``attestations_url`` key in the
-``http-post-bytes`` map of the ``mechanism`` map of the
-:ref:`file upload session creation response body `.
-Attestations **MUST** be uploaded to the ``attestations_url`` before
-:ref:`file upload session completion `.
+Servers **MAY** support uploading of digital attestations for files (see :pep:`740`). This support will be
+indicated by inclusion of an ``attestations_url`` key in the ``http-post-bytes`` map of the ``mechanism`` map
+of the :ref:`file upload session creation response body `. Attestations
+**MUST** be uploaded to the ``attestations_url`` before :ref:`file upload session completion
+`.
-To upload an attestation, a client submits a ``POST`` request to the ``attestations_url``
-containing a JSON array of :pep:`attestation objects <740#attestation-objects>` like:
+To upload an attestation, a client submits a ``POST`` request to the ``attestations_url`` containing a JSON
+array of :pep:`attestation objects <740#attestation-objects>` like:
.. code-block:: text
@@ -1002,19 +1250,15 @@ A server specific implementation file upload mechanism identifier has three part
--
-Server specific implementations **MUST** use ``vnd`` as their ``prefix``.
-The ``operator identifier`` **SHOULD** clearly identify the server operator,
-be unique from other well known indexes,
-and contain only alphanumeric characters ``[a-z0-9]``.
-The ``implementation identifier`` **SHOULD** concisely describe the underlying implementation
-and contain only alphanumeric characters ``[a-z0-9]`` and ``-``.
+Server specific implementations **MUST** use ``vnd`` as their ``prefix``. The ``operator identifier``
+**SHOULD** clearly identify the server operator, be unique from other well known indexes, and contain only
+alphanumeric characters ``[a-z0-9]``. The ``implementation identifier`` **SHOULD** concisely describe the
+underlying implementation and contain only alphanumeric characters ``[a-z0-9]`` and ``-``.
-When server operators need to make breaking changes to their upload mechanisms,
-they **SHOULD** create a new mechanism identifier rather than modifying the existing one.
-The recommended pattern is to append a version suffix like ``-v1``, ``-v2``, etc.
-to the implementation identifier.
-This allows clients to explicitly opt into new versions while maintaining
-backward compatibility with existing clients.
+When server operators need to make breaking changes to their upload mechanisms, they **SHOULD** create a new
+mechanism identifier rather than modifying the existing one. The recommended pattern is to append a version
+suffix like ``-v1``, ``-v2``, etc. to the implementation identifier. This allows clients to explicitly opt
+into new versions while maintaining backward compatibility with existing clients.
For example:
@@ -1035,13 +1279,279 @@ If a server intends to precisely match the behavior of another server's implemen
with that implementation's file upload mechanism name.
+.. _client-recommendations:
+
+Recommendations for Client Implementers
+=======================================
+
+This section is non-normative and provides guidance for client tool authors
+implementing the Upload 2.0 protocol. These recommendations are suggestions
+based on the expected usage patterns of the protocol; client authors are free
+to implement alternative approaches that best suit their users' needs.
+
+General Workflow
+----------------
+
+A typical upload workflow using the Upload 2.0 protocol follows these steps:
+
+1. Create a :ref:`publishing session ` for the project name and version.
+2. For each artifact (sdist, wheels), :ref:`create a file upload session `,
+ execute the negotiated upload mechanism, and :ref:`complete the file upload session
+ `.
+3. Optionally, if the index supports :ref:`stage previews `, use the ``links.stage``
+ URL to test the release before publishing.
+4. :ref:`Publish the session ` to make the release public,
+ or :ref:`cancel it ` if issues are discovered.
+
+Clients **SHOULD** handle failures gracefully at each step. If an error occurs during file upload,
+the client should :ref:`cancel the file upload session `. If an
+unrecoverable error occurs at any point, the client should :ref:`cancel the publishing session
+` to clean up server-side resources.
+
+Parallel Uploads
+~~~~~~~~~~~~~~~~
+
+Clients **MAY** upload multiple files in parallel by creating and executing multiple file upload
+sessions concurrently within the same publishing session. This can significantly improve upload
+times for releases with many wheel variants. However, clients should be prepared for servers that
+do not support parallel uploads and may return ``409 Conflict`` if parallel uploads are attempted.
+
+Multiple Sessions
+~~~~~~~~~~~~~~~~~
+
+Clients can decide whether they should create and manage a single session, multiple sessions in series, or
+multiple sessions in parallel, depending on the mix of artifacts being uploaded. Since publishing sessions
+are linked to a specific name-version identifier, if a single client command intends to upload several
+different name-version artifacts, each one must be in a separate publishing session.
+
+For example, ``twine upload foo-1.1.tar.gz foo-2.0.tar.gz bar-2.0.tar.gz`` would require three separate
+publishing sessions, however, if each sdist were also accompanied by wheels matching its name and version,
+three publishing sessions would still suffice. Clients should be able to manage all of this under-the-hood.
+
+Session Management
+~~~~~~~~~~~~~~~~~~
+
+Clients should monitor the ``expires-at`` timestamp in session responses. For long-running uploads
+(e.g., large files on slow connections), clients may need to :ref:`request session extensions
+` if the ``links.extend`` endpoint is available. If the server does
+not support extensions (indicated by the absence of ``links.extend``), clients should warn users
+when uploads may exceed the session lifetime.
+
+Suggested Command-Line Interfaces
+---------------------------------
+
+The following examples illustrate how existing tools might expose the Upload 2.0 protocol to users.
+These are suggestions only; actual implementations may vary.
+
+twine
+~~~~~
+
+`twine `__ currently provides a simple ``twine upload dist/*``
+command. The Upload 2.0 protocol could be exposed through additional options:
+
+``twine upload dist/*``
+ Maintains backward compatibility. Uses the Upload 2.0 protocol if available, falling back to
+ the legacy protocol if not. Creates a session, uploads all files, and publishes immediately.
+
+``twine upload --stage dist/*``
+ Uses the Upload 2.0 protocol to create a session and upload files, but does not publish.
+ This is useful even when the index does not support stage preview URLs, as it still provides
+ the atomic release semantics of Upload 2.0. If the index supports stage previews, prints the
+ ``links.stage`` URL for testing. Prints a session identifier that can be used with subsequent
+ commands. This session identifier is local to the client and is mapped internally to the
+ in-progress server session.
+
+``twine publish ``
+ Publishes a previously staged session.
+
+``twine cancel ``
+ Cancels a staged session and discards all uploaded files.
+
+``twine status ``
+ Queries and displays the current status of a session.
+
+uv
+~~
+
+`uv `__ could provide similar functionality with additional integration:
+
+``uv publish dist/*``
+ Creates a session, uploads all files, and publishes. May leverage parallel uploads for
+ faster publishing of multiple wheels.
+
+``uv publish --stage dist/*``
+ Uploads without publishing. Like twine, this is valuable even without stage preview support.
+
+``uv publish --test-install dist/*``
+ If the index supports stage previews, uploads files, installs the package from the stage URL
+ into a temporary virtual environment, optionally runs a smoke test command, and only publishes
+ if successful. This provides an integrated "upload, test, publish" workflow.
+
+GitHub Actions
+~~~~~~~~~~~~~~
+
+The `pypa/gh-action-pypi-publish `__ action could
+leverage staged releases to enable powerful CI/CD workflows. A multi-job workflow might look like:
+
+.. code-block:: yaml
+
+ jobs:
+ upload:
+ runs-on: ubuntu-latest
+ outputs:
+ stage-url: ${{ steps.upload.outputs.stage-url }}
+ session-id: ${{ steps.upload.outputs.session-id }}
+ steps:
+ - uses: actions/download-artifact@v4
+ with:
+ name: dist
+ path: dist/
+ - id: upload
+ uses: pypa/gh-action-pypi-publish@v2
+ with:
+ stage-only: true # Upload but don't publish
+
+ test:
+ needs: upload
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/setup-python@v5
+ - name: Test staged release
+ run: |
+ pip install --extra-index-url "${{ needs.upload.outputs.stage-url }}" my-package
+ python -c "import my_package; my_package.smoke_test()"
+
+ publish:
+ needs: [upload, test]
+ runs-on: ubuntu-latest
+ steps:
+ - uses: pypa/gh-action-pypi-publish@v2
+ with:
+ publish-session: ${{ needs.upload.outputs.session-id }}
+
+This pattern allows the actual PyPI artifacts to be tested in a realistic installation scenario
+before being published. If the test job fails, the workflow can include a cleanup job to cancel
+the session:
+
+.. code-block:: yaml
+
+ cancel-on-failure:
+ needs: [upload, test]
+ if: failure()
+ runs-on: ubuntu-latest
+ steps:
+ - uses: pypa/gh-action-pypi-publish@v2
+ with:
+ cancel-session: ${{ needs.upload.outputs.session-id }}
+
+Even when the index does not support stage preview URLs, the staged upload pattern is still
+valuable as it ensures atomic releases: either all artifacts are published together, or none are.
+
+Error Handling
+--------------
+
+Clients should implement robust error handling for the multi-step upload process:
+
+**File upload failures**: If a file upload fails (network error, validation error, etc.), the
+client should :ref:`cancel that file upload session ` before
+retrying. The client may then create a new file upload session for the same filename.
+
+**Partial upload recovery**: If some files have been successfully uploaded but others fail, the
+client has options:
+
+- Cancel the entire publishing session and start over.
+- Cancel only the failed file upload sessions and retry those files.
+- If using ``--stage`` mode, leave the session open for manual intervention.
+
+**Session expiration**: If a session expires during upload, the client must create a new publishing
+session and re-upload all files. Clients should monitor ``expires-at`` and warn users proactively.
+
+**Publishing failures**: If the publish request fails, the session remains in its current state.
+The client can query the session status to determine the cause and retry the publish operation.
+
+**Graceful cancellation**: When a user interrupts an upload (e.g., Ctrl+C), clients should attempt
+to cancel the publishing session to avoid leaving orphaned sessions on the server.
+
+Legacy API Fallback
+-------------------
+
+During the transition period, clients **SHOULD** support both the Upload 2.0 and legacy protocols.
+A suggested approach:
+
+1. Attempt to use Upload 2.0 by checking for the 2.0 endpoint or using content negotiation.
+2. If the server does not support Upload 2.0 (e.g., returns ``404`` or ``406``), fall back to the
+ legacy protocol.
+3. Provide a command-line option to force a specific protocol version if needed for debugging or
+ compatibility.
+
+
+.. _security-implications:
+
+Security Implications
+=====================
+
+Name squatting potential
+------------------------
+
+Does PEP 694 make it easier to (maliciously) register project names, i.e. to name- or typo-squat? The authors
+do not believe so. With the legacy API, it's trivially easy to create and upload a dummy package to register
+a project name. This PEP does not effectively change that equation either way, nor does it aim to. That
+said, indexes such as PyPI could impose additional limitations on project registration activities, such as
+rate limiting either the legacy API or Upload 2.0 API for empty packages or sessions. An index such as PyPI
+which supports organizations or :pep:`752`-style implicit namespaces, could implement different rate limiting
+rules for different actors. Such implementations are left as index-specific policy decisions.
+
+Session authorization
+---------------------
+
+Session access is authorized contemporaneously rather than being bound to the credentials that created the
+session (see :ref:`authentication`). Indexes **MUST** re-validate authorization on each session request --
+including artifact uploads, file upload session completion, session extension requests, and publishing -- so
+that a principal that loses upload permission while a session is open is denied on its subsequent requests,
+and a principal that gains permission may join an open session.
+
+This model has two consequences worth calling out. First, because every mutating operation is authorized
+uniformly, any principal currently authorized to upload to the project may add to, cancel, or publish another
+principal's open session. The blast radius is limited to the unpublished staging session, since published
+artifacts are immutable and publishing is atomic. Second, the :ref:`stage preview URL ` is a
+capability that is *not* gated by upload permission, so a principal whose permission is revoked mid-session --
+but who has already obtained the stage URL -- retains read-only preview access to the staged files until the
+session is published or canceled. This is a narrow and accepted limitation; an index that considers it a
+concern can mitigate it by canceling the affected session, or by limiting session lifetimes and extensions.
+
+Malware hosting potential
+-------------------------
+
+Staged releases, while useful for testing and embargoes, do provide some potential for larger scale hosting of
+malware which isn't detectable by third party external scanning tools, because staged artifacts are only
+visible to clients which hold the stage token/url. It's not clear how much proactive malware scanning is
+actually going on today with indexes such as PyPI, so it's unclear whether the (optional) staging feature is
+much of an additional malware vector. Indexes should likely do some amount of proactive malware scanning on
+all artifacts, regardless of the protocol used to upload them. Because of the multi-step protocol proposed in
+this PEP, indexes could share session links or uploaded staged files to trusted third party security partners
+who could assist in scanning.
+
+Indexes can also mitigate the problem by putting limits on session extensions, which might differ between
+projects depending on the user or (in the case of PyPI) organization which owns the project. Indexes can
+refuse to extend sessions, and they can use this to limit the availability of packages with unverified
+contents.
+
+Considering the testing and embargoed use cases may lead to different session expiry choices. Testing a
+release can have a relatively short session lifespan, e.g. on the order of hours. Embargoed sessions may need
+to be extended for several days or a few weeks. An index such as PyPI could use any number of criteria to
+determine the total lifetime of any particular session, such as whether the credentials are a user or an
+organization. An index could even support :ref:`index-specific-metadata` to decide whether the testing or
+embargoed use case is being employed.
+
+.. _faq:
+
FAQ
===
-Does this mean PyPI is planning to drop support for the existing upload API?
-----------------------------------------------------------------------------
+Does this mean PyPI is planning to drop support for the legacy upload API?
+--------------------------------------------------------------------------
-At this time PyPI does not have any specific plans to drop support for the existing upload API.
+At this time PyPI does not have any specific plans to drop support for the legacy upload API.
Unlike with :pep:`691` there are significant benefits to doing so, so it is likely that support for
the legacy upload API to be (responsibly) deprecated and removed at some point in the future.
@@ -1064,6 +1574,57 @@ index could define :ref:`index-specific metadata ` to,
an organization of which the publisher is a member, to own the new project.
+Why is the project name required when creating a publishing session?
+--------------------------------------------------------------------
+
+The project name is required at session creation because index permissions are fundamentally
+tied to project ownership. Users have roles and permissions on specific projects, and these
+permissions must be verified before any uploads can proceed.
+
+Requiring the project name upfront provides several benefits:
+
+**Immediate permission validation**: The server can verify that the authenticated user has
+upload permission for the project at session creation time, failing fast with a clear error
+rather than discovering permission issues after files have been uploaded.
+
+**Simplified error handling**: If a session could span multiple projects, a permission failure
+on one project mid-upload would leave the session in a complex partial state. With a single
+project per session, permission errors are unambiguous.
+
+**Trusted Publisher compatibility**: Indexes like PyPI support `Trusted Publishers
+`__ where OIDC tokens are scoped to specific projects.
+A single-project session aligns naturally with this authentication model.
+
+**Quota enforcement**: Projects may have different upload quotas or size limits. Validating
+these constraints upfront is simpler when the project is known at session creation.
+
+**Atomic release semantics**: A publishing session represents an atomic release of a single
+project version. Allowing multiple projects would fundamentally change this model and
+complicate the definition of what "publish" means for a session.
+
+
+Why is the version required when creating a publishing session?
+---------------------------------------------------------------
+
+The version is required at session creation to establish a validation contract before any file
+uploads begin. Since artifact filenames encode the version (per the `sdist `_
+and `wheel `_ filename specifications), the server can validate that all
+uploaded files match the declared version.
+
+This design enables deterministic behavior with :ref:`parallel uploads `.
+If the version were optional and inferred from the first uploaded file, a race condition would
+occur when multiple files are uploaded in parallel: whichever upload the server processes first
+would "win" and establish the version, causing other uploads with mismatched versions to fail
+non-deterministically.
+
+By requiring the version upfront, all parallel uploads validate against the same declared version.
+A file with a mismatched version always fails, regardless of upload timing or order.
+
+For :ref:`name registration ` where no artifacts are uploaded, the
+version can be any valid placeholder (e.g., ``"0.0.0a0"``) since it is ignored when no files are
+included in the session.
+
+
Open Questions
==============
@@ -1098,13 +1659,47 @@ as experience is gained operating Upload 2.0.
`_ ``hashlib.new()`` and
which does not require additional parameters.
-.. [#fn-immutable] Published files may still be yanked (i.e. :pep:`592`) or `deleted
- `__ as normal.
+.. _sdist-filename-spec: https://packaging.python.org/en/latest/specifications/source-distribution-format/#source-distribution-file-name
+.. _wheel-filename-spec: https://packaging.python.org/en/latest/specifications/binary-distribution-format/#file-name-convention
Change History
==============
+* `DD-MMM-2026 `__
+
+ * Session actions now use dedicated endpoint links instead of an ``action`` key in request bodies.
+ Publishing sessions add ``links.publish`` and ``links.extend``; file upload sessions add
+ ``links.complete`` and ``links.extend``. The ``links.session`` and ``links.file-upload-session``
+ endpoints are now used only for ``GET`` (status) and ``DELETE`` (cancel) operations.
+ * Add non-normative :ref:`Recommendations for Client Implementers ` section
+ with suggested UX patterns for tools like twine, uv, and GitHub Actions.
+ * Add FAQ entries explaining why project name and version are required at session creation.
+ * Add a :ref:`security-implications` section.
+ * Specify that attempting to replace an in-progress file upload returns a ``409 Conflict``.
+ * Specify that uploading a file matching one already published for an existing release returns a
+ ``409 Conflict``, since published artifacts are immutable.
+ * Clarify the wording of the **Multiple Sessions** client recommendation example.
+ * Relax session access from the exact creating credentials to any principal authorized to upload to the
+ project, evaluated contemporaneously on each request. Adds an **Authentication and Authorization** model,
+ handles permission changes mid-session, supports rotating Trusted Publishing tokens and multiple
+ publishers contributing to one session, and notes the related security implications.
+ * Remove the optional ``metadata`` key from the file upload session creation request. The uploaded file is
+ the authoritative source of metadata, which the index extracts from the file itself.
+ * Define an explicit publishing-session state machine. Rename the session-level ``pending`` status to
+ ``open``, add a transitional ``processing`` status for deferred (``202 Accepted``) publishing, and document
+ the ``error`` status as a still-editable state that records a failed deferred publish (with the reason
+ reported in ``notices``). Add a **Publishing Session States** section with state descriptions and a
+ transition table, specify that a synchronous publish failure leaves the session editable rather than
+ entering ``error``, and require the server to reject cancellation with a ``409 Conflict`` while a session is
+ ``processing``. Key the **Multiple Session Creation Requests** rule off any non-terminal state rather than
+ ``pending``.
+ * Document the file upload session state machine with a **File Upload Session States** section and transition
+ table. Specify that any completion failure -- synchronous or deferred -- moves the session to ``error``,
+ that an ``error`` file cannot be repaired in place (the client cancels or deletes it and starts a new file
+ upload session), and that the server **MUST** reject a ``DELETE`` with a ``409 Conflict`` while a session
+ is ``processing``.
+
* `07-Dec-2025 `__
* Error responses conform to the :rfc:`9457` format.