8000 coments · ClarkDing/firebase-admin-python@8333d89 · GitHub
[go: up one dir, main page]

Skip to content

Commit 8333d89

Browse files
committed
coments
1 parent 861ade0 commit 8333d89

File tree

5 files changed

+158
-120
lines changed

5 files changed

+158
-120
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
- [changed] Improved error handling in FCM by mapping more server-side
44
errors to client-side error codes.
55

6+
- [added] The Firebase dynamic links module with the `get_link_stats` API
7+
68
# v2.9.0
79

810

firebase_admin/dynamic_links.py

Lines changed: 71 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,17 @@
1414

1515
"""Firebase Dynamic Links module.
1616
17-
This module lets admins get the stats for a dynamic link.
17+
This module lets admins get statistics for a Firebase dynamic link.
1818
"""
1919

20-
from collections import namedtuple
21-
22-
from six.moves import urllib_parse
20+
from six.moves import urllib
2321
import six
2422

2523
from firebase_admin import _http_client
2624
from firebase_admin import _utils
2725

2826

29-
30-
31-
_LINKS_ATTRIBUTE = '_links'
27+
_LINKS_ATTRIBUTE = '_dynamic_links'
3228
_LINKS_BASE_URL = 'https://firebasedynamiclinks.googleapis.com/v1/ 8000 '
3329

3430
PLATFORM_DESKTOP = 'desktop'
@@ -41,40 +37,39 @@
4137
EVENT_TYPE_APP_FIRST_OPEN = 'app_first_open'
4238
EVENT_TYPE_APP_RE_OPEN = 'app_re_open'
4339

44-
StatOptions = namedtuple('StatOptions', ['duration_days'])
45-
4640
def get_link_stats(short_link, stat_options, app=None):
47-
""" Returns a LinkStats object with the event stats for the given short link
41+
""" Returns a `LinkStats` object with the event statistics for the given short link
4842
4943
Args:
5044
short_link: The string of the designated short link. e.g. https://abc12.app.goo.gl/link
5145
The link must belong to the project associated with the service account
5246
used to call this API.
53-
stat_options: an object containing a single field "duration_days" for which the
54-
app: a firebase_app instance or None, for default.
47+
stat_options: An object containing a single field "duration_days" for which the statistics
48+
are retrieved.
49+
app: (optional) `firebase_app` instance. (If missing uses default app.)
5550
5651
Returns:
57-
LinkStats: an LinkStats object. (containing an array of EventStats)
52+
LinkStats: An `LinkStats` object. (containing an array of `EventStats`)
5853
5954
Raises:
6055
ValueError: If any of the arguments are invalid.
61-
url must be encoded and start with "http"
56+
url must start with the protocol "http"
6257
stat_options should have a field with duration_days > 0
6358
"""
64-
return _get_link_service(app).get_stats(short_link, stat_options)
59+
return _get_link_service(app)._get_stats(short_link, stat_options)
6560

6661
def _get_link_service(app):
6762
"""Returns an _LinksService instance for an App.
6863
69-
If the App already has an _LinksService associated with it, simply returns
64+
If the App already has a _LinksService associated with it, simply returns
7065
it. Otherwise creates a new _LinksService, and adds it to the App before
7166
returning it.
7267
7368
Args:
7469
app: A Firebase App instance (or None to use the default App).
7570
7671
Returns:
77-
_LinksService: An _LinksService for the specified App instance.
72+
_LinksService: An `_LinksService` for the specified App instance.
7873
7974
Raises:
8075
ValueError: If the app argument is invalid.
@@ -83,7 +78,7 @@ def _get_link_service(app):
8378

8479

