From 0a7c0fb2433787efc1b26c40a4a98ba00f03a0e6 Mon Sep 17 00:00:00 2001 From: James Priebe Date: Mon, 16 Sep 2024 14:14:03 -0400 Subject: [PATCH 1/7] make service file/project id optional arguments, and clean up variables unecessarily scoped to class which are not used outside constructor --- pyfcm/baseapi.py | 60 ++++++++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/pyfcm/baseapi.py b/pyfcm/baseapi.py index 7178dd8..92cddd6 100644 --- a/pyfcm/baseapi.py +++ b/pyfcm/baseapi.py @@ -1,7 +1,6 @@ # from __future__ import annotations import json -import os import time import threading @@ -9,13 +8,12 @@ from requests.adapters import HTTPAdapter from urllib3 import Retry -from google.oauth2 import service_account +from google.oauth2 import service_account, Credentials import google.auth.transport.requests from pyfcm.errors import ( AuthenticationError, InvalidDataError, - FCMError, FCMSenderIdMismatchError, FCMServerError, FCMNotRegisteredError, @@ -25,13 +23,13 @@ class BaseAPI(object): - FCM_END_POINT = "https://fcm.googleapis.com/v1/projects" + FCM_END_POINT_BASE = "https://fcm.googleapis.com/v1/projects" def __init__( self, - service_account_file: str, - project_id: str, - credentials=None, + service_account_file: str=None, + project_id: str=None, + credentials: Credentials=None, proxy_dict=None, env=None, json_encoder=None, @@ -48,25 +46,38 @@ def __init__( json_encoder (BaseJSONEncoder): JSON encoder adapter (BaseAdapter): adapter instance """ - self.service_account_file = service_account_file - self.project_id = project_id - self.FCM_END_POINT = self.FCM_END_POINT + f"/{self.project_id}/messages:send" - self.FCM_REQ_PROXIES = None - self.custom_adapter = adapter - self.thread_local = threading.local() - self.credentials = credentials - - if not service_account_file and not credentials: + if not (service_account_file or credentials): raise AuthenticationError( "Please provide a service account file path or credentials in the constructor" ) + if credentials is not None: + self.credentials = credentials + else: + self.credentials = service_account.Credentials.from_service_account_file( + service_account_file, + scopes=["https://www.googleapis.com/auth/firebase.messaging"], + ) + + # prefer the project ID scoped to the supplied credentials. + # If, for some reason, the credentials do not specify a project id, + # we'll check for an explicitly supplied one, and raise an error otherwise + project_id = getattr(self.credentials, 'project_id', None) or project_id + + if not project_id: + raise AuthenticationError( + "Please provide a project_id either explicitly or through Google credentials." + ) + + self.fcm_end_point = self.FCM_END_POINT_BASE + f"/{project_id}/messages:send" + self.custom_adapter = adapter + self.thread_local = threading.local() + if ( proxy_dict and isinstance(proxy_dict, dict) and (("http" in proxy_dict) or ("https" in proxy_dict)) ): - self.FCM_REQ_PROXIES = proxy_dict self.requests_session.proxies.update(proxy_dict) if env == "app_engine": @@ -101,7 +112,7 @@ def requests_session(self): def send_request(self, payload=None, timeout=None): response = self.requests_session.post( - self.FCM_END_POINT, data=payload, timeout=timeout + self.fcm_end_point, data=payload, timeout=timeout ) if ( "Retry-After" in response.headers @@ -120,7 +131,7 @@ def send_async_request(self, params_list, timeout): payloads = [self.parse_payload(**params) for params in params_list] responses = asyncio.new_event_loop().run_until_complete( fetch_tasks( - end_point=self.FCM_END_POINT, + end_point=self.fcm_end_point, headers=self.request_headers(), payloads=payloads, timeout=timeout, @@ -138,16 +149,9 @@ def _get_access_token(self): """ # get OAuth 2.0 access token try: - if self.service_account_file: - credentials = service_account.Credentials.from_service_account_file( - self.service_account_file, - scopes=["https://www.googleapis.com/auth/firebase.messaging"], - ) - else: - credentials = self.credentials request = google.auth.transport.requests.Request() - credentials.refresh(request) - return credentials.token + self.credentials.refresh(request) + return self.credentials.token except Exception as e: raise InvalidDataError(e) From ee719ba4e91b0289344b9a10b2b4730dbd3141da Mon Sep 17 00:00:00 2001 From: James Priebe Date: Tue, 1 Oct 2024 11:26:36 -0400 Subject: [PATCH 2/7] cleanup, doc update, test --- CONTRIBUTING.rst | 28 ++++++++++++++++++++++++---- pyfcm/baseapi.py | 4 ++-- requirements.txt | 2 -- tests/test_fcm.py | 18 ++++++++++++++++++ 4 files changed, 44 insertions(+), 8 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index c902e20..42f93f8 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -28,15 +28,35 @@ Some simple guidelines to follow when contributing code: Tests ----- -Before commiting your changes, please run the tests. For running the tests you need a service account. - -**Please do not use a service account, which is used in production!** +Before commiting your changes, please run the tests. For running the tests you need service account credentials in a JSON file. +These do NOT have to be real credentials, but must have a properly encoded private key. You can create a key for testing using a site +like [cryptotools](https://cryptotools.net/rsagen). For example: + +```json +{ + "type": "service_account", + "project_id": "splendid-donkey-123", + "private_key_id": "12345", + "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMYTESTKEY\n-----END RSA PRIVATE KEY-----", + "client_email": "firebase-adminsdk@splendid-donkey-123.iam.gserviceaccount.com", + "client_id": "789", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-splendid-donkey-123.iam.gserviceaccount.com", + "universe_domain": "googleapis.com" +} +``` + + +**Please do not use a service account or private key, which is used in production!** :: pip install . ".[test]" - export GOOGLE_APPLICATION_CREDENTIALS="service_account.json" + export GOOGLE_APPLICATION_CREDENTIALS="path/to/service_account.json" + export FCM_TEST_PROJECT_ID="test-project-id" python -m pytest diff --git a/pyfcm/baseapi.py b/pyfcm/baseapi.py index 92cddd6..588e9a2 100644 --- a/pyfcm/baseapi.py +++ b/pyfcm/baseapi.py @@ -8,7 +8,8 @@ from requests.adapters import HTTPAdapter from urllib3 import Retry -from google.oauth2 import service_account, Credentials +from google.oauth2 import service_account +from google.oauth2.credentials import Credentials import google.auth.transport.requests from pyfcm.errors import ( @@ -199,7 +200,6 @@ def parse_response(self, response): FCMSenderIdMismatchError: the authenticated sender is different from the sender registered to the token FCMNotRegisteredError: device token is missing, not registered, or invalid """ - if response.status_code == 200: if ( "content-length" in response.headers diff --git a/requirements.txt b/requirements.txt index 51b2945..798d3bd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,5 +7,3 @@ rsa==4.9 requests>=2.6.0 urllib3==1.26.19 pytest-mock==3.14.0 - - diff --git a/tests/test_fcm.py b/tests/test_fcm.py index ecabc93..f923ff3 100644 --- a/tests/test_fcm.py +++ b/tests/test_fcm.py @@ -1,5 +1,7 @@ import pytest from pyfcm import FCMNotification, errors +import os +from google.oauth2 import service_account def test_push_service_without_credentials(): @@ -10,6 +12,22 @@ def test_push_service_without_credentials(): pass +def test_push_service_directly_passed_credentials(): + service_account_file = os.getenv("GOOGLE_APPLICATION_CREDENTIALS", None) + credentials = service_account.Credentials.from_service_account_file( + service_account_file, + scopes=["https://www.googleapis.com/auth/firebase.messaging"], + ) + push_service = FCMNotification(credentials=credentials) + + # We should infer the project ID/endpoint from credentials + # without the need to explcitily pass it + assert push_service.fcm_end_point == ( + "https://fcm.googleapis.com/v1/projects/" + f"{credentials.project_id}/messages:send" + ) + + def test_notify(push_service, generate_response): response = push_service.notify( fcm_token="Test", From f00ba0d609aee9a5c02adf19d885cc0cd115971c Mon Sep 17 00:00:00 2001 From: James Priebe Date: Tue, 1 Oct 2024 11:27:30 -0400 Subject: [PATCH 3/7] md --- CONTRIBUTING.rst | 2 +- service_account_test.json | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 service_account_test.json diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 42f93f8..0560d13 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -32,7 +32,7 @@ Before commiting your changes, please run the tests. For running the tests you n These do NOT have to be real credentials, but must have a properly encoded private key. You can create a key for testing using a site like [cryptotools](https://cryptotools.net/rsagen). For example: -```json +``` { "type": "service_account", "project_id": "splendid-donkey-123", diff --git a/service_account_test.json b/service_account_test.json new file mode 100644 index 0000000..01dec29 --- /dev/null +++ b/service_account_test.json @@ -0,0 +1,13 @@ +{ + "type": "service_account", + "project_id": "splendid-donkey-123", + "private_key_id": "12345", + "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIICXAIBAAKBgQCoW2+3UZ7W+WjdoGkLNbup4SWanIZOVzoP9vfJZzmrI7Zap0nN\nYpcDP/aH66sUg7RL2mMMOkrzj5sq4SksREn01ZaqeaPUGF35JAaCPj+OWIJmkMSP\n/xy2nUMXBKQMIdRIXpU71fiH2iG2oPbBNRQY2cM+MASlXYIizrJvv3c6CQIDAQAB\nAoGBAKHVCwVHcw0oQBJSQMcixprcrr35WezyUgDIoJU8IaYNtRtdFUdVTt4z3PH4\nqsIUe/oyGeXGHwgS8c/9EgvYNNGRcteFHvFjeIjSyLmpufrUu3frdtHUfJb1No8X\nyr4Q930zqLMF8IC9pJ/7yWf551/b/TYNneaI6cvcTaHgfK+lAkEA3lmLdl8j/dtL\nrz6Atj/X/TZDi6+jHHhEbfQbhEvuZ5h/xjQIT+7tkfcy0La/XYWP1gUkaH2Z6t+b\noRPaXotDPwJBAMHWEPLRDscSNKuCPi0Euo0wq/hrnFVtdmp4OTkmK4EqOUS4k2t+\n1UpC7lhrIcoZubyaeWUB+YLtzPSRP/ST2LcCQFI60X3kb54ZdOMJfXZpJArL/6zw\nNqV3wO7dATQrFK8RUefOJGjTVt7NiehwPVNr6qbe3fkawkp/icHHYtHmNOcCQGUA\nS0KLJp0aYnF/4zAIB8DsPJ+sSwDEkfB2hrK9reuW+dJSLxbTNwaEC7fs0uWBNCQP\nhfPY7I+Jo8NIMEAcDc8CQHnuRi8tuw4syrNKBOns/dWChupQcDfu/83ISkaUY7mV\nz65qVN0CYDP08YjA+8GDG1mPnYknJx8iokR+XU/Etpw=\n-----END RSA PRIVATE KEY-----", + "client_email": "firebase-adminsdk@splendid-donkey-123.iam.gserviceaccount.com", + "client_id": "789", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-splendid-donkey-123.iam.gserviceaccount.com", + "universe_domain": "googleapis.com" +} \ No newline at end of file From fb14cfffba58ed4c2a9f86234904fdd39b97fd0c Mon Sep 17 00:00:00 2001 From: James Priebe Date: Tue, 1 Oct 2024 11:29:31 -0400 Subject: [PATCH 4/7] code block --- CONTRIBUTING.rst | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 0560d13..c4ac625 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -32,22 +32,20 @@ Before commiting your changes, please run the tests. For running the tests you n These do NOT have to be real credentials, but must have a properly encoded private key. You can create a key for testing using a site like [cryptotools](https://cryptotools.net/rsagen). For example: -``` -{ - "type": "service_account", - "project_id": "splendid-donkey-123", - "private_key_id": "12345", - "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMYTESTKEY\n-----END RSA PRIVATE KEY-----", - "client_email": "firebase-adminsdk@splendid-donkey-123.iam.gserviceaccount.com", - "client_id": "789", - "auth_uri": "https://accounts.google.com/o/oauth2/auth", - "token_uri": "https://oauth2.googleapis.com/token", - "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", - "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-splendid-donkey-123.iam.gserviceaccount.com", - "universe_domain": "googleapis.com" -} -``` - +:: + { + "type": "service_account", + "project_id": "splendid-donkey-123", + "private_key_id": "12345", + "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMYTESTKEY\n-----END RSA PRIVATE KEY-----", + "client_email": "firebase-adminsdk@splendid-donkey-123.iam.gserviceaccount.com", + "client_id": "789", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-splendid-donkey-123.iam.gserviceaccount.com", + "universe_domain": "googleapis.com" + } **Please do not use a service account or private key, which is used in production!** From a64e44e26ce376f4d86e586a7ae164372fd14f80 Mon Sep 17 00:00:00 2001 From: James Priebe Date: Tue, 1 Oct 2024 11:33:47 -0400 Subject: [PATCH 5/7] Update CONTRIBUTING.rst --- CONTRIBUTING.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index c4ac625..8eed784 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -30,9 +30,10 @@ Tests Before commiting your changes, please run the tests. For running the tests you need service account credentials in a JSON file. These do NOT have to be real credentials, but must have a properly encoded private key. You can create a key for testing using a site -like [cryptotools](https://cryptotools.net/rsagen). For example: +like `cryptotools `_ . For example: :: + { "type": "service_account", "project_id": "splendid-donkey-123", From 23de6d8333a4812e0985f5f563c705e55a775d2d Mon Sep 17 00:00:00 2001 From: James Priebe Date: Tue, 1 Oct 2024 11:35:36 -0400 Subject: [PATCH 6/7] remove test file --- service_account_test.json | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 service_account_test.json diff --git a/service_account_test.json b/service_account_test.json deleted file mode 100644 index 01dec29..0000000 --- a/service_account_test.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "type": "service_account", - "project_id": "splendid-donkey-123", - "private_key_id": "12345", - "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIICXAIBAAKBgQCoW2+3UZ7W+WjdoGkLNbup4SWanIZOVzoP9vfJZzmrI7Zap0nN\nYpcDP/aH66sUg7RL2mMMOkrzj5sq4SksREn01ZaqeaPUGF35JAaCPj+OWIJmkMSP\n/xy2nUMXBKQMIdRIXpU71fiH2iG2oPbBNRQY2cM+MASlXYIizrJvv3c6CQIDAQAB\nAoGBAKHVCwVHcw0oQBJSQMcixprcrr35WezyUgDIoJU8IaYNtRtdFUdVTt4z3PH4\nqsIUe/oyGeXGHwgS8c/9EgvYNNGRcteFHvFjeIjSyLmpufrUu3frdtHUfJb1No8X\nyr4Q930zqLMF8IC9pJ/7yWf551/b/TYNneaI6cvcTaHgfK+lAkEA3lmLdl8j/dtL\nrz6Atj/X/TZDi6+jHHhEbfQbhEvuZ5h/xjQIT+7tkfcy0La/XYWP1gUkaH2Z6t+b\noRPaXotDPwJBAMHWEPLRDscSNKuCPi0Euo0wq/hrnFVtdmp4OTkmK4EqOUS4k2t+\n1UpC7lhrIcoZubyaeWUB+YLtzPSRP/ST2LcCQFI60X3kb54ZdOMJfXZpJArL/6zw\nNqV3wO7dATQrFK8RUefOJGjTVt7NiehwPVNr6qbe3fkawkp/icHHYtHmNOcCQGUA\nS0KLJp0aYnF/4zAIB8DsPJ+sSwDEkfB2hrK9reuW+dJSLxbTNwaEC7fs0uWBNCQP\nhfPY7I+Jo8NIMEAcDc8CQHnuRi8tuw4syrNKBOns/dWChupQcDfu/83ISkaUY7mV\nz65qVN0CYDP08YjA+8GDG1mPnYknJx8iokR+XU/Etpw=\n-----END RSA PRIVATE KEY-----", - "client_email": "firebase-adminsdk@splendid-donkey-123.iam.gserviceaccount.com", - "client_id": "789", - "auth_uri": "https://accounts.google.com/o/oauth2/auth", - "token_uri": "https://oauth2.googleapis.com/token", - "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", - "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-splendid-donkey-123.iam.gserviceaccount.com", - "universe_domain": "googleapis.com" -} \ No newline at end of file From 21323b7beb36adf7a0e6db5f9d13c775ae8db23f Mon Sep 17 00:00:00 2001 From: James Priebe Date: Tue, 1 Oct 2024 11:51:12 -0400 Subject: [PATCH 7/7] black --- pyfcm/async_fcm.py | 1 - pyfcm/baseapi.py | 13 +++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pyfcm/async_fcm.py b/pyfcm/async_fcm.py index 14c5429..447a6aa 100644 --- a/pyfcm/async_fcm.py +++ b/pyfcm/async_fcm.py @@ -35,7 +35,6 @@ async def send_request(end_point, headers, payload, timeout=5): timeout = aiohttp.ClientTimeout(total=timeout) async with aiohttp.ClientSession(headers=headers, timeout=timeout) as session: - async with session.post(end_point, data=payload) as res: result = await res.text() result = json.loads(result) diff --git a/pyfcm/baseapi.py b/pyfcm/baseapi.py index 588e9a2..5b47f89 100644 --- a/pyfcm/baseapi.py +++ b/pyfcm/baseapi.py @@ -28,9 +28,9 @@ class BaseAPI(object): def __init__( self, - service_account_file: str=None, - project_id: str=None, - credentials: Credentials=None, + service_account_file: str = None, + project_id: str = None, + credentials: Credentials = None, proxy_dict=None, env=None, json_encoder=None, @@ -63,7 +63,7 @@ def __init__( # prefer the project ID scoped to the supplied credentials. # If, for some reason, the credentials do not specify a project id, # we'll check for an explicitly supplied one, and raise an error otherwise - project_id = getattr(self.credentials, 'project_id', None) or project_id + project_id = getattr(self.credentials, "project_id", None) or project_id if not project_id: raise AuthenticationError( @@ -125,7 +125,6 @@ def send_request(self, payload=None, timeout=None): return response def send_async_request(self, params_list, timeout): - import asyncio from .async_fcm import fetch_tasks @@ -287,7 +286,9 @@ def parse_payload( else: raise InvalidDataError("Provided fcm_options is in the wrong format") - fcm_payload["notification"] = ( + fcm_payload[ + "notification" + ] = ( {} ) # - https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#notification # If title is present, use it