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