1414# See the License for the specific language governing permissions and
1515# limitations under the License.
1616import argparse
17+ import fnmatch
1718import logging
1819import os .path
19- from typing import Dict , List , cast
20+ from typing import Dict , List , NamedTuple , cast
2021
2122import requests
2223from rich import print
@@ -91,6 +92,44 @@ def _make_package(
9192 return package
9293
9394
95+ class Inputs (NamedTuple ):
96+ """Represents structured user inputs."""
97+
98+ dists : List [str ]
99+ signatures : Dict [str , str ]
100+ attestations_by_dist : Dict [str , List [str ]]
101+
102+
103+ def _split_inputs (
104+ inputs : List [str ],
105+ ) -> Inputs :
106+ """
107+ Split the unstructured list of input files provided by the user into groups.
108+
109+ Three groups are returned: upload files (i.e. dists), signatures, and attestations.
110+
111+ Upload files are returned as a linear list, signatures are returned as a
112+ dict of ``basename -> path``, and attestations are returned as a dict of
113+ ``dist-path -> [attestation-path]``.
114+ """
115+ signatures = {os .path .basename (i ): i for i in fnmatch .filter (inputs , "*.asc" )}
116+ attestations = fnmatch .filter (inputs , "*.*.attestation" )
117+ dists = [
118+ dist
119+ for dist in inputs
120+ if dist not in (set (signatures .values ()) | set (attestations ))
121+ ]
122+
123+ attestations_by_dist = {}
124+ for dist in dists :
125+ dist_basename = os .path .basename (dist )
126+ attestations_by_dist [dist ] = [
127+ a for a in attestations if os .path .basename (a ).startswith (dist_basename )
128+ ]
129+
130+ return Inputs (dists , signatures , attestations_by_dist )
131+
132+
94133def upload (upload_settings : settings .Settings , dists : List [str ]) -> None :
95134 """Upload one or more distributions to a repository, and display the progress.
96135
@@ -105,17 +144,17 @@ def upload(upload_settings: settings.Settings, dists: List[str]) -> None:
105144 The configured options related to uploading to a repository.
106145 :param dists:
107146 The distribution files to upload to the repository. This can also include
108- ``.asc`` files; the GPG signatures will be added to the corresponding uploads.
147+ ``.asc`` and ``.attestation`` files, which will be added to their respective
148+ file uploads.
109149
110150 :raises twine.exceptions.TwineException:
111151 The upload failed due to a configuration error.
112152 :raises requests.HTTPError:
113153 The repository responded with an error.
114154 """
115155 dists = commands ._find_dists (dists )
116- # Determine if the user has passed in pre-signed distributions
117- signatures = {os .path .basename (d ): d for d in dists if d .endswith (".asc" )}
118- uploads = [i for i in dists if not i .endswith (".asc" )]
156+ # Determine if the user has passed in pre-signed distributions or any attestations.
157+ uploads , signatures , _ = _split_inputs (dists )
119158
120159 upload_settings .check_repository_url ()
121160 repository_url = cast (str , upload_settings .repository_config ["repository" ])
0 commit comments