Upload: Clean up output. #283

Merged
merged 1 commit into from Feb 1, 2016
Jump to file or symbol
Failed to load files and symbols.
+167 −80
Split
View
@@ -33,7 +33,7 @@ before_script:
- sudo lxc config device add xenial /dev/sda1 disk source=$(pwd) path=$(pwd)
# Install the snapcraft dependencies.
- sudo lxc exec xenial -- apt-get update
- - sudo lxc exec xenial -- apt-get install -y pyflakes python-flake8 python3.5 python3-apt python3-docopt python3-coverage python3-fixtures python3-flake8 python3-jsonschema python3-mccabe python3-mock python3-pep8 python3-pip python3-requests python3-requests-oauthlib python3-responses python3-ssoclient python3-testscenarios python3-testtools python3-xdg python3-yaml python3-lxml squashfs-tools
+ - sudo lxc exec xenial -- apt-get install -y pyflakes python-flake8 python3.5 python3-apt python3-docopt python3-coverage python3-fixtures python3-flake8 python3-jsonschema python3-mccabe python3-mock python3-pep8 python3-pip python3-requests python3-requests-oauthlib python3-responses python3-ssoclient python3-testscenarios python3-testtools python3-xdg python3-yaml python3-lxml squashfs-tools python3-progressbar python3-requests-toolbelt
script:
- sudo -E lxc exec xenial -- su - ubuntu -c "cd $(pwd); ./runtests.sh $TEST_SUITE"
after_success:
View
@@ -11,8 +11,10 @@ Build-Depends: debhelper (>= 9),
python3-jsonschema,
python3-lxml,
python3-pkg-resources,
+ python3-progressbar,
python3-requests,
python3-requests-oauthlib,
+ python3-requests-toolbelt,
python3-responses,
python3-setuptools,
python3-ssoclient,
@@ -14,9 +14,8 @@ store on your behalf:
Enter your Ubuntu One SSO credentials.
Email: me@example.com
Password:
- OTP: <press enter here if you haven't enabled two-factor authentication>
+ One-time password (just press enter if you don't use two-factor authentication):
Authenticating against Ubuntu One SSO.
- Starting new HTTPS connection (1): login.ubuntu.com
Login successful.
These credentials will remain valid until you revoke them, which you can do
@@ -34,19 +33,12 @@ Now, let's upload that snap!
Get into the directory containing the `snapcraft.yaml`, and do the following:
$ snapcraft upload
- Uploading foo_1_amd64.snap
- Uploading files...
- Starting new HTTPS connection (1): upload.apps.ubuntu.com
- Uploading new version...
- Starting new HTTPS connection (1): myapps.developer.ubuntu.com
- Package submitted to https://myapps.developer.ubuntu.com/dev/api/click-package-upload/foo/
- Checking package status...
- Package scan completed.
- Application uploaded successfully.
- Uploaded as revision 1.
- Please check out the application at: https://myapps.ubuntu.com/dev/click-apps/1337/.
-
- foo_1_amd64.snap upload complete
+
+ Uploading foo_1_amd64.snap [==========================================] 100%
+ Checking package status... |
+
+ Application uploaded successfully (as revision 1)
+ Please check out the application at: https://myapps.ubuntu.com/dev/click-apps/1337/
Your snap has now been uploaded to the store, and is now undergoing an
automated review. You'll be emailed when the review has completed, at which time
@@ -45,5 +45,6 @@ def test_upload(self):
self.assertIn('Snap assemble_1.0_amd64.snap not found. Running snap '
'step to create it.', output)
- self.assertIn('Uploading {}'.format(snap_file_path), output)
- self.assertIn('{} upload failed'.format(snap_file_path), output)
+ self.assertIn('Upload failed', output)
+ self.assertIn('No valid credentials found. Have you run "snapcraft '
+ 'login"?', output)
@@ -47,7 +47,8 @@ def main(argv=None):
print('Enter your Ubuntu One SSO credentials.')
email = input('Email: ')
password = getpass.getpass('Password: ')
- otp = input('OTP: ')
+ otp = input('One-time password (just press enter if you don\'t use '
+ 'two-factor authentication): ')
@elopio

elopio Jan 30, 2016

Member

I like this!

@kyrofa

kyrofa Feb 1, 2016

Member

It's a bit verbose as we discussed, but I think it's a good intermediate solution until the uploader is smart enough to know whether or not it's actually required.

logger.info('Authenticating against Ubuntu One SSO.')
response = login(email, password, token_name='snapcraft', otp=otp)
@@ -62,10 +62,5 @@ def main(argv=None):
snap_name))
snap.main(argv=argv)
- logger.info('Uploading {}'.format(snap_name))
config = load_config()
- success = upload(snap_name, config=config)
- if success:
- logger.info('{} upload complete'.format(snap_name))
- else:
- logger.info('{} upload failed'.format(snap_name))
+ upload(snap_name, config=config)
View
@@ -60,3 +60,6 @@ def configure(logger_name=None):
logger.addHandler(handler)
logger.setLevel(logging.INFO)
+
+ # INFO from the requests lib is too noisy
+ logging.getLogger("requests").setLevel(logging.WARNING)
@@ -16,9 +16,15 @@
from __future__ import absolute_import, unicode_literals
import json
import logging
+import functools
+import time
import os
import re
+from concurrent.futures import ThreadPoolExecutor
+from progressbar import (ProgressBar, Percentage, Bar, AnimatedMarker)
+from requests_toolbelt import (MultipartEncoder, MultipartEncoderMonitor)
+
from .common import (
get_oauth_session,
is_scan_completed,
@@ -32,10 +38,14 @@
SCAN_STATUS_POLL_RETRIES,
)
-
logger = logging.getLogger(__name__)
+def _update_progress_bar(progress_bar, maximum_value, monitor):
+ if monitor.bytes_read <= maximum_value:
+ progress_bar.update(monitor.bytes_read)
+
+
def upload(binary_filename, metadata_filename='', metadata=None, config=None):
"""Create a new upload based on a snap package."""
@@ -48,15 +58,16 @@ def upload(binary_filename, metadata_filename='', metadata=None, config=None):
return
name = match.groupdict()['name']
- logger.info('Uploading files...')
+ # Print a newline so the progress bar has some breathing room.
+ print('')
+
data = upload_files(binary_filename, config=config)
success = data.get('success', False)
errors = data.get('errors', [])
if not success:
logger.info('Upload failed:\n\n%s\n', '\n'.join(errors))
return False
- logger.info('Uploading new version...')
meta = read_metadata(metadata_filename)
meta.update(metadata or {})
result = upload_app(name, data, metadata=meta, config=config)
@@ -65,19 +76,25 @@ def upload(binary_filename, metadata_filename='', metadata=None, config=None):
app_url = result.get('application_url', '')
revision = result.get('revision')
+ # Print another newline to make sure the user sees the final result of the
+ # upload (success/failure).
+ print('')
+
if success:
- logger.info('Application uploaded successfully.')
+ message = 'Application uploaded successfully'
if revision:
- logger.info('Uploaded as revision %s.', revision)
+ message += ' (as revision {})'.format(revision)
+
+ logger.info(message)
else:
logger.info('Upload did not complete.')
if errors:
- logger.info('Some errors were detected:\n\n%s\n\n',
+ logger.info('Some errors were detected:\n\n%s\n',
'\n'.join(errors))
if app_url:
- logger.info('Please check out the application at: %s.\n',
+ logger.info('Please check out the application at: %s\n',
app_url)
return success
@@ -92,26 +109,50 @@ def upload_files(binary_filename, config=None):
updown_url = os.environ.get('UBUNTU_STORE_UPLOAD_ROOT_URL',
UBUNTU_STORE_UPLOAD_ROOT_URL)
unscanned_upload_url = urljoin(updown_url, 'unscanned-upload/')
- files = {'binary': open(binary_filename, 'rb')}
result = {'success': False, 'errors': []}
session = get_oauth_session(config)
if session is None:
- result['errors'] = ['No valid credentials found.']
+ result['errors'] = [
+ 'No valid credentials found. Have you run "snapcraft login"?']
return result
try:
+ binary_file_size = os.path.getsize(binary_filename)
+ binary_file = open(binary_filename, 'rb')
+ encoder = MultipartEncoder(
+ fields={
+ 'binary': ('filename', binary_file, 'application/octet-stream')
+ }
+ )
+
+ # Create a progress bar that looks like: Uploading foo [== ] 50%
+ progress_bar = ProgressBar(
+ widgets=['Uploading {} '.format(binary_filename),
+ Bar(marker='=', left='[', right=']'), ' ', Percentage()],
+ maxval=os.path.getsize(binary_filename)).start()
+
+ # Create a monitor for this upload, so that progress can be displayed
+ monitor = MultipartEncoderMonitor(
+ encoder, functools.partial(_update_progress_bar, progress_bar,
+ binary_file_size))
+
+ # Begin upload
response = session.post(
unscanned_upload_url,
- files=files)
+ data=monitor, headers={'Content-Type': monitor.content_type})
+
+ # Make sure progress bar shows 100% complete
+ progress_bar.finish()
+
if response.ok:
response_data = response.json()
result.update({
'success': response_data.get('successful', True),
'upload_id': response_data['upload_id'],
'binary_filesize': os.path.getsize(binary_filename),
- 'source_uploaded': 'source' in files,
+ 'source_uploaded': False,
})
else:
logger.error(
@@ -125,9 +166,8 @@ def upload_files(binary_filename, config=None):
'An unexpected error was found while uploading files.')
result['errors'] = [str(err)]
finally:
- # make sure to close any open files used for request
- for fd in files.values():
- fd.close()
+ # Close the open file
+ binary_file.close()
@elopio

elopio Jan 30, 2016

Member

@sergiusens copied the same block in #285. I prefer your removal of the confusing dictionary of 1 element, so I think this one should land first.

@sergiusens

sergiusens Feb 1, 2016

Collaborator

Right, this may need to be refactored to support multiple snap uploads when library snaps come (hope we can have something for next week).

@kyrofa

kyrofa Feb 1, 2016

Member

Ah, yeah we'll have to discuss how we want to do the progress bar in that case.

return result
@@ -153,7 +193,8 @@ def upload_app(name, upload_data, metadata=None, config=None):
session = get_oauth_session(config)
if session is None:
- result['errors'] = ['No valid credentials found.']
+ result['errors'] = [
+ 'No valid credentials found. Have you run "snapcraft login"?']
return result
if metadata is None:
@@ -167,11 +208,39 @@ def upload_app(name, upload_data, metadata=None, config=None):
if response.ok:
response_data = response.json()
status_url = response_data['status_url']
- logger.info('Package submitted to %s', upload_url)
- logger.info('Checking package status...')
- completed, data = get_scan_data(session, status_url)
+
+ # This is just a waiting game, so we'll show an indeterminate
+ # AnimatedMarker for it.
+ progress_indicator = ProgressBar(
+ widgets=['Checking package status... ', AnimatedMarker()],
+ maxval=7).start()
+
+ # Execute the package scan in another thread so we can update the
+ # progress indicator.
+ with ThreadPoolExecutor(max_workers=1) as executor:
+ future = executor.submit(get_scan_data, session, status_url)
+
+ count = 0
+ while not future.done():
+ # Annoyingly, there doesn't seem to be a way to actually
+ # make a progress indicator that will go on forever, so we
+ # need to restart this one each time we reach the end of
+ # its animation.
+ if count >= 7:
+ progress_indicator.start()
+ count = 0
+
+ # Actually update the progress indicator
+ progress_indicator.update(count)
+ count += 1
+ time.sleep(0.15)
+
+ # Grab the results from the package scan
+ completed, data = future.result()
+
+ progress_indicator.finish()
+
if completed:
- logger.info('Package scan completed.')
message = data.get('message', '')
if not message:
result['success'] = True
@@ -263,7 +332,7 @@ def get_scan_data(session, status_url):
@retry(terminator=is_scan_completed,
retries=SCAN_STATUS_POLL_RETRIES,
delay=SCAN_STATUS_POLL_DELAY,
- backoff=1, logger=logger)
+ backoff=1)
def get_status():
return session.get(status_url)
@@ -80,9 +80,7 @@ def test_upload(self):
'Staging part1 \n'
'Stripping part1 \n'
'Snapping snap-test_1.0_amd64.snap\n'
- 'Snapped snap-test_1.0_amd64.snap\n'
- 'Uploading snap-test_1.0_amd64.snap\n'
- 'snap-test_1.0_amd64.snap upload complete\n',
+ 'Snapped snap-test_1.0_amd64.snap\n',
self.fake_logger.output)
self.assertTrue(os.path.exists(common.get_stagedir()),
@@ -114,9 +112,7 @@ def test_upload_failed(self):
'Staging part1 \n'
'Stripping part1 \n'
'Snapping snap-test_1.0_amd64.snap\n'
- 'Snapped snap-test_1.0_amd64.snap\n'
- 'Uploading snap-test_1.0_amd64.snap\n'
- 'snap-test_1.0_amd64.snap upload failed\n',
+ 'Snapped snap-test_1.0_amd64.snap\n',
self.fake_logger.output)
self.assertTrue(os.path.exists(common.get_stagedir()),
@@ -140,11 +136,6 @@ def test_just_upload_if_snap_file_exists(self):
upload.main()
- self.assertEqual(
- 'Uploading snap-test_1.0_amd64.snap\n'
- 'snap-test_1.0_amd64.snap upload complete\n',
- self.fake_logger.output)
-
# stages are not build if snap file already exists
self.assertFalse(os.path.exists(common.get_stagedir()),
'Expected a stage directory')
Oops, something went wrong.