""" ITunes Reporter. Util module for working with Apple's Reporter Java application to download Apple Sales & Trends / Payments & Financial Reports. Requires following environment variables: REPORTER_JAR - path to Reporter jar file (ex. /Foo/Bar/Reporter.jar) IODA_VENDOR_PROP_FILE - Property file for IODA Vendor ORCHARD_VENDOR_PROP_FILE - Property file for Orchard Vendor Note Property files must be in same directory where files will be downloaded, it cannot be a relative or absolute path (this is a Reporter.jar requirement). General Guide http://help.apple.com/itc/contentreporterguide/#/itc0f2481229 Property Files Guide http://help.apple.com/itc/contentreporterguide/#/itc960331fbb Report Definitions Guide https://help.apple.com/itc/contentsalesandtrends/#/itc8b8ced395 @todo(pkuong): 1. report_type is being used multiple times. Suggesting to add constants for them. """ from collections import namedtuple from contextlib import contextmanager from datetime import datetime import os import shlex import subprocess from typing import List import xml.etree.ElementTree as ET from dateutil.relativedelta import relativedelta from secrets_manager.swf_ext import SWFSecretsManager import xmltodict # todo(jpenner): move to config file if we make this module its own pkg # 'FANCLUB', 'VENDOR_ID': '85356999', 'ACCOUNT': 5516 was removed from VENDORS # due to DS-3321 VENDORS = { 'ORCHARD': { 'VENDOR_ID': '80028967', 'ACCOUNT': 1526 }, 'IODA': { 'VENDOR_ID': '80029727', 'ACCOUNT': 1682 }, 'BFM_ORCHARD': { 'VENDOR_ID': '85420853', 'ACCOUNT': 2170 }, 'BFM_IODA': { 'VENDOR_ID': '80031813', 'ACCOUNT': 2170 }, 'FINETUNES': { 'VENDOR_ID': '80032046', 'ACCOUNT': 2243 }, 'PRIDDIS': { 'VENDOR_ID': '80035150', 'ACCOUNT': 2913 }, 'RELAPSE': { 'VENDOR_ID': '80032084', 'ACCOUNT': 2246 }, 'RDA': { 'VENDOR_ID': '80031727', 'ACCOUNT': 2151 }, 'IRIS': { 'VENDOR_ID': '80028547', 'ACCOUNT': 1522 }, 'PHONOFILE': { 'VENDOR_ID': '80032543', 'ACCOUNT': 2341 }, 'DKUA': { 'VENDOR_ID': '80034465', 'ACCOUNT': 2969 }, 'AWAL': { 'VENDOR_ID': '80031998', 'ACCOUNT': 2234 }, 'ARTSPAGES': { 'VENDOR_ID': '80032827', 'ACCOUNT': 2452 }, 'ALTAFONTE': { 'VENDOR_ID': '85320019', 'ACCOUNT': 2818 }, } ACCESS_TOKEN_SECRET = 'AccessToken' PROP_FILE_TEMPLATE = ("""AccessToken={access_token} Account={account} Mode=Robot.xml SalesUrl=https://reportingitc-reporter.apple.com/reportservice/sales/v1 FinanceUrl=\ https://reportingitc-reporter.apple.com/reportservice/finance/v1""") REPORTER_JAR = os.getenv('REPORTER_JAR', '') ENV = os.environ.get('Environment', 'dev') ReportVersions = namedtuple( 'ReportVersions', [ 'amEvent', # not in use (was used by deprecated apple_music flow) 'amContent', 'amContainer', 'amContentDemographics', 'amStreams', 'amStreamsSummary', 'amSubscriptionReference', 'itunes', 'amNonRoyaltyStreams', 'amNonRoyaltySummaryStreams', 'amShazam', 'amLibraryEvents', 'amTotalLibraryAdds', 'amArtists', 'amPlaylists', 'amSongs' ] ) REPORT_VERSIONS = ReportVersions( amEvent='1_2', # not in use (was used by deprecated apple_music flow) amContent='1_2', amContainer='1_0', amContentDemographics='1_2', amStreams='1_3', amStreamsSummary='1_0', amSubscriptionReference='', itunes='1_2', amNonRoyaltyStreams='1_1', amNonRoyaltySummaryStreams='1_0', amShazam='1_2', amLibraryEvents='1_0', amTotalLibraryAdds='1_0', amArtists='1_0', amPlaylists='1_0', amSongs='1_0', ) REPORT_VERSIONS_BY_DATES = dict( amEvent={ datetime(2019, 5, 13): '1_2', datetime(2015, 1, 1): '1_1', }, amContent={ datetime(2019, 5, 13): '1_2', datetime(2017, 11, 16): '1_1', datetime(2015, 1, 1): '1_0', }, amContainer={ datetime(2025, 5, 1): '1_1', datetime(2021, 4, 8): '1_0', }, amContentDemographics={ datetime(2015, 1, 1): '1_0', datetime(2023, 4, 24): '1_2', }, amStreams={ datetime(2020, 11, 9): '1_3', datetime(2019, 5, 13): '1_2', datetime(2017, 11, 16): '1_1', datetime(2015, 1, 1): '1_0', }, amStreamsSummary={ datetime(2025, 5, 1): '1_2', datetime(2021, 6, 7): '1_1', datetime(2021, 3, 1): '1_0', }, amSubscriptionReference={ datetime(2020, 11, 9): '', }, itunes={ datetime(2022, 9, 5): '1_3', datetime(2019, 5, 13): '1_2', datetime(2015, 1, 1): '1_1', }, amNonRoyaltyStreams={ datetime(2019, 5, 13): '1_1', datetime(2015, 1, 1): '1_0', }, amNonRoyaltySummaryStreams={ datetime(2025, 5, 1): '1_2', datetime(2021, 6, 7): '1_1', datetime(2021, 4, 8): '1_0', }, amShazam={ datetime(2015, 1, 1): '1_0', datetime(2019, 11, 1): '1_1', datetime(2020, 2, 27): '1_2', }, amLibraryEvents={ datetime(2020, 3, 8): '1_0', }, amTotalLibraryAdds={ datetime(2020, 3, 8): '1_0', }, amArtists={ datetime(2019, 5, 13): '1_0', }, amPlaylists={ datetime(2019, 5, 13): '1_0', }, amSongs={ datetime(2019, 5, 13): '1_0', }, ) REPORT_VERSIONS_BY_DATES_AWAL = dict( amEvent={ datetime(2019, 5, 13): '1_2', datetime(2015, 1, 1): '1_1', }, amContent={ datetime(2019, 5, 13): '1_2', datetime(2017, 12, 18): '1_1', datetime(2015, 1, 1): '1_0', }, amContainer={ datetime(2025, 5, 1): '1_1', datetime(2021, 4, 8): '1_0', }, amContentDemographics={ datetime(2015, 1, 1): '1_0', datetime(2023, 4, 24): '1_2', }, amStreams={ datetime(2019, 5, 13): '1_2', datetime(2017, 12, 18): '1_1', datetime(2015, 1, 1): '1_0', }, amStreamsSummary={ datetime(2025, 5, 1): '1_2', datetime(2021, 6, 7): '1_1', datetime(2021, 3, 1): '1_0', }, amSubscriptionReference={ datetime(2020, 11, 9): '', }, itunes={ # 2022-10-04: since reporter have report for only last year, # it's unable to check when V1_3 actually starts, but at least # since 2021-10-04 AWAL reports are available under this version datetime(2021, 10, 4): '1_3', datetime(2019, 5, 13): '1_2', datetime(2015, 1, 1): '1_1', }, amNonRoyaltyStreams={ datetime(2019, 5, 13): '1_1', datetime(2015, 1, 1): '1_0', }, amNonRoyaltySummaryStreams={ datetime(2025, 5, 1): '1_2', datetime(2021, 6, 7): '1_1', datetime(2021, 4, 8): '1_0', }, amShazam={ datetime(2015, 1, 1): '1_0', datetime(2019, 12, 10): '1_1', datetime(2021, 8, 17): '1_2', }, amLibraryEvents={ datetime(2020, 3, 8): '1_0', }, amTotalLibraryAdds={ datetime(2020, 3, 8): '1_0', }, amArtists={ datetime(2019, 5, 13): '1_0', }, amPlaylists={ datetime(2019, 5, 13): '1_0', }, amSongs={ datetime(2019, 5, 13): '1_0', }, ) class ReporterException(Exception): """Reporter Exception. Reporter exception that capture the error code and message returned by the Reporter App. For Error Codes see: http://help.apple.com/itc/contentreporterguide/#/itc1d0cb44bd """ INVALID_TOKEN = 124 INVALID_USERNAME_OR_PASSWORD = 108 UNKNOWN_ERROR = 130 FILE_UNAVAILABLE_ERR_CODE = 210 UNEXPECTED_ERROR = 212 NO_SALES_FOR_DATE = 213 NO_REPORT_IS_AVAILABLE = 220 # Stop reporter after catching one of these exceptions NO_RETRY_EXCEPTIONS = ( INVALID_USERNAME_OR_PASSWORD, INVALID_TOKEN, NO_SALES_FOR_DATE ) def __init__(self, message, error_code): """Create an EmrAggregateLoad flow. Args: message (str): Error message from Reporter xml. error_code (int): Error code from Reporter xml. """ super(ReporterException, self).__init__(message) self.error_code = error_code class ItunesReporter(object): """Class encapsulating convenience methods to handle iTunes reports.""" def __init__( self, reporter_jar, vendor_id, account, date=None, report_versions=None, country=None, access_token=None): """Initialize an ITunesReporter object. Object is vendor & date specific. Args: reporter_jar (str): Path to Reporter app jar file. vendor_id (int): Vendor id supplied by Apple. account (int): Apple Account number. date (str): YYYY-MM-DD str for report requested (optional, but will break if not set when methods are executed). report_versions (dict): report versions (ITunes reporter specific). country (str): Country code. access_token (str): API token (Optional). """ self.reporter_jar = reporter_jar self.vendor_id = vendor_id self.account = account self.date = date self.access_token = access_token # Date is not needed for getVendorsAndRegions command # more information: # https://help.apple.com/itc/contentreporterguide/#/itc0bc116f31 if date: # There can be available only the latest version of a report, # but it will download the previous version the file of the report # if the current report version was not available at that moment # A message can be like 'The report for the date you requested # was not available on version 1_1 at the time. # Successfully downloaded '. if (not report_versions and VENDORS['AWAL']['VENDOR_ID']) == vendor_id: report_versions = REPORT_VERSIONS_BY_DATES_AWAL else: report_versions = REPORT_VERSIONS_BY_DATES self.file_report_versions = self.get_report_versions( report_versions, date) self.current_report_versions = self.get_report_versions( report_versions, date, last=True) self.country = country def get_report_versions(self, report_versions, date, last=False): """Get ReportVersions with correct versions of reports. Args: date (str): YYYY-MM-DD str for report requested. report_versions (dict): report versions (ITunes reporter specific). last (bool): Return only the latest version of reports. Returns: ReportVersions: The namedtuple with reports and correct versions. """ current_date = datetime.strptime(date, '%Y-%m-%d') result = {} for report, versions in report_versions.items(): for date, version in sorted(versions.items(), reverse=True): if current_date >= date or last: result[report] = version break else: result[report] = None return ReportVersions(**result) @property def date_yymm(self): """Get date property in YYMM format. Returns: str: Date in YYMM format. """ return self.date_obj.strftime('%Y%m') @property def date_rm_dash(self): """Get date property without dashes ('-'). Returns: str: Date in YYYYMMDD format. """ return self.date.replace('-', '') @property def date_mmyy(self): """Get date property in MMYY format. Returns: str: Date in MMYY format. """ return self.date_obj.strftime('%m%y') @property def date_obj(self): """Get date object. Returns: datetime: Date object. """ if self.date: return datetime.strptime(self.date, '%Y-%m-%d') else: raise ValueError('Must provide the date.') @property def apple_music_event_file(self): """Get filename for amEvent report. Returns: str: Filename of the report. """ return 'AppleMusic_Event_{vendor_id}_{date}_V{version}.txt.gz'.format( vendor_id=self.vendor_id, date=self.date_rm_dash, version=self.file_report_versions.amEvent) @property def apple_music_content_file(self): """Get filename for amContent report. Returns: str: Filename of the report. """ if self.file_report_versions.amContent == '1_0': file = 'AppleMusic_Content_{vendor_id}_{date}.txt.gz' else: file = 'AppleMusic_Content_{vendor_id}_{date}_V{version}.txt.gz' return file.format( vendor_id=self.vendor_id, date=self.date_rm_dash, version=self.file_report_versions.amContent) @property def apple_music_content_demographics_file(self): """Get filename for amContentDemographics report. Returns: str: Filename of the report. """ if self.file_report_versions.amContentDemographics == '1_0': file = 'AppleMusic_ContentDemographics_{vendor_id}_{date}.txt.gz' else: file = \ 'AppleMusic_ContentDemographics_{vendor_id}_{date}_' \ 'V{version}.txt.gz' return file.format( vendor_id=self.vendor_id, date=self.date_rm_dash, version=self.file_report_versions.amContentDemographics) @property def apple_music_shazam_file(self): """Get filename for amShazam report. Returns: str: Filename of the report. """ return ( 'AMShazam_{vendor_id}_{date}_V{version}.txt.gz').format( vendor_id=self.vendor_id, date=self.date_rm_dash, version=self.file_report_versions.amShazam) @property def apple_music_streams_file(self): """Get filename for amStreams report. Returns: str: Filename of the report. """ if self.file_report_versions.amStreams == '1_0': file = 'AppleMusic_Streams_{vendor_id}_{date}.txt.gz' else: file = 'AppleMusic_Streams_{vendor_id}_{date}_V{version}.txt.gz' return file.format( vendor_id=self.vendor_id, date=self.date_rm_dash, version=self.file_report_versions.amStreams) @property def apple_music_summary_streams_file(self): """Get filename for Summary amStreams report. Returns: str: Filename of the report. """ return ( 'AppleMusic_SummaryStreams_{vendor_id}_{date}_V{version}.txt.gz' ).format( vendor_id=self.vendor_id, date=self.date_rm_dash, version=self.file_report_versions.amStreamsSummary) @property def apple_music_summary_non_royalty_streams_file(self): """Get filename for Summary amNonRoyaltyStreams report. Returns: str: Filename of the report. """ return ( 'AppleMusic_NonRoyaltySummaryStreams_' '{vendor_id}_{date}_V{version}.txt.gz' ).format( vendor_id=self.vendor_id, date=self.date_rm_dash, version=self.file_report_versions.amNonRoyaltySummaryStreams) @property def apple_music_container_file(self): """Get filename for amContainer report. Returns: str: Filename of the report. """ return ( 'AppleMusic_ContainerSummary_{vendor_id}_{date}_V{version}.txt.gz' ).format( vendor_id=self.vendor_id, date=self.date_rm_dash, version=self.file_report_versions.amContainer) @property def apple_music_sub_reference_file(self): """Get filename for amSubscriptionReference report. Returns: str: Filename of the report. """ return 'AppleMusic_SubscriptionReference_{vendor_id}_{date}.txt.gz'\ .format( vendor_id=self.vendor_id, date=self.date_rm_dash) @property def apple_music_libraryevents_file(self): """Get filename for amLibraryEvents report. Returns: str: Filename of the report. """ return ( 'AppleMusic_LibraryEvents_{vendor_id}_{date}.txt.gz').format( vendor_id=self.vendor_id, date=self.date_rm_dash) @property def apple_music_totallibraryadds_file(self): """Get filename for amTotalLibraryAdds report. Returns: str: Filename of the report. """ return ( 'AppleMusic_TotalLibraryAdds_{vendor_id}_{date}.txt.gz').format( vendor_id=self.vendor_id, date=self.date_rm_dash) @property def apple_music_artists_file(self): """Get filename for amArtists report. Returns: str: Filename of the report. """ return ( 'AppleMusic_ArtistUniques_{vendor_id}_{date}.txt.gz').format( vendor_id=self.vendor_id, date=self.date_rm_dash) @property def apple_music_playlists_file(self): """Get filename for amPlaylists report. Returns: str: Filename of the report. """ return ( 'AppleMusic_PlaylistUniques_{vendor_id}_{date}.txt.gz').format( vendor_id=self.vendor_id, date=self.date_rm_dash) @property def apple_music_songs_file(self): """Get filename for amSongs report. Returns: str: Filename of the report. """ return ( 'AppleMusic_SongUniques_{vendor_id}_{date}.txt.gz').format( vendor_id=self.vendor_id, date=self.date_rm_dash) @property def itunes_file(self): """Get filename for iTunes file. Returns: str: Filename of the report. """ return 'D_D_{vendor_id}_{date}_V{version}.txt.gz'.format( vendor_id=self.vendor_id, date=self.date_rm_dash, version=self.file_report_versions.itunes) @property def apple_music_summary_file(self): """Get filename for Apple Music Summary file. Returns: str: Filename of the report. """ return 'S2_{vendor_id}_{date}_{country}.txt.gz'.format( vendor_id=self.vendor_id, date=self.date_mmyy, country=self.country) @property def apple_music_non_royalty_streams_file(self): """Get filename for amStreams report. Returns: str: Filename of the report. """ if self.file_report_versions.amNonRoyaltyStreams == '1_0': file = 'AppleMusic_NonRoyaltyStreams_{vendor_id}_{date}.txt.gz' else: file = ('AppleMusic_NonRoyaltyStreams_{vendor_id}_{date}' '_V{version}.txt.gz') return file.format( vendor_id=self.vendor_id, date=self.date_rm_dash, version=self.file_report_versions.amNonRoyaltyStreams) @property def apple_podcasts_sales_summary_file(self): """Get filename for apple podcast sales summary report. Returns: str: Filename of the report. """ return ( 'ApplePodcasts_SalesSummary_{vendor_id}_{date}.txt.gz').format( vendor_id=self.vendor_id, date=self.date_rm_dash) @property def apple_podcasts_sales_summary_weekly_file(self): """Get filename for apple podcast sales summary weekly report. Returns: str: Filename of the report. """ return ( 'ApplePodcasts_SalesSummary_Weekly_{vendor_id}_{date}.txt.gz' ).format( vendor_id=self.vendor_id, date=self.date_rm_dash) @property def apple_podcasts_sales_summary_monthly_file(self): """Get filename for apple podcast sales summary monthly report. Returns: str: Filename of the report. """ return ( 'ApplePodcasts_SalesSummary_Monthly_{vendor_id}_{date}.txt.gz' ).format( vendor_id=self.vendor_id, date=self.date_rm_dash) @property def apple_podcasts_subscription_snapshot_file(self): """Get filename for apple podcast subscription snapshot report. Returns: str: Filename of the report. """ return ( 'ApplePodcasts_SubscriptionSnapshot_{vendor_id}_{date}.txt.gz' ).format(vendor_id=self.vendor_id, date=self.date_rm_dash) @property def apple_podcasts_subscription_events_file(self): """Get filename for apple podcast subscription events report. Returns: str: Filename of the report. """ return ( 'ApplePodcasts_SubscriptionEvent_{vendor_id}_{date}.txt.gz' ).format(vendor_id=self.vendor_id, date=self.date_rm_dash) @property def apple_podcasts_content_performance_file(self): """Get filename for apple podcast content performance report. Returns: str: Filename of the report. """ return ( 'ApplePodcasts_ContentPerformance_{vendor_id}_{date}.txt.gz' ).format(vendor_id=self.vendor_id, date=self.date_rm_dash) def download_sales_report(self, report_type): """Download a sales report. If a Reporter error occurs, a ReporterException is raised. Args: report_type (str): Name of report (amEvent, amContent, amStreams). Returns: str: Output from Reporter Application. """ report = { 'amEvent': self.download_apple_music_event_file, 'amContent': self.download_apple_music_content_file, 'amContentDemographics': self.download_apple_music_content_demographics_file, 'amStreams': self.download_apple_music_streams_file, 'amStreamsSummary': self.download_apple_music_summary_streams_file, 'amSubscriptionReference': self.download_apple_music_sub_reference_file, 'itunes': self.download_itunes_file, 'amNonRoyaltyStreams': self.download_apple_music_non_royalty_streams_file, 'amContainer': self.download_apple_music_container_file, 'amNonRoyaltySummaryStreams': self.download_apple_music_summary_non_royalty_streams_file, 'amShazam': self.download_apple_music_shazam_file, 'amLibraryEvents': self.download_apple_music_libraryevents_file, 'amTotalLibraryAdds': self.download_apple_music_totallibraryadds_file, 'amArtists': self.download_apple_music_artists_file, 'amPlaylists': self.download_apple_music_playlists_file, 'amSongs': self.download_apple_music_songs_file, 'apSubscriptionsSales': self.download_apple_podcasts_sales_summary_file, 'apSubscriptionsSnapshot': self.download_apple_podcasts_subscription_snapshot_file, 'apSubscriptionsSnapshotMonthly': self.download_apple_podcasts_subs_snapshot_monthly_file, 'apContentPerformance': self.download_apple_podcasts_content_performance_file, 'apSubscriptionsEvents': self.download_apple_podcasts_subscription_events_file, 'apSubscriptionsEventsWeekly': self.download_apple_podcasts_subscription_events_weekly_file, 'apSubscriptionsEventsMonthly': self.download_apple_podcasts_subscription_events_monthly_file, 'apSubscriptionsSnapshotWeekly': self.download_apple_podcasts_subscription_snapshot_weekly_file, 'apSubscriptionsSalesWeekly': self.download_apple_podcasts_sales_summary_weekly_file, 'apSubscriptionsSalesMonthly': self.download_apple_podcasts_sales_summary_monthly_file }.get(report_type) if not report: raise Exception('Unrecognized Sales Report Type: {}'.format( report_type)) return report() def download_finance_report(self, report_type): """Download a finance report. If a Reporter error occurs, a ReporterException is raised. Args: report_type (str): Name of report (AppleMusicSummary, etc.). Returns: str: Output from Reporter Application. """ if report_type != 'AppleMusicSummary': raise Exception('Unrecognized Finance Report Type: {}'.format( report_type)) return self.download_apple_music_summary_file() def download_apple_music_summary_file(self): """Download AppleMusicSummary report. If a Reporter error occurs, a ReporterException is raised. Returns: str: Output from Reporter Application. """ try: return self._execute_itunes_finance_reporter( 'AppleMusicSummary', self.country, self.date) except ReporterException as exception: if exception.error_code in ReporterException.NO_RETRY_EXCEPTIONS: raise exception else: # badly designed recursive calls # todo: consider redesign return self.download_apple_music_summary_file() except subprocess.CalledProcessError: return self.download_apple_music_summary_file() def download_apple_music_event_file(self): """Download an amEvent report. If a Reporter error occurs, a ReporterException is raised. Returns: str: Output from Reporter Application. """ response = self._execute_itunes_reporter( 'amEvent', 'Detailed', 'Daily', self.date_rm_dash, self.current_report_versions.amEvent) return response def download_apple_music_content_file(self): """Download an amContent report. If a Reporter error occurs, a ReporterException is raised. Returns: str: output from Reporter Application. """ response = self._execute_itunes_reporter( 'amContent', 'Detailed', 'Daily', self.date_rm_dash, self.current_report_versions.amContent) return response def download_apple_music_content_demographics_file(self): """Download an amContentDemographics report. If a Reporter error occurs, a ReporterException is raised. Returns: str: output from Reporter Application. """ response = self._execute_itunes_reporter( 'amContentDemographics', 'Summary', 'Daily', self.date_rm_dash, self.current_report_versions.amContentDemographics) return response def download_apple_music_shazam_file(self): """Download an amShazam report. If a Reporter error occurs, a ReporterException is raised. Returns: str: output from Reporter Application. """ response = self._execute_itunes_reporter( 'amShazam', 'Summary', 'Daily', self.date_rm_dash, self.current_report_versions.amShazam) return response def download_apple_music_streams_file(self): """Download an amStreams report. If a Reporter error occurs, a ReporterException is raised. Returns: str: Output from Reporter Application. """ response = self._execute_itunes_reporter( 'amStreams', 'Detailed', 'Daily', self.date_rm_dash, self.current_report_versions.amStreams) return response def download_apple_music_summary_streams_file(self): """Download an Summary amStreams report. If a Reporter error occurs, a ReporterException is raised. Returns: str: Output from Reporter Application. """ response = self._execute_itunes_reporter( 'amStreams', 'Summary', 'Daily', self.date_rm_dash, self.current_report_versions.amStreamsSummary) return response def download_apple_music_summary_non_royalty_streams_file(self): """Download an Summary amNonRoyaltyStreams report. If a Reporter error occurs, a ReporterException is raised. Returns: str: Output from Reporter Application. """ response = self._execute_itunes_reporter( 'amNonRoyaltyStreams', 'Summary', 'Daily', self.date_rm_dash, self.current_report_versions.amNonRoyaltySummaryStreams) return response def download_apple_music_container_file(self): """Download an Summary amContainer report. If a Reporter error occurs, a ReporterException is raised. Returns: str: Output from Reporter Application. """ response = self._execute_itunes_reporter( 'amContainer', 'Summary', 'Daily', self.date_rm_dash, self.current_report_versions.amContainer) return response def download_apple_music_sub_reference_file(self): """Download an amSubscriptionReference report. If a Reporter error occurs, a ReporterException is raised. Returns: str: Output from Reporter Application. """ response = self._execute_itunes_reporter( 'amSubscriptionReference', 'Summary', 'Daily', self.date_rm_dash, self.current_report_versions.amSubscriptionReference) return response def download_itunes_file(self): """Download an iTunes report. If a Reporter error occurs, a ReporterException is raised. Returns: str: Output from Reporter Application. """ response = self._execute_itunes_reporter( 'Sales', 'Detailed', 'Daily', self.date_rm_dash, self.current_report_versions.itunes) return response def download_apple_music_non_royalty_streams_file(self): """Download an amNonRoyaltyStreams report. If a Reporter error occurs, a ReporterException is raised. Returns: str: Output from Reporter Application. """ response = self._execute_itunes_reporter( 'amNonRoyaltyStreams', 'Detailed', 'Daily', self.date_rm_dash, self.current_report_versions.amNonRoyaltyStreams) return response def download_apple_music_libraryevents_file(self): """Download an amLibraryEvents report. If a Reporter error occurs, a ReporterException is raised. Returns: str: Output from Reporter Application. """ response = self._execute_itunes_reporter( 'amLibraryEvents', 'Detailed', 'Daily', self.date_rm_dash, self.current_report_versions.amLibraryEvents) return response def download_apple_music_totallibraryadds_file(self): """Download an amTotalLibraryAdds report. If a Reporter error occurs, a ReporterException is raised. Returns: str: Output from Reporter Application. """ response = self._execute_itunes_reporter( 'amTotalLibraryAdds', 'Summary', 'Daily', self.date_rm_dash, self.current_report_versions.amTotalLibraryAdds) return response def download_apple_music_artists_file(self): """Download an amArtists report. If a Reporter error occurs, a ReporterException is raised. Returns: str: Output from Reporter Application. """ response = self._execute_itunes_reporter( 'amArtists', 'Uniques', 'Daily', self.date_rm_dash, self.current_report_versions.amArtists) return response def download_apple_music_playlists_file(self): """Download an amPlaylists report. If a Reporter error occurs, a ReporterException is raised. Returns: str: Output from Reporter Application. """ response = self._execute_itunes_reporter( 'amPlaylists', 'Uniques', 'Daily', self.date_rm_dash, self.current_report_versions.amPlaylists) return response def download_apple_music_songs_file(self): """Download an amSongs report. If a Reporter error occurs, a ReporterException is raised. Returns: str: Output from Reporter Application. """ response = self._execute_itunes_reporter( 'amSongs', 'Uniques', 'Daily', self.date_rm_dash, self.current_report_versions.amSongs) return response def download_apple_podcasts_sales_summary_file(self): """Download an apple podcasts subscription sales summary report. If a Reporter error occurs, a ReporterException is raised. Returns: str: Output from Reporter Application. """ response = self._execute_itunes_reporter( 'apSubscriptionsSales', 'Summary', 'Daily', self.date_rm_dash, '1_0') return response def download_apple_podcasts_sales_summary_weekly_file(self): """Download an apple podcasts subscription sales summary weekly report. If a Reporter error occurs, a ReporterException is raised. Returns: str: Output from Reporter Application. """ response = self._execute_itunes_reporter( 'apSubscriptionsSales', 'Summary', 'Weekly', self.date_rm_dash, '1_0') return response def download_apple_podcasts_sales_summary_monthly_file(self): """Download apple podcasts subscription sales summary monthly report. If a Reporter error occurs, a ReporterException is raised. Returns: str: Output from Reporter Application. """ response = self._execute_itunes_reporter( 'apSubscriptionsSales', 'Summary', 'Monthly', self.date_yymm, '1_0') return response def download_apple_podcasts_subscription_snapshot_file(self): """Download an apple podcasts subscription snapshot report. If a Reporter error occurs, a ReporterException is raised. Returns: str: Output from Reporter Application. """ response = self._execute_itunes_reporter( 'apSubscriptionsSnapshot', 'Summary', 'Daily', self.date_rm_dash, '1_0') return response def download_apple_podcasts_subs_snapshot_monthly_file(self): """Download an apple podcasts subscription snapshot monthly report. If a Reporter error occurs, a ReporterException is raised. Returns: str: Output from Reporter Application. """ response = self._execute_itunes_reporter( 'apSubscriptionsSnapshot', 'Summary', 'Monthly', self.date_yymm, '1_0') return response def download_apple_podcasts_subscription_snapshot_weekly_file(self): """Download an apple podcasts subscription snapshot weekly report. If a Reporter error occurs, a ReporterException is raised. Returns: str: Output from Reporter Application. """ response = self._execute_itunes_reporter( 'apSubscriptionsSnapshot', 'Summary', 'Weekly', self.date_rm_dash, '1_0') return response def download_apple_podcasts_subscription_events_file(self): """Download an apple podcasts subscription events report. If a Reporter error occurs, a ReporterException is raised. Returns: str: Output from Reporter Application. """ response = self._execute_itunes_reporter( 'apSubscriptionsEvents', 'Summary', 'Daily', self.date_rm_dash, '1_0') return response def download_apple_podcasts_subscription_events_weekly_file(self): """Download an apple podcasts subscription events weekly report. If a Reporter error occurs, a ReporterException is raised. Returns: str: Output from Reporter Application. """ response = self._execute_itunes_reporter( 'apSubscriptionsEvents', 'Summary', 'Weekly', self.date_rm_dash, '1_0') return response def download_apple_podcasts_subscription_events_monthly_file(self): """Download an apple podcasts subscription events monthly report. If a Reporter error occurs, a ReporterException is raised. Returns: str: Output from Reporter Application. """ response = self._execute_itunes_reporter( 'apSubscriptionsEvents', 'Summary', 'Monthly', self.date_yymm, '1_0') return response def download_apple_podcasts_content_performance_file(self): """Download an apple podcasts content performance report. If a Reporter error occurs, a ReporterException is raised. Returns: str: Output from Reporter Application. """ response = self._execute_itunes_reporter( 'apContentPerformance', 'Summary', 'Monthly', self.date_yymm, '1_0') return response def get_sales_report_file_name(self, report_type): """Get filename for amContent report for the specified date. Args: report_type (str): Name of report (amEvent, amContent, amStreams). Returns: str: Filename of the report. """ report_file = { 'amEvent': self.apple_music_event_file, 'amContent': self.apple_music_content_file, 'amContentDemographics': self.apple_music_content_demographics_file, 'amStreams': self.apple_music_streams_file, 'amStreamsSummary': self.apple_music_summary_streams_file, 'amContainer': self.apple_music_container_file, 'amNonRoyaltySummaryStreams': self.apple_music_summary_non_royalty_streams_file, 'amSubscriptionReference': self.apple_music_sub_reference_file, 'itunes': self.itunes_file, 'amNonRoyaltyStreams': self.apple_music_non_royalty_streams_file, 'amShazam': self.apple_music_shazam_file, 'amLibraryEvents': self.apple_music_libraryevents_file, 'amTotalLibraryAdds': self.apple_music_totallibraryadds_file, 'amArtists': self.apple_music_artists_file, 'amPlaylists': self.apple_music_playlists_file, 'amSongs': self.apple_music_songs_file, 'apSubscriptionsSales': self.apple_podcasts_sales_summary_file, 'apSubscriptionsSalesWeekly': self.apple_podcasts_sales_summary_weekly_file, 'apSubscriptionsSalesMonthly': self.apple_podcasts_sales_summary_monthly_file, 'apSubscriptionsSnapshot': self.apple_podcasts_subscription_snapshot_file, 'apContentPerformance': self.apple_podcasts_content_performance_file, 'apSubscriptionsEvents': self.apple_podcasts_subscription_events_file }.get(report_type) if not report_file: raise Exception('Unrecognized Sales Report Type: {}'.format( report_type)) return report_file def get_finance_report_file_name(self, report_type): """Get filename for Finance report for the specified date. Args: report_type (str): Name of report (AppleMusicSummary, etc.). Returns: str: Filename of the report. """ if report_type != 'AppleMusicSummary': raise Exception('Unrecognized Finance Report Type: {}'.format( report_type)) return self.apple_music_summary_file @contextmanager def create_properties_file(self): """Create properties file for a vendor in current directory. It implemented as context manager in order to cleanup file. """ if not self.access_token: secrets_manager_client = SWFSecretsManager( environment=ENV, service_name='itunesconnect') self.access_token = secrets_manager_client.get_cred( ACCESS_TOKEN_SECRET) prop_file_name = '{vendor_id}_Reporter.properties'.format( vendor_id=self.vendor_id) with open(prop_file_name, 'w+') as prop_file: prop_file.write(PROP_FILE_TEMPLATE.format( access_token=self.access_token, account=self.account)) yield prop_file_name try: os.remove(prop_file_name) except FileNotFoundError: pass # np, file was deleted externally def get_available_regions(self, report_type: str) -> List: """Execute the Reporter to get available regions for such report type. For Arg meaning, see: http://help.apple.com/itc/contentreporterguide/#/itcbd9ed14ac Args: report_type: Type of information contained in report. Returns: Available regions for given type of report. """ with self.create_properties_file() as property_file: cmd = (f'java -jar {self.reporter_jar} p={property_file} ' f'Finance.getVendorsAndRegions') command = shlex.split(cmd) try: # Response does not exceed 100KB and successful execution # time take 3-5 min max. There is no need to set timeout higher # than 15 min output = subprocess.check_output( command, universal_newlines=True, timeout=60 * 25) except subprocess.CalledProcessError as exception: # determine if this is a reporter error, or error on our end # (an exception is raised either way) reporter_exception = self._parse_reporter_exception(exception) if reporter_exception: raise reporter_exception from exception else: raise exception tree = ET.fromstring(output) parsed_regions = set() for v in tree.findall('Vendor'): for r in v.findall('Region'): region_reports = [ report.text for report in r.find('Reports').findall('Report')] if report_type in region_reports: parsed_regions.add(r.find('Code').text) return list(parsed_regions) def _execute_itunes_reporter( self, report_type, report_subtype, date_type, date, version): """Execute the Reporter application. If a Reporter error occurs, a ReporterException is raised. For Arg meaning, see: http://help.apple.com/itc/contentreporterguide/#/itcbd9ed14ac Args: report_type (str): Type of information contained in report. report_subtype (str): Level of detail in the report. date_type (str): Length of time covered by the report. date (str): YYYYMMDD of report. version (str): report version. Returns: str: Output from Reporter Application. """ with self.create_properties_file() as property_file: cmd = (f'java -jar {self.reporter_jar} p={property_file} ' f'Sales.getReport {self.vendor_id}, {report_type}, ' f'{report_subtype}, {str(date_type)}, {date}, {version}') command = shlex.split(cmd) try: output = subprocess.check_output( command, universal_newlines=True, timeout=14000) except subprocess.CalledProcessError as exception: # determine if this is a reporter error, or error on our end # (an exception is raised either way) reporter_exception = self._parse_reporter_exception(exception) if reporter_exception: raise reporter_exception from exception else: raise exception return output def _execute_itunes_finance_reporter( self, report_type, country, date): """Execute the finance Reporter application. If a Reporter error occurs, a ReporterException is raised. For Arg meaning, see: http://help.apple.com/itc/contentreporterguide/#/itcbd9ed14ac Args: report_type (str): Type of information contained in report. country (str): Country code. date (str): date of report. Returns: str: Output from Reporter Application. """ # Finance reporter requires to query 3 months later date # to fetch current report date = self.date_obj + relativedelta(months=3) with self.create_properties_file() as property_file: cmd = (f'java -jar {self.reporter_jar} p={property_file} ' f'Finance.getReport {self.vendor_id}, {country}, ' f'{report_type}, {str(date.year)}, {str(date.month)}') command = shlex.split(cmd) try: # Report file does not exceed 100KB and successful execution # time take 3-5 min max. There is no need to set timeout higher # than 15 min output = subprocess.check_output( command, universal_newlines=True, timeout=60*25) except subprocess.CalledProcessError as exception: # determine if this is a reporter error, or error on our end # (an exception is raised either way) reporter_exception = self._parse_reporter_exception(exception) if reporter_exception: raise reporter_exception from exception else: raise exception return output def _parse_reporter_exception(self, exception): """Determine if Exception is a result of a Reporter app error. This method attempts to parse a CalledProcessError as xml. If the output from the CalledProcessError is xml, it assumes the Error was a result of an Reporter app error, and raises a ReporterException. If it cannot parse the xml, None is returned. Args: exception (CalledProcessError): Exception to be inspected. Returns: ReporterException: If the exception is a Reporter app error, returns a ReporterException. Otherwise None is returned. """ # try parsing original exception as xml formatted reporter error try: error = xmltodict.parse(exception.output.strip()) return ReporterException( error['Error']['Message'], int(error['Error']['Code'])) except: # noqa # if unable to parse output, not a reporter exception pass return None def get_reporter(vendor_name, date=None, country=None, vendors=None, access_token=None): """Get ITunesReporter object for a vendor (factory method). Args: vendor_name (str): Vendor we want to use with the Reporter App ('ORCHARD' or 'IODA'). date (str): YYYY-MM-DD str for report requested country (str): Country code. vendors (dict): Information about Apple Music vendor accounts. access_token (str): API access token. Returns: ItunesReporter: ItunesReporter object hydrated for a vendor. """ vendors = vendors or VENDORS if vendor_name not in vendors.keys(): raise Exception( 'Unrecognized iTunes Reporter Vendor ({})'.format(vendor_name)) account = vendors.get(vendor_name).get('ACCOUNT') return ItunesReporter( reporter_jar=REPORTER_JAR, access_token=access_token, account=account, vendor_id=vendors.get(vendor_name).get('VENDOR_ID'), date=date, country=country) def get_apple_podcast_reporter(account_id, vendor_id, date, secrets_path): """Get ApplePodcastsReporter object for a vendor (factory method). Args: account_id (int): Account ID we want to use with the Reporter App. vendor_id (int): Vendor's ID we want to use with the Reporter App. date (str): YYYY-MM-DD str for report requested. secrets_path (str): aws secrets manager path. Returns: ItunesReporter: ItunesReporter object hydrated for a vendor. """ secrets_manager_client = SWFSecretsManager( environment=ENV, service_name=secrets_path) access_token = secrets_manager_client.get_cred(ACCESS_TOKEN_SECRET) return ItunesReporter( reporter_jar=REPORTER_JAR, account=account_id, vendor_id=vendor_id, date=date, access_token=access_token )