ยปCore Development>Code coverage>Lib/packaging/command/upload.py

Python code coverage for Lib/packaging/command/upload.py

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