Skip to content

Commit

Permalink
Merge pull request #362 from J-Priebe/creds-args
Browse files Browse the repository at this point in the history
Make service account file & credentials optional arguments, clean up BaseAPI scope
  • Loading branch information
olucurious authored Oct 31, 2024
2 parents 3e79b05 + 21323b7 commit 4f38a86
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 36 deletions.
25 changes: 22 additions & 3 deletions CONTRIBUTING.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,34 @@ 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.
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:

**Please do not use a service account, which is used in production!**
::

{
"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

Expand Down
1 change: 0 additions & 1 deletion pyfcm/async_fcm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
65 changes: 35 additions & 30 deletions pyfcm/baseapi.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# from __future__ import annotations

import json
import os
import time
import threading

Expand All @@ -10,12 +9,12 @@
from urllib3 import Retry

from google.oauth2 import service_account
from google.oauth2.credentials import Credentials
import google.auth.transport.requests

from pyfcm.errors import (
AuthenticationError,
InvalidDataError,
FCMError,
FCMSenderIdMismatchError,
FCMServerError,
FCMNotRegisteredError,
Expand All @@ -25,13 +24,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,
Expand All @@ -48,25 +47,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":
Expand Down Expand Up @@ -101,7 +113,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
Expand All @@ -113,14 +125,13 @@ 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

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,
Expand All @@ -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)

Expand Down Expand Up @@ -195,7 +199,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
Expand Down Expand Up @@ -283,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
Expand Down
2 changes: 0 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,3 @@ rsa==4.9
requests>=2.6.0
urllib3==1.26.19
pytest-mock==3.14.0


18 changes: 18 additions & 0 deletions tests/test_fcm.py
Original file line number Diff line number Diff line change
@@ -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():
Expand All @@ -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",
Expand Down

0 comments on commit 4f38a86

Please sign in to comment.