Skip to content

Commit 4f38a86

Browse files
authored
Merge pull request #362 from J-Priebe/creds-args
Make service account file & credentials optional arguments, clean up BaseAPI scope
2 parents 3e79b05 + 21323b7 commit 4f38a86

File tree

5 files changed

+75
-36
lines changed

5 files changed

+75
-36
lines changed

CONTRIBUTING.rst

+22-3
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,34 @@ Some simple guidelines to follow when contributing code:
2828
Tests
2929
-----
3030

31-
Before commiting your changes, please run the tests. For running the tests you need a service account.
31+
Before commiting your changes, please run the tests. For running the tests you need service account credentials in a JSON file.
32+
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
33+
like `cryptotools <https://cryptotools.net/rsagen/>`_ . For example:
3234

33-
**Please do not use a service account, which is used in production!**
35+
::
36+
37+
{
38+
"type": "service_account",
39+
"project_id": "splendid-donkey-123",
40+
"private_key_id": "12345",
41+
"private_key": "-----BEGIN RSA PRIVATE KEY-----\nMYTESTKEY\n-----END RSA PRIVATE KEY-----",
42+
"client_email": "[email protected]",
43+
"client_id": "789",
44+
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
45+
"token_uri": "https://oauth2.googleapis.com/token",
46+
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
47+
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-splendid-donkey-123.iam.gserviceaccount.com",
48+
"universe_domain": "googleapis.com"
49+
}
50+
51+
**Please do not use a service account or private key, which is used in production!**
3452

3553
::
3654

3755
pip install . ".[test]"
3856

39-
export GOOGLE_APPLICATION_CREDENTIALS="service_account.json"
57+
export GOOGLE_APPLICATION_CREDENTIALS="path/to/service_account.json"
58+
export FCM_TEST_PROJECT_ID="test-project-id"
4059

4160
python -m pytest
4261

pyfcm/async_fcm.py

-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ async def send_request(end_point, headers, payload, timeout=5):
3535
timeout = aiohttp.ClientTimeout(total=timeout)
3636

3737
async with aiohttp.ClientSession(headers=headers, timeout=timeout) as session:
38-
3938
async with session.post(end_point, data=payload) as res:
4039
result = await res.text()
4140
result = json.loads(result)

pyfcm/baseapi.py

+35-30
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# from __future__ import annotations
22

33
import json
4-
import os
54
import time
65
import threading
76

@@ -10,12 +9,12 @@
109
from urllib3 import Retry
1110

1211
from google.oauth2 import service_account
12+
from google.oauth2.credentials import Credentials
1313
import google.auth.transport.requests
1414

