diff --git a/platypush/plugins/google/__init__.py b/platypush/plugins/google/__init__.py index 39cbb1f18c..0500a03f2a 100644 --- a/platypush/plugins/google/__init__.py +++ b/platypush/plugins/google/__init__.py @@ -1,56 +1,93 @@ +from typing import Collection, Optional + from platypush.plugins import Plugin class GooglePlugin(Plugin): """ - Executes calls to the Google APIs using the google-api-python-client. + Integrates with the Google APIs using the google-api-python-client. + This class is extended by ``GoogleMailPlugin``, ``GoogleCalendarPlugin`` etc. + In order to use Google services (like GMail, Maps, Calendar etc.) with your account you need to: 1. Create your Google application, if you don't have one already, on - the developers console, https://console.developers.google.com + the `developers console `_. - 2. Click on "Credentials", then "Create credentials" -> "OAuth client ID" + 2. You may have to explicitly enable your user to use the app if the app + is created in test mode. Go to "OAuth consent screen" and add your user's + email address to the list of authorized users. - 3 Select "Other", enter whichever description you like, and create + 3. Select the scopes that you want to enable for your application, depending + on the integrations that you want to use. + See https://developers.google.com/identity/protocols/oauth2/scopes + for a list of the available scopes. - 4. Click on the "Download JSON" icon next to your newly created client ID + 4. Click on "Credentials", then "Create credentials" -> "OAuth client ID". - 5. Generate a credentials file for the needed scope:: + 5 Select "Desktop app", enter whichever name you like, and click "Create". - python -m platypush.plugins.google.credentials \ - 'https://www.googleapis.com/auth/gmail.compose' ~/client_secret.json + 6. Click on the "Download JSON" icon next to your newly created client ID. + + 7. Generate a credentials file for the required scope: + + .. code-block:: bash + + mkdir -p /credentials/google + python -m platypush.plugins.google.credentials \ + 'calendar.readonly' \ + /credentials/google/client_secret.json """ - def __init__(self, scopes=None, **kwargs): + def __init__( + self, + scopes: Optional[Collection[str]] = None, + secrets_path: Optional[str] = None, + **kwargs + ): """ - Initialized the Google plugin with the required scopes. - - :param scopes: List of required scopes - :type scopes: list + :param scopes: List of scopes required by the API. + See https://developers.google.com/identity/protocols/oauth2/scopes + for a list of the available scopes. Override it in your configuration + only if you need specific scopes that aren't normally required by the + plugin. + :param secrets_path: Path to the client secrets file. + You can create your secrets.json from https://console.developers.google.com. + Default: ``/credentials/google/client_secret.json``. """ - from platypush.plugins.google.credentials import get_credentials + from platypush.plugins.google.credentials import ( + get_credentials, + default_secrets_file, + ) super().__init__(**kwargs) self._scopes = scopes or [] + self._secrets_path: str = secrets_path or default_secrets_file if self._scopes: - scopes = ' '.join(sorted(self._scopes)) - self.credentials = {scopes: get_credentials(scopes)} + scopes = " ".join(sorted(self._scopes)) + try: + self.credentials = { + scopes: get_credentials(scopes, secrets_file=self._secrets_path) + } + except AssertionError as e: + self.logger.warning(str(e)) else: self.credentials = {} - def get_service(self, service, version, scopes=None): + def get_service( + self, service: str, version: str, scopes: Optional[Collection[str]] = None + ): import httplib2 from apiclient import discovery if scopes is None: - scopes = getattr(self, 'scopes', []) + scopes = getattr(self, "scopes", []) - scopes = ' '.join(sorted(scopes)) + scopes = " ".join(sorted(scopes)) credentials = self.credentials[scopes] http = credentials.authorize(httplib2.Http()) return discovery.build(service, version, http=http, cache_discovery=False) diff --git a/platypush/plugins/google/calendar/__init__.py b/platypush/plugins/google/calendar/__init__.py index 0a0032d847..5f097eb774 100644 --- a/platypush/plugins/google/calendar/__init__.py +++ b/platypush/plugins/google/calendar/__init__.py @@ -6,11 +6,41 @@ from platypush.plugins.calendar import CalendarInterface class GoogleCalendarPlugin(GooglePlugin, CalendarInterface): - """ + r""" Google Calendar plugin. + + In order to use this plugin: + + 1. Create your Google application, if you don't have one already, on + the `developers console `_. + + 2. You may have to explicitly enable your user to use the app if the app + is created in test mode. Go to "OAuth consent screen" and add your user's + email address to the list of authorized users. + + 3. Select the scopes that you want to enable for your application, depending + on the integrations that you want to use. + See https://developers.google.com/identity/protocols/oauth2/scopes + for a list of the available scopes. + + 4. Click on "Credentials", then "Create credentials" -> "OAuth client ID". + + 5 Select "Desktop app", enter whichever name you like, and click "Create". + + 6. Click on the "Download JSON" icon next to your newly created client ID. + + 7. Generate a credentials file for the required scope: + + .. code-block:: bash + + mkdir -p /credentials/google + python -m platypush.plugins.google.credentials \ + 'calendar.readonly' \ + /credentials/google/client_secret.json + """ - scopes = ['https://www.googleapis.com/auth/calendar.readonly'] + scopes = ['calendar.readonly'] def __init__(self, *args, **kwargs): super().__init__(scopes=self.scopes, *args, **kwargs) diff --git a/platypush/plugins/google/credentials.py b/platypush/plugins/google/credentials.py index 2af682c75f..a7388820c7 100644 --- a/platypush/plugins/google/credentials.py +++ b/platypush/plugins/google/credentials.py @@ -1,35 +1,93 @@ import argparse -import httplib2 import os +import re import sys +import textwrap as tw +from typing import List, Optional +import httplib2 from oauth2client import client from oauth2client import tools from oauth2client.file import Storage +from platypush.config import Config -def get_credentials_filename(*scopes): - from platypush.config import Config +credentials_dir = os.path.join(Config.get_workdir(), "credentials", "google") +default_secrets_file = os.path.join(credentials_dir, "client_secret.json") +"""Default path for the Google API client secrets file""" - scope_name = '-'.join([scope.split('/')[-1] for scope in scopes]) - credentials_dir = os.path.join( - Config.get('workdir'), 'credentials', 'google') +def _parse_scopes(*scopes: str) -> List[str]: + return sorted( + { + t.split("/")[-1].strip() + for scope in scopes + for t in re.split(r"[\s,]", scope) + if t + } + ) + + +def get_credentials_filename(*scopes: str): + parsed_scopes = _parse_scopes(*scopes) + scope_name = "-".join([scope.split("/")[-1] for scope in parsed_scopes]) os.makedirs(credentials_dir, exist_ok=True) - return os.path.join(credentials_dir, scope_name + '.json') + matching_scope_file = next( + iter( + os.path.join(credentials_dir, scopes_file) + for scopes_file in { + os.path.basename(file) + for file in os.listdir(credentials_dir) + if file.endswith(".json") + } + if not set(parsed_scopes).difference( + set(scopes_file.split(".json")[0].split("-")) + ) + ), + None, + ) + + if matching_scope_file: + return matching_scope_file + + return os.path.join(credentials_dir, scope_name + ".json") -def get_credentials(scope): - credentials_file = get_credentials_filename(*sorted(scope.split(' '))) - if not os.path.exists(credentials_file): - raise RuntimeError(('Credentials file {} not found. Generate it through:\n' + - '\tpython -m platypush.plugins.google.credentials "{}" ' + - '\n' + - '\t\t[--auth_host_name AUTH_HOST_NAME]\n' + - '\t\t[--noauth_local_webserver]\n' + - '\t\t[--auth_host_port [AUTH_HOST_PORT [AUTH_HOST_PORT ...]]]\n' + - '\t\t[--logging_level [DEBUG,INFO,WARNING,ERROR,CRITICAL]]\n'). - format(credentials_file, scope)) +def get_credentials(scope: str, secrets_file: Optional[str] = None): + scopes = _parse_scopes(scope) + credentials_file = get_credentials_filename(*scopes) + + # If we don't have a credentials file for the required set of scopes, but we have a secrets file, + # then try and generate the credentials file from the stored secrets. + if ( + not os.path.isfile(credentials_file) + and secrets_file + and os.path.isfile(secrets_file) + ): + # If DISPLAY or BROWSER are set, then we can open the authentication URL in the browser. + # Otherwise, we'll have to use the --noauth_local_webserver flag and copy/paste the URL + args = ( + ["--noauth_local_webserver"] + if not (os.getenv("DISPLAY") or os.getenv("BROWSER")) + else [] + ) + + generate_credentials(secrets_file, scope, *args) + + assert os.path.isfile(credentials_file), tw.dedent( + f""" + Credentials file {credentials_file} not found. Generate it through: + python -m platypush.plugins.google.credentials "{','.join(scopes)}" /path/to/client_secret.json + [--auth_host_name AUTH_HOST_NAME] + [--noauth_local_webserver] + [--auth_host_port [AUTH_HOST_PORT [AUTH_HOST_PORT ...]]] + [--logging_level [DEBUG,INFO,WARNING,ERROR,CRITICAL]] + + Specify --noauth_local_webserver if you're running this script on a headless machine. + You will then get an authentication URL on the logs. + Otherwise, the URL will be opened in the available browser. + """ + ) store = Storage(credentials_file) credentials = store.get() @@ -40,16 +98,20 @@ def get_credentials(scope): return credentials -def generate_credentials(client_secret_path, scope): - credentials_file = get_credentials_filename(*sorted(scope.split(' '))) +def generate_credentials(client_secret_path: str, scope: str, *args: str): + scopes = _parse_scopes(scope) + credentials_file = get_credentials_filename(*scopes) store = Storage(credentials_file) + scope = ' '.join( + f'https://www.googleapis.com/auth/{scope}' for scope in _parse_scopes(scope) + ) flow = client.flow_from_clientsecrets(client_secret_path, scope) - flow.user_agent = 'Platypush' - flow.access_type = 'offline' - flags = argparse.ArgumentParser(parents=[tools.argparser]).parse_args() + flow.user_agent = "Platypush" + flow.access_type = "offline" # type: ignore + flags = argparse.ArgumentParser(parents=[tools.argparser]).parse_args(args) # type: ignore tools.run_flow(flow, store, flags) - print('Storing credentials to ' + credentials_file) + print("Storing credentials to", credentials_file) def main(): @@ -57,23 +119,29 @@ def main(): Generates a Google API credentials file given client secret JSON and scopes. Usage:: - python -m platypush.plugins.google.credentials [client_secret.json location] [comma-separated list of scopes] + python -m platypush.plugins.google.credentials \ + [spaces/comma-separated list of scopes] \ + [client_secret.json location] + """ - scope = sys.argv.pop(1) if len(sys.argv) > 1 \ - else input('Space separated list of OAuth scopes: ') + args = sys.argv[1:] + scope = ( + args.pop(0) if args else input("Space/comma separated list of OAuth scopes: ") + ).strip() - client_secret_path = os.path.expanduser( - sys.argv.pop(1) if len(sys.argv) > 1 - else input('Google credentials JSON file location: ')) + if args: + client_secret_path = args.pop(0) + elif os.path.isfile(default_secrets_file): + client_secret_path = default_secrets_file + else: + client_secret_path = input("Google credentials JSON file location: ") - # Uncomment to force headless (no browser spawned) authentication - # sys.argv.append('--noauth_local_webserver') - - generate_credentials(client_secret_path, scope) + client_secret_path = os.path.abspath(os.path.expanduser(client_secret_path)).strip() + generate_credentials(client_secret_path, scope, *args) -if __name__ == '__main__': +if __name__ == "__main__": main() # vim:sw=4:ts=4:et: diff --git a/platypush/plugins/google/drive/__init__.py b/platypush/plugins/google/drive/__init__.py index ddee090731..7e61de8df2 100644 --- a/platypush/plugins/google/drive/__init__.py +++ b/platypush/plugins/google/drive/__init__.py @@ -8,8 +8,36 @@ from platypush.message.response.google.drive import GoogleDriveFile class GoogleDrivePlugin(GooglePlugin): - """ + r""" Google Drive plugin. + + 1. Create your Google application, if you don't have one already, on + the `developers console `_. + + 2. You may have to explicitly enable your user to use the app if the app + is created in test mode. Go to "OAuth consent screen" and add your user's + email address to the list of authorized users. + + 3. Select the scopes that you want to enable for your application, depending + on the integrations that you want to use. + See https://developers.google.com/identity/protocols/oauth2/scopes + for a list of the available scopes. + + 4. Click on "Credentials", then "Create credentials" -> "OAuth client ID". + + 5 Select "Desktop app", enter whichever name you like, and click "Create". + + 6. Click on the "Download JSON" icon next to your newly created client ID. + + 7. Generate a credentials file for the required scope: + + .. code-block:: bash + + mkdir -p /credentials/google + python -m platypush.plugins.google.credentials \ + 'drive,drive.appfolder,drive.photos.readonly' \ + /credentials/google/client_secret.json + """ scopes = [ @@ -21,7 +49,7 @@ class GoogleDrivePlugin(GooglePlugin): def __init__(self, *args, **kwargs): super().__init__(scopes=self.scopes, *args, **kwargs) - def get_service(self, **kwargs): + def get_service(self, **_): return super().get_service(service='drive', version='v3') # noinspection PyShadowingBuiltins @@ -85,7 +113,7 @@ class GoogleDrivePlugin(GooglePlugin): else: filter += ' ' - filter += "'{}' in parents".format(folder_id) + filter += f"'{folder_id}' in parents" while True: results = ( @@ -216,7 +244,7 @@ class GoogleDrivePlugin(GooglePlugin): while not done: status, done = downloader.next_chunk() - self.logger.info('Download progress: {}%'.format(status.progress())) + self.logger.info('Download progress: %s%%', status.progress()) with open(path, 'wb') as f: f.write(fh.getbuffer().tobytes()) @@ -269,8 +297,8 @@ class GoogleDrivePlugin(GooglePlugin): add_parents: Optional[List[str]] = None, remove_parents: Optional[List[str]] = None, mime_type: Optional[str] = None, - starred: bool = None, - trashed: bool = None, + starred: Optional[bool] = None, + trashed: Optional[bool] = None, ) -> GoogleDriveFile: """ Update the metadata or the content of a file. diff --git a/platypush/plugins/google/fit/__init__.py b/platypush/plugins/google/fit/__init__.py index f5043a3160..17dbc1a07a 100644 --- a/platypush/plugins/google/fit/__init__.py +++ b/platypush/plugins/google/fit/__init__.py @@ -3,8 +3,45 @@ from platypush.plugins.google import GooglePlugin class GoogleFitPlugin(GooglePlugin): - """ + r""" Google Fit plugin. + + In order to use this plugin: + + 1. Create your Google application, if you don't have one already, on + the `developers console `_. + + 2. You may have to explicitly enable your user to use the app if the app + is created in test mode. Go to "OAuth consent screen" and add your user's + email address to the list of authorized users. + + 3. Select the scopes that you want to enable for your application, depending + on the integrations that you want to use. + See https://developers.google.com/identity/protocols/oauth2/scopes + for a list of the available scopes. + + 4. Click on "Credentials", then "Create credentials" -> "OAuth client ID". + + 5 Select "Desktop app", enter whichever name you like, and click "Create". + + 6. Click on the "Download JSON" icon next to your newly created client ID. + + 7. Generate a credentials file for the required scope: + + .. code-block:: bash + + $ mkdir -p /credentials/google + $ roles=" + fitness.activity.read, + fitness.body.read, + fitness.body_temperature.read, + fitness.heart_rate.read, + fitness.sleep.read, + fitness.location.read + " + $ python -m platypush.plugins.google.credentials "$roles" \ + /credentials/google/client_secret.json + """ scopes = [ diff --git a/platypush/plugins/google/mail/__init__.py b/platypush/plugins/google/mail/__init__.py index 0941d95d51..c8e1355b83 100644 --- a/platypush/plugins/google/mail/__init__.py +++ b/platypush/plugins/google/mail/__init__.py @@ -15,8 +15,38 @@ from platypush.plugins.google import GooglePlugin class GoogleMailPlugin(GooglePlugin): - """ - GMail plugin. It allows you to programmatically compose and (TODO) get emails + r""" + GMail plugin. It allows you to programmatically compose and (TODO) get emails. + + To use this plugin: + + 1. Create your Google application, if you don't have one already, on + the `developers console `_. + + 2. You may have to explicitly enable your user to use the app if the app + is created in test mode. Go to "OAuth consent screen" and add your user's + email address to the list of authorized users. + + 3. Select the scopes that you want to enable for your application, depending + on the integrations that you want to use. + See https://developers.google.com/identity/protocols/oauth2/scopes + for a list of the available scopes. + + 4. Click on "Credentials", then "Create credentials" -> "OAuth client ID". + + 5 Select "Desktop app", enter whichever name you like, and click "Create". + + 6. Click on the "Download JSON" icon next to your newly created client ID. + + 7. Generate a credentials file for the required scope: + + .. code-block:: bash + + mkdir -p /credentials/google + python -m platypush.plugins.google.credentials \ + 'gmail.modify' \ + /credentials/google/client_secret.json + """ scopes = ['https://www.googleapis.com/auth/gmail.modify'] @@ -64,8 +94,7 @@ class GoogleMailPlugin(GooglePlugin): content = fp.read() if main_type == 'text': - # noinspection PyUnresolvedReferences - msg = mimetypes.MIMEText(content, _subtype=sub_type) + msg = MIMEText(str(content), _subtype=sub_type) elif main_type == 'image': msg = MIMEImage(content, _subtype=sub_type) elif main_type == 'audio': diff --git a/platypush/plugins/google/maps/__init__.py b/platypush/plugins/google/maps/__init__.py index cc234c29d4..b7b14d8615 100644 --- a/platypush/plugins/google/maps/__init__.py +++ b/platypush/plugins/google/maps/__init__.py @@ -14,18 +14,24 @@ datetime_types = Union[str, int, float, datetime] class GoogleMapsPlugin(GooglePlugin): """ Plugins that provides utilities to interact with Google Maps API services. + + It requires you to create a Google application - you can create one at the + `developers console `_. + + After that, you'll need to create a new API key from the _Credentials_ tab. + + This integration doesn't require any additional scopes. """ scopes = [] - def __init__(self, api_key, *args, **kwargs): + def __init__(self, api_key: str, **kwargs): """ :param api_key: Server-side API key to be used for the requests, get one at https://console.developers.google.com - :type api_key: str """ - super().__init__(scopes=self.scopes, *args, **kwargs) + super().__init__(scopes=self.scopes, **kwargs) self.api_key = api_key @action @@ -43,7 +49,7 @@ class GoogleMapsPlugin(GooglePlugin): response = requests.get( 'https://maps.googleapis.com/maps/api/geocode/json', params={ - 'latlng': '{},{}'.format(latitude, longitude), + 'latlng': f'{latitude},{longitude}', 'key': self.api_key, }, ).json() @@ -59,9 +65,10 @@ class GoogleMapsPlugin(GooglePlugin): if 'results' in response and response['results']: result = response['results'][0] self.logger.info( - 'Google Maps geocode response for latlng ({},{}): {}'.format( - latitude, longitude, result - ) + 'Google Maps geocode response for latlng (%f,%f): %s', + latitude, + longitude, + result, ) address['address'] = result['formatted_address'].split(',')[0] @@ -91,7 +98,7 @@ class GoogleMapsPlugin(GooglePlugin): response = requests.get( 'https://maps.googleapis.com/maps/api/elevation/json', params={ - 'locations': '{},{}'.format(latitude, longitude), + 'locations': f'{latitude},{longitude}', 'key': self.api_key, }, ).json() @@ -192,16 +199,21 @@ class GoogleMapsPlugin(GooglePlugin): """ rs = requests.get( 'https://maps.googleapis.com/maps/api/distancematrix/json', + timeout=20, params={ 'origins': '|'.join(origins), 'destinations': '|'.join(destinations), 'units': units, **( - {'departure_time': to_datetime(departure_time)} + {'departure_time': to_datetime(departure_time).isoformat()} if departure_time else {} ), - **({'arrival_time': to_datetime(arrival_time)} if arrival_time else {}), + **( + {'arrival_time': to_datetime(arrival_time).isoformat()} + if arrival_time + else {} + ), **({'avoid': '|'.join(avoid)} if avoid else {}), **({'language': language} if language else {}), **({'mode': mode} if mode else {}), diff --git a/platypush/plugins/google/pubsub/__init__.py b/platypush/plugins/google/pubsub/__init__.py index 1484cdd4bd..1834844ddb 100644 --- a/platypush/plugins/google/pubsub/__init__.py +++ b/platypush/plugins/google/pubsub/__init__.py @@ -9,15 +9,18 @@ class GooglePubsubPlugin(Plugin): Send messages over a Google pub/sub instance. You'll need a Google Cloud active project and a set of credentials to use this plugin: - 1. Create a project on the `Google Cloud console `_ if - you don't have one already. + 1. Create a project on the `Google Cloud console + `_ if you don't have + one already. - 2. In the `Google Cloud API console `_ - create a new service account key. Select "New Service Account", choose the role "Pub/Sub Editor" and leave - the key type as JSON. + 2. In the `Google Cloud API console + `_ + create a new service account key. Select "New Service Account", choose + the role "Pub/Sub Editor" and leave the key type as JSON. - 3. Download the JSON service credentials file. By default platypush will look for the credentials file under - ~/.credentials/platypush/google/pubsub.json. + 3. Download the JSON service credentials file. By default Platypush + will look for the credentials file under + ``~/.credentials/platypush/google/pubsub.json``. """ @@ -29,8 +32,8 @@ class GooglePubsubPlugin(Plugin): def __init__(self, credentials_file: str = default_credentials_file, **kwargs): """ - :param credentials_file: Path to the JSON credentials file for Google pub/sub (default: - ~/.credentials/platypush/google/pubsub.json) + :param credentials_file: Path to the JSON credentials file for Google + pub/sub (default: ``~/.credentials/platypush/google/pubsub.json``) """ super().__init__(**kwargs) self.credentials_file = credentials_file @@ -52,10 +55,13 @@ class GooglePubsubPlugin(Plugin): """ Sends a message to a topic - :param topic: Topic/channel where the message will be delivered. You can either specify the full topic name in - the format ``projects//topics/``, where ```` must be the ID of your - Google Pub/Sub project, or just ```` - in such case it's implied that you refer to the - ``topic_name`` under the ``project_id`` of your service credentials. + :param topic: Topic/channel where the message will be delivered. You + can either specify the full topic name in the format + ``projects//topics/``, where + ```` must be the ID of your Google Pub/Sub project, or + just ```` - in such case it's implied that you refer to + the ``topic_name`` under the ``project_id`` of your service + credentials. :param msg: Message to be sent. It can be a list, a dict, or a Message object :param kwargs: Extra arguments to be passed to .publish() """ @@ -65,8 +71,8 @@ class GooglePubsubPlugin(Plugin): credentials = self.get_credentials(self.publisher_audience) publisher = pubsub_v1.PublisherClient(credentials=credentials) - if not topic.startswith('projects/{}/topics/'.format(self.project_id)): - topic = 'projects/{}/topics/{}'.format(self.project_id, topic) + if not topic.startswith(f'projects/{self.project_id}/topics/'): + topic = f'projects/{self.project_id}/topics/{topic}' try: publisher.create_topic(topic) diff --git a/platypush/plugins/google/translate/__init__.py b/platypush/plugins/google/translate/__init__.py index 632a26cc3e..3fbd0fd5b6 100644 --- a/platypush/plugins/google/translate/__init__.py +++ b/platypush/plugins/google/translate/__init__.py @@ -1,7 +1,6 @@ import os from typing import Optional, List -# noinspection PyPackageRequirements from google.cloud import translate_v2 as translate from platypush.message.response.translate import TranslateResponse @@ -13,16 +12,19 @@ class GoogleTranslatePlugin(Plugin): Plugin to interact with the Google Translate API. You'll need a Google Cloud active project and a set of credentials to use this plugin: - 1. Create a project on the `Google Cloud console `_ if - you don't have one already. + 1. Create a project on the `Google Cloud console + `_ if you don't + have one already. - 2. In the menu navigate to the *Artificial Intelligence* section and select *Translations* and enable the API. + 2. In the menu navigate to the *Artificial Intelligence* section and + select *Translations* and enable the API. - 3. From the menu select *APIs & Services* and create a service account. You can leave role and permissions - empty. + 3. From the menu select *APIs & Services* and create a service account. + You can leave role and permissions empty. - 4. Create a new private JSON key for the service account and download it. By default platypush will look for the - credentials file under ``~/.credentials/platypush/google/translate.json``. + 4. Create a new private JSON key for the service account and download + it. By default platypush will look for the credentials file under + ``~/.credentials/platypush/google/translate.json``. """ @@ -39,11 +41,14 @@ class GoogleTranslatePlugin(Plugin): ): """ :param target_language: Default target language (default: 'en'). - :param credentials_file: Google service account JSON credentials file. If none is specified then the plugin will - search for the credentials file in the following order: + :param credentials_file: Google service account JSON credentials file. + If none is specified then the plugin will search for the credentials + file in the following order: + + 1. ``~/.credentials/platypush/google/translate.json`` + 2. Context from the ``GOOGLE_APPLICATION_CREDENTIALS`` + environment variable. - 1. ``~/.credentials/platypush/google/translate.json`` - 2. Context from the ``GOOGLE_APPLICATION_CREDENTIALS`` environment variable. """ super().__init__(**kwargs) self.target_language = target_language @@ -64,7 +69,7 @@ class GoogleTranslatePlugin(Plugin): for i in range(min(pos, len(text) - 1), -1, -1): if text[i] in [' ', '\t', ',', '.', ')', '>']: return i - elif text[i] in ['(', '<']: + if text[i] in ['(', '<']: return i - 1 if i > 0 else 0 return 0 @@ -86,7 +91,6 @@ class GoogleTranslatePlugin(Plugin): return parts - # noinspection PyShadowingBuiltins @action def translate( self, @@ -121,7 +125,6 @@ class GoogleTranslatePlugin(Plugin): if not result: result = response else: - # noinspection PyTypeChecker result['translatedText'] += ' ' + response['translatedText'] return TranslateResponse( diff --git a/platypush/plugins/google/youtube/__init__.py b/platypush/plugins/google/youtube/__init__.py index 82dfe0f033..bafcccb6eb 100644 --- a/platypush/plugins/google/youtube/__init__.py +++ b/platypush/plugins/google/youtube/__init__.py @@ -1,10 +1,41 @@ +from typing import Collection, Optional, Union from platypush.plugins import action from platypush.plugins.google import GooglePlugin class GoogleYoutubePlugin(GooglePlugin): - """ + r""" YouTube plugin. + + Requirements: + + 1. Create your Google application, if you don't have one already, on + the `developers console `_. + + 2. You may have to explicitly enable your user to use the app if the app + is created in test mode. Go to "OAuth consent screen" and add your user's + email address to the list of authorized users. + + 3. Select the scopes that you want to enable for your application, depending + on the integrations that you want to use. + See https://developers.google.com/identity/protocols/oauth2/scopes + for a list of the available scopes. + + 4. Click on "Credentials", then "Create credentials" -> "OAuth client ID". + + 5 Select "Desktop app", enter whichever name you like, and click "Create". + + 6. Click on the "Download JSON" icon next to your newly created client ID. + + 7. Generate a credentials file for the required scope: + + .. code-block:: bash + + mkdir -p /credentials/google + python -m platypush.plugins.google.credentials \ + 'youtube.readonly' \ + /credentials/google/client_secret.json + """ scopes = ['https://www.googleapis.com/auth/youtube.readonly'] @@ -19,28 +50,28 @@ class GoogleYoutubePlugin(GooglePlugin): super().__init__(scopes=self.scopes, *args, **kwargs) @action - def search(self, parts=None, query='', types=None, max_results=25, **kwargs): + def search( + self, + parts: Optional[Union[str, Collection[str]]] = None, + query: str = '', + types: Optional[Union[str, Collection[str]]] = None, + max_results: int = 25, + **kwargs + ): """ Search for YouTube content. :param parts: List of parts to get (default: snippet). - See the `Getting started - Part `_. - :type parts: list[str] or str - + See the `Getting started - Part + `_. :param query: Query string (default: empty string) - :type query: str - :param types: List of types to retrieve (default: video). - See the `Getting started - Resources `_. - :type types: list[str] or str - + See the `Getting started - Resources + `_. :param max_results: Maximum number of items that will be returned (default: 25). - :type max_results: int - :param kwargs: Any extra arguments that will be transparently passed to the YouTube API. See the `Getting started - parameters `_. - :return: A list of YouTube resources. See the `Getting started - Resource `_.