8580
class LinkStats(object):
86-
"""The LinkStats object is returned by get_link_stats, it contains a list of EventStats"""
81+
"""The `LinkStats` object is returned by get_link_stats, it contains a list of `EventStats`"""
8782
def __init__(self, event_stats):
8883
if not isinstance(event_stats, (list, tuple)):
8984
raise ValueError('Invalid data argument: {0}. Must be a list or tuple'
@@ -95,46 +90,16 @@ def __init__(self, event_stats):
9590

9691
@property
9792
def event_stats(self):
98-
"""Returns the event_stats for this link.
93+
"""Returns the event statistics for this link, for the requested period.
9994
10095
Returns:
101-
event_stats: A list of EventStats.
96+
event_stats: A list of `EventStats`.
10297
"""
10398
return self._stats
10499

105-
106-
class _LinksService(object):
107-
"""Provides methods for the Firebase Dynamic Links interaction"""
108-
def __init__(self, app):
109-
self._client = _http_client.JsonHttpClient(
110-
credential=app.credential.get_credential(),
111-
base_url=_LINKS_BASE_URL)
112-
self._timeout = app.options.get('httpTimeout')
113-
self._links_request = '{0}/linkStats?durationDays={1}'
114-
115-
def _populated_request(self, url, options):
116-
days = options.duration_days
117-
# This is due to six.urllib_parse mistaken error for python 2
118-
#pylint: disable=too-many-function-args
119-
# Complaints about the named second argument needed to replace "/"
120-
#pylint: disable=redundant-keyword-arg
121-
url_quoted = urllib_parse.quote(url, safe="")
122-
#pylint: enable=redundant-keyword-arg
123-
#pylint: enable=too-many-function-args
124-
return self._links_request.format(url_quoted, days)
125-
126-
def get_stats(self, url, options):
127-
_validate_url(url)
128-
_validate_stat_options(options)
129-
url_p = self._populated_request(url, options)
130-
resp = self._client.request('get', url_p)
131-
link_event_stats = resp.json().get('linkEventStats', [])
132-
event_stats = [EventStats.from_json(**es) for es in link_event_stats]
133-
134-
return LinkStats(event_stats)
135-
136100
class EventStats(object):
137-
"""EventStats is a single stat item containing (platform, event, count)"""
101+
"""`EventStats` is a single stat item containing (platform, event, count)"""
102+
138103
_platforms = {
139104
'DESKTOP': PLATFORM_DESKTOP,
140105
'IOS': PLATFORM_IOS,
@@ -160,7 +125,9 @@ def __repr__(self):
160125
self.platform, self.event, self.count)
161126

162127
@classmethod
163-
def from_json(cls, platform, event, count):
128+
def make_event_stat(cls, platform, event, count):
129+
"""make_event_stat creates an EventStat object given the appropriate constants. e.g:
130+
make_event_stat(platform=PLATFORM_DESKTOP, event=EVENT_TYPE_REDIRECT, count=4)"""
164131
return EventStats(cls._platforms[platform],
165132
cls._event_types[event],
166133
int(count))
@@ -171,10 +138,10 @@ def platform(self):
171138

172139
@platform.setter
173140
def platform(self, platform):
174-
if isinstance(platform, str) and platform in self._platforms.keys():
141+
if isinstance(platform, six.string_types) and platform in self._platforms.keys():
175142
raise ValueError(('Raw string {} detected. Use one of the dynamic_links.PLATFORM_...' +
176-
' constants, or the from_json() method.').format(platform))
177-
if not isinstance(platform, str) or platform not in self._platforms.values():
143+
' constants, or the make_event_stat() method.').format(platform))
144+
if not isinstance(platform, six.string_types) or platform not in self._platforms.values():
178145
raise ValueError('platform {}, not recognized'.format(platform))
179146
self._platform = platform
180147

@@ -184,10 +151,10 @@ def event(self):
184151

185152
@event.setter
186153
def event(self, event):
187-
if isinstance(event, str) and event in self._event_types.keys():
154+
if isinstance(event, six.string_types) and event in self._event_types.keys():
188155
raise ValueError(('Raw string {} detected. Use one of the dynamic_links.EVENT_TYPES_' +
189-
' constants, or the from_json() method.').format(event))
190-
if not isinstance(event, str) or event not in self._event_types.values():
156+
' constants, or the make_event_stat() method.').format(event))
157+
if not isinstance(event, six.string_types) or event not in self._event_types.values():
191158
raise ValueError('event_type {}, not recognized'.format(event))
192159
self._event = event
193160

@@ -202,14 +169,49 @@ def count(self, count):
202169
self._count = count
203170

204171

205-
def _validate_url(url):
206-
if not isinstance(url, six.string_types) or not url.startswith('https://'):
207-
raise ValueError('Url must be a string and begin with "https://".')
172+
class StatOptions(object):
173+
def __init__(self, duration_days):
174+
self.duration_days = duration_days
175+
176+
@property
177+
def duration_days(self):
178+
return self._duration_days
179+
180+
@duration_days.setter
181+
def duration_days(self, duration_days):
182+
if (isinstance(duration_days, bool)
183+
or not isinstance(duration_days, int)
184+
or duration_days < 1):
185+
raise ValueError('duration_days must be positive integer (got {})'
186+
.format(duration_days))
187+
self._duration_days = duration_days
208188

209-
def _validate_stat_options(options):
210-
if not isinstance(options, StatOptions):
211-
raise ValueError('Options must be of type StatOptions.')
212-
if (isinstance(options.duration_days, bool)
213-
or not isinstance(options.duration_days, int)
214-
or options.duration_days < 1):
215-
raise ValueError('duration_days: {} must be positive int'.format(options.duration_days))
189+
class _LinksService(object):
190+
"""Provides methods for the Firebase dynamic links interaction"""
191+
def __init__(self, app):
192+
self._client = _http_client.JsonHttpClient(
193+
credential=app.credential.get_credential(),
194+
base_url=_LINKS_BASE_URL)
195+
self._timeout = app.options.get('httpTimeout')
196+
self._request_string = '{0}/linkStats?durationDays={1}'
197+
198+
def _format_request_string(self, short_link, options):
199+
days = options.duration_days
200+
# Complaints about the named second argument needed to replace "/"
201+
#pylint: disable=redundant-keyword-arg
202+
url_quoted = urllib.parse.quote(short_link, safe='')
203+
#pylint: enable=redundant-keyword-arg
204+
return self._request_string.format(url_quoted, days)
205+
206+
def _get_stats(self, short_link, stat_options):
207+
if(not isinstance(short_link, six.string_types)
208+
or not short_link.startswith('https://')):
209+
raise ValueError('short_link must be a string and begin with "https://".')
210+
if not isinstance(stat_options, StatOptions):
211+
raise ValueError('stat_options must be of type StatOptions.')
212+
213+
request_string = self._format_request_string(short_link, stat_options)
214+
resp = self._client.body('get', request_string)
215+
link_event_stats_dict = resp.get('linkEventStats', [])
216+
event_stats = [EventStats.make_event_stat(**es) for es in link_event_stats_dict]
217+
return LinkStats(event_stats)

integration/test_dynamic_links.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
sys.stderr.write("""
3131
==============================================================================================
3232
To run end to end tests you must do the following:
33-
1. From the firebase console, create a short link under dynamic links.
33+
1. From the firebase console, create a short link under "Grow > Dynamic Links".
3434
2. From your broser or phone, go to that short link and see that it redirects.
3535
3. Wait up to 36 hours.
3636
4. Create a file named dynamic_links_e2e_url.txt under tests/data/.
@@ -43,7 +43,7 @@ class TestEndToEnd(object):
4343
"""Runs an end to end test, see comment string for setup."""
4444

4545
def test_get_stats(self):
46-
if(dynamic_links_e2e_url) == 0:
46+
if not dynamic_links_e2e_url:
4747
return
4848
link_stats = dynamic_links.get_link_stats(
4949
dynamic_links_e2e_url,
@@ -61,7 +61,7 @@ def test_get_stats_nonexistant_link(self):
6161
assert len(link_stats.event_stats) == 0
6262

6363
class TestServerErrors(object):
64-
def test_unautherized(self):
64+
def test_unauthorized(self):
6565
with pytest.raises(requests.exceptions.HTTPError) as excinfo:
6666
dynamic_links.get_link_stats(
6767
'https://fake1.app.goo.gl/uQWc',

tests/get_link_stats_vals.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""comparison for unit tests"""
2+
comparison = [
3+
{
4+
"platform": "android",
5+
"count": 123,
6+
"event": "click"
7+
},
8+
{
9+
"platform": "ios",
10+
"count": 123,
11+
"event": "click"
12+
},
13+
{
14+
"platform": "desktop",
15+
"count": 456,
16+
"event": "click"
17+
},
18+
{
19+
"platform": "android",
20+
"count": 99,
21+
"event": "app_install"
22+
},
23+
{
24+
"platform": "android",
25+
"count": 42,
26+
"event": "app_first_open"
27+
},
28+
{
29+
"platform": "android",
30+
"count": 142,
31+
"event": "app_re_open"
32+
},
33+
{
34+
"platform": "ios",
35+
"count": 124,
36+
"event": "redirect"
37+
}
38+
]

0 commit comments

Comments
 (0)
0