blob: e39016c901abbf094a6c2a764f49e6987f3527b8 [file] [log] [blame]
Tarek Ziade1231a4e2011-05-19 13:07:25 +02001"""Upload a distribution to a project index."""
2
3import os
4import socket
5import logging
6import platform
7import urllib.parse
8from io import BytesIO
9from base64 import standard_b64encode
10from hashlib import md5
11from urllib.error import HTTPError
12from urllib.request import urlopen, Request
13
14from packaging import logger
15from packaging.errors import PackagingOptionError
16from packaging.util import (spawn, read_pypirc, DEFAULT_REPOSITORY,
Éric Araujoce5fe832011-07-08 16:27:12 +020017 DEFAULT_REALM, encode_multipart)
Tarek Ziade1231a4e2011-05-19 13:07:25 +020018from packaging.command.cmd import Command
19
20
21class upload(Command):
22
23 description = "upload distribution to PyPI"
24
25 user_options = [
26 ('repository=', 'r',
27 "repository URL [default: %s]" % DEFAULT_REPOSITORY),
28 ('show-response', None,
29 "display full response text from server"),
30 ('sign', 's',
31 "sign files to upload using gpg"),
32 ('identity=', 'i',
33 "GPG identity used to sign files"),
34 ('upload-docs', None,
35 "upload documentation too"),
36 ]
37
38 boolean_options = ['show-response', 'sign']
39
40 def initialize_options(self):
41 self.repository = None
42 self.realm = None
43 self.show_response = False
44 self.username = ''
45 self.password = ''
46 self.show_response = False
47 self.sign = False
48 self.identity = None
49 self.upload_docs = False
50
51 def finalize_options(self):
52 if self.repository is None:
53 self.repository = DEFAULT_REPOSITORY
54 if self.realm is None:
55 self.realm = DEFAULT_REALM
56 if self.identity and not self.sign:
57 raise PackagingOptionError(
58 "Must use --sign for --identity to have meaning")
59 config = read_pypirc(self.repository, self.realm)
60 if config != {}:
61 self.username = config['username']
62 self.password = config['password']
63 self.repository = config['repository']
64 self.realm = config['realm']
65
66 # getting the password from the distribution
67 # if previously set by the register command
68 if not self.password and self.distribution.password:
69 self.password = self.distribution.password
70
71 def run(self):
72 if not self.distribution.dist_files:
73 raise PackagingOptionError(
74 "No dist file created in earlier command")
75 for command, pyversion, filename in self.distribution.dist_files:
76 self.upload_file(command, pyversion, filename)
77 if self.upload_docs:
78 upload_docs = self.get_finalized_command("upload_docs")
79 upload_docs.repository = self.repository
80 upload_docs.username = self.username
81 upload_docs.password = self.password
82 upload_docs.run()
83
84 # XXX to be refactored with register.post_to_server
85 def upload_file(self, command, pyversion, filename):
86 # Makes sure the repository URL is compliant
87 scheme, netloc, url, params, query, fragments = \
88 urllib.parse.urlparse(self.repository)
89 if params or query or fragments:
90 raise AssertionError("Incompatible url %s" % self.repository)
91
92 if scheme not in ('http', 'https'):
93 raise AssertionError("unsupported scheme " + scheme)
94
95 # Sign if requested
96 if self.sign:
97 gpg_args = ["gpg", "--detach-sign", "-a", filename]
98 if self.identity:
99 gpg_args[2:2] = ["--local-user", self.identity]
100 spawn(gpg_args,
101 dry_run=self.dry_run)
102
103 # Fill in the data - send all the metadata in case we need to
104 # register a new release
105 with open(filename, 'rb') as f:
106 content = f.read()
107
108 data = self.distribution.metadata.todict()
109
110 # extra upload infos
111 data[':action'] = 'file_upload'
112 data['protcol_version'] = '1'
113 data['content'] = (os.path.basename(filename), content)
114 data['filetype'] = command
115 data['pyversion'] = pyversion
116 data['md5_digest'] = md5(content).hexdigest()
117
118 if command == 'bdist_dumb':
119 data['comment'] = 'built for %s' % platform.platform(terse=True)
120
121 if self.sign:
122 with open(filename + '.asc') as fp:
123 sig = fp.read()
124 data['gpg_signature'] = [
125 (os.path.basename(filename) + ".asc", sig)]
126
127 # set up the authentication
128 # The exact encoding of the authentication string is debated.
129 # Anyway PyPI only accepts ascii for both username or password.
130 user_pass = (self.username + ":" + self.password).encode('ascii')
131 auth = b"Basic " + standard_b64encode(user_pass)
132
133 # Build up the MIME payload for the POST data
Éric Araujoce5fe832011-07-08 16:27:12 +0200134 files = []
135 for key in ('content', 'gpg_signature'):
136 if key in data:
137 filename_, value = data.pop(key)
138 files.append((key, filename_, value))
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200139
Éric Araujoce5fe832011-07-08 16:27:12 +0200140 content_type, body = encode_multipart(data.items(), files)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200141
142 logger.info("Submitting %s to %s", filename, self.repository)
143
144 # build the Request
Éric Araujoce5fe832011-07-08 16:27:12 +0200145 headers = {'Content-type': content_type,
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200146 'Content-length': str(len(body)),
147 'Authorization': auth}
148
Éric Araujoce5fe832011-07-08 16:27:12 +0200149 request = Request(self.repository, body, headers)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200150 # send the data
151 try:
152 result = urlopen(request)
153 status = result.code
154 reason = result.msg
155 except socket.error as e:
156 logger.error(e)
157 return
158 except HTTPError as e:
159 status = e.code
160 reason = e.msg
161
162 if status == 200:
163 logger.info('Server response (%s): %s', status, reason)
164 else:
165 logger.error('Upload failed (%s): %s', status, reason)
166
167 if self.show_response and logger.isEnabledFor(logging.INFO):
168 sep = '-' * 75
169 logger.info('%s\n%s\n%s', sep, result.read().decode(), sep)