1515
from pyfcm.errors import (
1616
AuthenticationError,
1717
InvalidDataError,
18-
FCMError,
1918
FCMSenderIdMismatchError,
2019
FCMServerError,
2120
FCMNotRegisteredError,
@@ -25,13 +24,13 @@
2524

2625

2726
class BaseAPI(object):
28-
FCM_END_POINT = "https://fcm.googleapis.com/v1/projects"
27+
FCM_END_POINT_BASE = "https://fcm.googleapis.com/v1/projects"
2928

3029
def __init__(
3130
self,
32-
service_account_file: str,
33-
project_id: str,
34-
credentials=None,
31+
service_account_file: str = None,
32+
project_id: str = None,
33+
credentials: Credentials = None,
3534
proxy_dict=None,
3635
env=None,
3736
json_encoder=None,
@@ -48,25 +47,38 @@ def __init__(
4847
json_encoder (BaseJSONEncoder): JSON encoder
4948
adapter (BaseAdapter): adapter instance
5049
"""
51-
self.service_account_file = service_account_file
52-
self.project_id = project_id
53-
self.FCM_END_POINT = self.FCM_END_POINT + f"/{self.project_id}/messages:send"
54-
self.FCM_REQ_PROXIES = None
55-
self.custom_adapter = adapter
56-
self.thread_local = threading.local()
57-
self.credentials = credentials
58-
59-
if not service_account_file and not credentials:
50+
if not (service_account_file or credentials):
6051
raise AuthenticationError(
6152
"Please provide a service account file path or credentials in the constructor"
6253
)
6354

55+
if credentials is not None:
56+
self.credentials = credentials
57+
else:
58+
self.credentials = service_account.Credentials.from_service_account_file(
59+
service_account_file,
60+
scopes=["https://www.googleapis.com/auth/firebase.messaging"],
61+
)
62+
63+
# prefer the project ID scoped to the supplied credentials.
64+
# If, for some reason, the credentials do not specify a project id,
65+
# we'll check for an explicitly supplied one, and raise an error otherwise
66+
project_id = getattr(self.credentials, "project_id", None) or project_id
67+
68+
if not project_id:
69+
raise AuthenticationError(
70+
"Please provide a project_id either explicitly or through Google credentials."
71+
)
72+
73+
self.fcm_end_point = self.FCM_END_POINT_BASE + f"/{project_id}/messages:send"
74+
self.custom_adapter = adapter
75+
self.thread_local = threading.local()
76+
6477
if (
6578
proxy_dict
6679
and isinstance(proxy_dict, dict)
6780
and (("http" in proxy_dict) or ("https" in proxy_dict))
6881
):
69-
self.FCM_REQ_PROXIES = proxy_dict
7082
self.requests_session.proxies.update(proxy_dict)
7183

7284
if env == "app_engine":
@@ -101,7 +113,7 @@ def requests_session(self):
101113

102114
def send_request(self, payload=None, timeout=None):
103115
response = self.requests_session.post(
104-
self.FCM_END_POINT, data=payload, timeout=timeout
116+
self.fcm_end_point, data=payload, timeout=timeout
105117
)
106118
if (
107119
"Retry-After" in response.headers
@@ -113,14 +125,13 @@ def send_request(self, payload=None, timeout=None):
113125
return response
114126

115127
def send_async_request(self, params_list, timeout):
116-
117128
import asyncio
118129
from .async_fcm import fetch_tasks
119130

120131
payloads = [self.parse_payload(**params) for params in params_list]
121132
responses = asyncio.new_event_loop().run_until_complete(
122133
fetch_tasks(
123-
end_point=self.FCM_END_POINT,
134+
end_point=self.fcm_end_point,
124135
headers=self.request_headers(),
125136
payloads=payloads,
126137
timeout=timeout,
@@ -138,16 +149,9 @@ def _get_access_token(self):
138149
"""
139150
# get OAuth 2.0 access token
140151
try:
141-
if self.service_account_file:
142-
credentials = service_account.Credentials.from_service_account_file(
143-
self.service_account_file,
144-
scopes=["https://www.googleapis.com/auth/firebase.messaging"],
145-
)
146-
else:
147-
credentials = self.credentials
148152
request = google.auth.transport.requests.Request()
149-
credentials.refresh(request)
150-
return credentials.token
153+
self.credentials.refresh(request)
154+
return self.credentials.token
151155
except Exception as e:
152156
raise InvalidDataError(e)
153157

@@ -195,7 +199,6 @@ def parse_response(self, response):
195199
FCMSenderIdMismatchError: the authenticated sender is different from the sender registered to the token
196200
FCMNotRegisteredError: device token is missing, not registered, or invalid
197201
"""
198-
199202
if response.status_code == 200:
200203
if (
201204
"content-length" in response.headers
@@ -283,7 +286,9 @@ def parse_payload(
283286
else:
284287
raise InvalidDataError("Provided fcm_options is in the wrong format")
285288

286-
fcm_payload["notification"] = (
289+
fcm_payload[
290+
"notification"
291+
] = (
287292
{}
288293
) # - https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#notification
289294
# If title is present, use it

requirements.txt

-2
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,3 @@ rsa==4.9
77
requests>=2.6.0
88
urllib3==1.26.19
99
pytest-mock==3.14.0
10-
11-

tests/test_fcm.py

+18
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import pytest
22
from pyfcm import FCMNotification, errors
3+
import os
4+
from google.oauth2 import service_account
35

46

57
def test_push_service_without_credentials():
@@ -10,6 +12,22 @@ def test_push_service_without_credentials():
1012
pass
1113

1214

15+
def test_push_service_directly_passed_credentials():
16+
service_account_file = os.getenv("GOOGLE_APPLICATION_CREDENTIALS", None)
17+
credentials = service_account.Credentials.from_service_account_file(
18+
service_account_file,
19+
scopes=["https://www.googleapis.com/auth/firebase.messaging"],
20+
)
21+
push_service = FCMNotification(credentials=credentials)
22+
23+
# We should infer the project ID/endpoint from credentials
24+
# without the need to explcitily pass it
25+
assert push_service.fcm_end_point == (
26+
"https://fcm.googleapis.com/v1/projects/"
27+
f"{credentials.project_id}/messages:send"
28+
)
29+
30+
1331
def test_notify(push_service, generate_response):
1432
response = push_service.notify(
1533
fcm_token="Test",

0 commit comments

Comments
 (0)