Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Delta Between Two Patch Sets: sitescripts/extensions/bin/createNightlies.py

Issue 29374637: Issue 4549 - Implement the Windows Store API to upload development builds (Closed)
Left Patch Set: Switch to urllib2 Created Feb. 15, 2017, 3:41 a.m.
Right Patch Set: Rebase Created Feb. 16, 2017, 1:14 p.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
Left: Side by side diff | Download
Right: Side by side diff | Download
« no previous file with change/comment | « ensure_dependencies.py ('k') | sitescripts/extensions/test/sitescripts.ini.template » ('j') | no next file with change/comment »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
LEFTRIGHT
1 # This file is part of the Adblock Plus web scripts, 1 # This file is part of the Adblock Plus web scripts,
2 # Copyright (C) 2006-2016 Eyeo GmbH 2 # Copyright (C) 2006-2016 Eyeo GmbH
3 # 3 #
4 # Adblock Plus is free software: you can redistribute it and/or modify 4 # Adblock Plus is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License version 3 as 5 # it under the terms of the GNU General Public License version 3 as
6 # published by the Free Software Foundation. 6 # published by the Free Software Foundation.
7 # 7 #
8 # Adblock Plus is distributed in the hope that it will be useful, 8 # Adblock Plus is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
(...skipping 24 matching lines...) Expand all
35 import shutil 35 import shutil
36 import struct 36 import struct
37 import subprocess 37 import subprocess
38 import sys 38 import sys
39 import tempfile 39 import tempfile
40 import time 40 import time
41 from urllib import urlencode 41 from urllib import urlencode
42 import urllib2 42 import urllib2
43 import urlparse 43 import urlparse
44 import zipfile 44 import zipfile
45 import contextlib
45 46
46 from xml.dom.minidom import parse as parseXml 47 from xml.dom.minidom import parse as parseXml
47 48
48 from sitescripts.extensions.utils import ( 49 from sitescripts.extensions.utils import (
49 compareVersions, Configuration, 50 compareVersions, Configuration,
50 writeAndroidUpdateManifest 51 writeAndroidUpdateManifest
51 ) 52 )
52 from sitescripts.utils import get_config, get_template 53 from sitescripts.utils import get_config, get_template
53 54
54 MAX_BUILDS = 50 55 MAX_BUILDS = 50
56
57
58 # Google and Microsoft APIs use HTTP error codes with error message in
59 # body. So we add the response body to the HTTPError to get more
60 # meaningful error messages.
61 class HTTPErrorBodyHandler(urllib2.HTTPDefaultErrorHandler):
62 def http_error_default(self, req, fp, code, msg, hdrs):
63 raise urllib2.HTTPError(req.get_full_url(), code,
64 '{}\n{}'.format(msg, fp.read()), hdrs, fp)
55 65
56 66
57 class NightlyBuild(object): 67 class NightlyBuild(object):
58 """ 68 """
59 Performs the build process for an extension, 69 Performs the build process for an extension,
60 generating changelogs and documentation. 70 generating changelogs and documentation.
61 """ 71 """
62 72
63 def __init__(self, config): 73 def __init__(self, config):
64 """ 74 """
(...skipping 399 matching lines...) Expand 10 before | Expand all | Expand 10 after
464 474
465 try: 475 try:
466 urllib2.urlopen(request).close() 476 urllib2.urlopen(request).close()
467 except urllib2.HTTPError as e: 477 except urllib2.HTTPError as e:
468 try: 478 try:
469 logging.error(e.read()) 479 logging.error(e.read())
470 finally: 480 finally:
471 e.close() 481 e.close()
472 raise 482 raise
473 483
474 # Google and Microsoft APIs use HTTP error codes with error message in
475 # body. So we add the response body to the HTTPError to get more
476 # meaningful error messages.
477
478 class HTTPErrorBodyHandler(urllib2.HTTPDefaultErrorHandler):
Sebastian Noack 2017/02/15 11:20:04 I'd put this class on the top-level rather than ne
Oleksandr 2017/02/16 07:13:12 Done.
479 def http_error_default(self, req, fp, code, msg, hdrs):
480 raise urllib2.HTTPError(req.get_full_url(), code,
481 '{}\n{}'.format(msg, fp.read()), hdrs, fp)
482
483 def uploadToChromeWebStore(self): 484 def uploadToChromeWebStore(self):
484 485
485 opener = urllib2.build_opener(NightlyBuild.HTTPErrorBodyHandler) 486 opener = urllib2.build_opener(HTTPErrorBodyHandler)
486 487
487 # use refresh token to obtain a valid access token 488 # use refresh token to obtain a valid access token
488 # https://developers.google.com/accounts/docs/OAuth2WebServer#refresh 489 # https://developers.google.com/accounts/docs/OAuth2WebServer#refresh
489 490
490 response = json.load(opener.open( 491 response = json.load(opener.open(
491 'https://accounts.google.com/o/oauth2/token', 492 'https://accounts.google.com/o/oauth2/token',
492 493
493 urlencode([ 494 urlencode([
494 ('refresh_token', self.config.refreshToken), 495 ('refresh_token', self.config.refreshToken),
495 ('client_id', self.config.clientID), 496 ('client_id', self.config.clientID),
(...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after
531 request.add_header('Authorization', auth_token) 532 request.add_header('Authorization', auth_token)
532 request.add_header('x-goog-api-version', '2') 533 request.add_header('x-goog-api-version', '2')
533 request.add_header('Content-Length', '0') 534 request.add_header('Content-Length', '0')
534 535
535 response = json.load(opener.open(request)) 536 response = json.load(opener.open(request))
536 537
537 if any(status not in ('OK', 'ITEM_PENDING_REVIEW') for status in respons e['status']): 538 if any(status not in ('OK', 'ITEM_PENDING_REVIEW') for status in respons e['status']):
538 raise Exception({'status': response['status'], 'statusDetail': respo nse['statusDetail']}) 539 raise Exception({'status': response['status'], 'statusDetail': respo nse['statusDetail']})
539 540
540 def get_windows_store_access_token(self): 541 def get_windows_store_access_token(self):
541
Sebastian Noack 2017/02/15 11:20:04 Nit: we don't add blank lines at the top of any bl
Oleksandr 2017/02/16 07:13:12 Done.
542 auth_token = ''
543 # use refresh token to obtain a valid access token 542 # use refresh token to obtain a valid access token
544 # https://docs.microsoft.com/en-us/azure/active-directory/active-directo ry-protocols-oauth-code#refreshing-the-access-tokens 543 # https://docs.microsoft.com/en-us/azure/active-directory/active-directo ry-protocols-oauth-code#refreshing-the-access-tokens
545 server = 'https://login.microsoftonline.com' 544 server = 'https://login.microsoftonline.com'
546 token_path = '{}/{}/oauth2/token'.format(server, self.config.tenantID) 545 token_path = '{}/{}/oauth2/token'.format(server, self.config.tenantID)
547 546
548 opener = urllib2.build_opener(NightlyBuild.HTTPErrorBodyHandler) 547 opener = urllib2.build_opener(HTTPErrorBodyHandler)
549 post_data = urlencode([ 548 post_data = urlencode([
550 ('refresh_token', self.config.refreshToken), 549 ('refresh_token', self.config.refreshToken),
551 ('client_id', self.config.clientID), 550 ('client_id', self.config.clientID),
552 ('client_secret', self.config.clientSecret), 551 ('client_secret', self.config.clientSecret),
553 ('grant_type', 'refresh_token'), 552 ('grant_type', 'refresh_token'),
554 ('resource', 'https://graph.windows.net') 553 ('resource', 'https://graph.windows.net')
555 ]) 554 ])
556 request = urllib2.Request(token_path, post_data) 555 request = urllib2.Request(token_path, post_data)
557 response = json.load(opener.open(request)) 556 with contextlib.closing(opener.open(request)) as response:
Sebastian Noack 2017/02/15 11:20:04 The response has to be closed, and this time for r
Oleksandr 2017/02/16 07:13:11 Done.
558 auth_token = '{0[token_type]} {0[access_token]}'.format(response) 557 data = json.load(response)
558 auth_token = '{0[token_type]} {0[access_token]}'.format(data)
559 559
560 return auth_token 560 return auth_token
561 561
562 def upload_appx_file_to_windows_store(self, file_upload_url): 562 def upload_appx_file_to_windows_store(self, file_upload_url):
563
564 # Add .appx file to a .zip file 563 # Add .appx file to a .zip file
565 zip_path = os.path.splitext(self.path)[0] + '.zip' 564 zip_path = os.path.splitext(self.path)[0] + '.zip'
566 with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf: 565 with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
567 zf.write(self.path, os.path.basename(self.path)) 566 zf.write(self.path, os.path.basename(self.path))
568 567
569 # Upload that .zip file 568 # Upload that .zip file
570 file_upload_url = file_upload_url.replace('+', '%2B') 569 file_upload_url = file_upload_url.replace('+', '%2B')
571 request = urllib2.Request(file_upload_url) 570 request = urllib2.Request(file_upload_url)
572 request.get_method = lambda: 'PUT' 571 request.get_method = lambda: 'PUT'
573 request.add_header('x-ms-blob-type', 'BlockBlob') 572 request.add_header('x-ms-blob-type', 'BlockBlob')
574 573
575 opener = urllib2.build_opener(NightlyBuild.HTTPErrorBodyHandler) 574 opener = urllib2.build_opener(HTTPErrorBodyHandler)
576 575
577 with open(zip_path, 'rb') as file: 576 with open(zip_path, 'rb') as file:
578
579 request.add_header('Content-Length', 577 request.add_header('Content-Length',
580 os.fstat(file.fileno()).st_size - file.tell()) 578 os.fstat(file.fileno()).st_size - file.tell())
581 request.add_data(file) 579 request.add_data(file)
582 580 opener.open(request).close()
583 opener.open(request) 581
Sebastian Noack 2017/02/15 11:20:04 Please don't forget closing the response.
584 582 # Clone the previous submission for the new one. Largely based on code
583 # from https://msdn.microsoft.com/en-us/windows/uwp/monetize/python-code-exa mples-for-the-windows-store-submission-api#create-an-app-submission
585 def upload_to_windows_store(self): 584 def upload_to_windows_store(self):
586 585 opener = urllib2.build_opener(HTTPErrorBodyHandler)
587 auth_token = self.get_windows_store_access_token() 586
588 587 headers = {'Authorization': self.get_windows_store_access_token(),
589 opener = urllib2.build_opener(NightlyBuild.HTTPErrorBodyHandler)
590
591 # Clone the previous submission for the new one. Largely based on code
592 # from https://msdn.microsoft.com/en-us/windows/uwp/monetize/python-code -examples-for-the-windows-store-submission-api#create-an-app-submission
593 headers = {'Authorization': auth_token,
594 'Content-type': 'application/json'} 588 'Content-type': 'application/json'}
595
596 api_server = 'https://manage.devcenter.microsoft.com'
Sebastian Noack 2017/02/15 11:20:04 Since you only use this variable once, perhaps inl
597 589
598 # Get application 590 # Get application
599 # https://docs.microsoft.com/en-us/windows/uwp/monetize/get-an-app 591 # https://docs.microsoft.com/en-us/windows/uwp/monetize/get-an-app
600 api_path = '{}/v1.0/my/applications/{}'.format( 592 api_path = '{}/v1.0/my/applications/{}'.format(
601 api_server, 593 'https://manage.devcenter.microsoft.com',
602 self.config.devbuildGalleryID 594 self.config.devbuildGalleryID
603 ) 595 )
604 596
605 request = urllib2.Request(api_path, None, headers) 597 request = urllib2.Request(api_path, None, headers)
606 app_obj = json.load(opener.open(request)) 598 with contextlib.closing(opener.open(request)) as response:
Sebastian Noack 2017/02/15 11:20:05 Again, please make sure to close the response.
599 app_obj = json.load(response)
607 600
608 # Delete existing in-progress submission 601 # Delete existing in-progress submission
609 # https://docs.microsoft.com/en-us/windows/uwp/monetize/delete-an-app-su bmission 602 # https://docs.microsoft.com/en-us/windows/uwp/monetize/delete-an-app-su bmission
610 submissions_path = api_path + '/submissions' 603 submissions_path = api_path + '/submissions'
611 if 'pendingApplicationSubmission' in app_obj: 604 if 'pendingApplicationSubmission' in app_obj:
612 remove_id = app_obj['pendingApplicationSubmission']['id'] 605 remove_id = app_obj['pendingApplicationSubmission']['id']
613 remove_path = '{}/{}'.format(submissions_path, remove_id) 606 remove_path = '{}/{}'.format(submissions_path, remove_id)
614 request = urllib2.Request(remove_path, '', headers) 607 request = urllib2.Request(remove_path, '', headers)
615 request.get_method = lambda: 'DELETE' 608 request.get_method = lambda: 'DELETE'
616 opener.open(request) 609 opener.open(request).close()
617 610
618 # Create submission 611 # Create submission
619 # https://msdn.microsoft.com/en-us/windows/uwp/monetize/create-an-app-su bmission 612 # https://msdn.microsoft.com/en-us/windows/uwp/monetize/create-an-app-su bmission
620 request = urllib2.Request(submissions_path, '', headers) 613 request = urllib2.Request(submissions_path, '', headers)
621 request.get_method = lambda: 'POST' 614 request.get_method = lambda: 'POST'
622 submission = json.load(opener.open(request)) 615 with contextlib.closing(opener.open(request)) as response:
Sebastian Noack 2017/02/15 11:20:04 Again, please make sure to close the response.
Oleksandr 2017/02/16 07:13:11 Done.
616 submission = json.load(response)
623 617
624 submission_id = submission['id'] 618 submission_id = submission['id']
625 file_upload_url = submission['fileUploadUrl'] 619 file_upload_url = submission['fileUploadUrl']
626 620
627 # Update submission 621 new_submission_path = '{}/{}'.format(submissions_path,
628 old_submission = submission['applicationPackages'][0] 622 submission_id)
629 old_submission['fileStatus'] = 'PendingDelete'
630 submission['applicationPackages'].append(
Sebastian Noack 2017/02/15 11:20:04 Perhaps cache submission['applicationPackages'] in
631 {'fileStatus': 'PendingUpload'})
632 added_submission = submission['applicationPackages'][1]
633 added_submission['fileName'] = os.path.basename(self.path)
634
635 old_min_sys_ram = old_submission['minimumSystemRam']
636 added_submission['minimumSystemRam'] = old_min_sys_ram
637
638 old_directx_version = old_submission['minimumDirectXVersion']
639 added_submission['minimumDirectXVersion'] = old_directx_version
Sebastian Noack 2017/02/15 11:20:05 Why do we modify the added_submission dictionary a
Oleksandr 2017/02/16 07:13:10 Interesting. Initially it was required to modify t
640
641 new_submission_path = '{}/{}'.format(
642 submissions_path, submission_id)
643 623
644 request = urllib2.Request(new_submission_path, None, headers) 624 request = urllib2.Request(new_submission_path, None, headers)
645 opener.open(request) 625 opener.open(request).close()
646 626
647 self.upload_appx_file_to_windows_store(file_upload_url) 627 self.upload_appx_file_to_windows_store(file_upload_url)
648 628
649 # Commit submission 629 # Commit submission
650 # https://msdn.microsoft.com/en-us/windows/uwp/monetize/commit-an-app-su bmission 630 # https://msdn.microsoft.com/en-us/windows/uwp/monetize/commit-an-app-su bmission
651 commit_path = '{}/commit'.format(new_submission_path) 631 commit_path = '{}/commit'.format(new_submission_path)
652 request = urllib2.Request(commit_path, '', headers) 632 request = urllib2.Request(commit_path, '', headers)
653 request.get_method = lambda: 'POST' 633 request.get_method = lambda: 'POST'
654 submission = json.load(opener.open(request)) 634 with contextlib.closing(opener.open(request)) as response:
Sebastian Noack 2017/02/15 11:20:04 Again, please make sure to close the response.
Oleksandr 2017/02/16 07:13:11 Done.
635 submission = json.load(response)
655 636
656 if submission['status'] != 'CommitStarted': 637 if submission['status'] != 'CommitStarted':
657 raise Exception({'status': submission['status'], 638 raise Exception({'status': submission['status'],
658 'statusDetails': submission['statusDetails']}) 639 'statusDetails': submission['statusDetails']})
659 640
660 def run(self): 641 def run(self):
661 """ 642 """
662 Run the nightly build process for one extension 643 Run the nightly build process for one extension
663 """ 644 """
664 try: 645 try:
(...skipping 78 matching lines...) Expand 10 before | Expand all | Expand 10 after
743 except Exception as ex: 724 except Exception as ex:
744 logging.error('The build for %s failed:', repo) 725 logging.error('The build for %s failed:', repo)
745 logging.exception(ex) 726 logging.exception(ex)
746 727
747 file = open(nightlyConfigFile, 'wb') 728 file = open(nightlyConfigFile, 'wb')
748 nightlyConfig.write(file) 729 nightlyConfig.write(file)
749 730
750 731
751 if __name__ == '__main__': 732 if __name__ == '__main__':
752 main() 733 main()
LEFTRIGHT

Powered by Google App Engine
This is Rietveld