Skip to content

Commit ce6c6a9

Browse files
authored
Merge branch 'main' into resource-gallery-386
2 parents dd6f817 + 2338c6e commit ce6c6a9

39 files changed

+1193
-1239
lines changed

.github/workflows/get-metrics.py

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
import datetime
2+
import json
3+
import os
4+
5+
import cartopy
6+
import google
7+
import matplotlib
8+
import matplotlib.cm as cm
9+
import matplotlib.colors as colors
10+
import matplotlib.pyplot as plt
11+
import numpy as np
12+
from google.analytics.data_v1beta import BetaAnalyticsDataClient
13+
from google.analytics.data_v1beta.types import DateRange, Dimension, Metric, RunReportRequest
14+
15+
# Project ID Numbers
16+
PORTAL_ID = '266784902'
17+
FOUNDATIONS_ID = '281776420'
18+
COOKBOOKS_ID = '324070631'
19+
20+
# Access Secrets
21+
PRIVATE_KEY_ID = os.environ.get('PRIVATE_KEY_ID')
22+
# Ensure GH secrets doesn't intrudce extra '\' new line characters (related to '\' being an escape character)
23+
PRIVATE_KEY = os.environ.get('PRIVATE_KEY').replace('\\n', '\n')
24+
25+
credentials_dict = {
26+
'type': 'service_account',
27+
'project_id': 'cisl-vast-pythia',
28+
'private_key_id': PRIVATE_KEY_ID,
29+
'private_key': PRIVATE_KEY,
30+
'client_email': '[email protected]',
31+
'client_id': '113402578114110723940',
32+
'auth_uri': 'https://accounts.google.com/o/oauth2/auth',
33+
'token_uri': 'https://oauth2.googleapis.com/token',
34+
'auth_provider_x509_cert_url': 'https://www.googleapis.com/oauth2/v1/certs',
35+
'client_x509_cert_url': 'https://www.googleapis.com/robot/v1/metadata/x509/pythia-metrics-api%40cisl-vast-pythia.iam.gserviceaccount.com',
36+
'universe_domain': 'googleapis.com',
37+
}
38+
39+
try:
40+
client = BetaAnalyticsDataClient.from_service_account_info(credentials_dict)
41+
except google.auth.exceptions.MalformedError as e:
42+
print('Malformed Error:', repr(e))
43+
# Insight into reason for failure without exposing secret key
44+
# 0: Secret not found, else malformed
45+
# 706: extra quote, 732: extra '\', 734: both
46+
print('Length of PRIVATE_KEY:', len(PRIVATE_KEY))
47+
48+
pre_project_date = '2020-03-31' # Random date before project start
49+
50+
51+
def _format_rounding(value):
52+
"""
53+
Helper function for rounding string displays. 1,232 -> 1.2K
54+
"""
55+
return f'{round(value / 1000, 1):.1f}K'
56+
57+
58+
# The rest of this file alternates between functions for requesting information from Google Analytics
59+
# And functions that use that request image to form either a .json or a .png file to be used in write-metrics-md.py
60+
def _run_total_users_report(property_id):
61+
"""
62+
Function for requesting cumulative active users from a project since project start.
63+
"""
64+
request = RunReportRequest(
65+
property=f'properties/{property_id}',
66+
dimensions=[],
67+
metrics=[Metric(name='activeUsers')],
68+
date_ranges=[DateRange(start_date=pre_project_date, end_date='today')],
69+
)
70+
response = client.run_report(request)
71+
72+
total_users = 0
73+
for row in response.rows:
74+
total_users += int(row.metric_values[0].value)
75+
76+
return _format_rounding(total_users)
77+
78+
79+
def get_total_users(PORTAL_ID, FOUNDATIONS_ID, COOKBOOKS_ID):
80+
"""
81+
Function for taking cumulative active users from each project and dumping it into a JSON with the current datetime.
82+
"""
83+
metrics_dict = {}
84+
metrics_dict['Now'] = str(datetime.datetime.now())
85+
metrics_dict['Portal'] = _run_total_users_report(PORTAL_ID)
86+
metrics_dict['Foundations'] = _run_total_users_report(FOUNDATIONS_ID)
87+
metrics_dict['Cookbooks'] = _run_total_users_report(COOKBOOKS_ID)
88+
89+
# Save to JSON, Remember Action is called from root directory
90+
with open('portal/metrics/user_metrics.json', 'w') as outfile:
91+
json.dump(metrics_dict, outfile)
92+
93+
94+
def _run_active_users_this_year(property_id):
95+
"""
96+
Function for requesting active users by day from a project since year start.
97+
"""
98+
current_year = datetime.datetime.now().year
99+
start_date = f'{current_year}-01-01'
100+
101+
request = RunReportRequest(
102+
property=f'properties/{property_id}',
103+
dimensions=[Dimension(name='date')],
104+
metrics=[Metric(name='activeUsers')],
105+
date_ranges=[DateRange(start_date=start_date, end_date='today')],
106+
)
107+
response = client.run_report(request)
108+
109+
dates = []
110+
user_counts = []
111+
for row in response.rows:
112+
date_str = row.dimension_values[0].value
113+
date = datetime.datetime.strptime(date_str, '%Y%m%d')
114+
dates.append(date)
115+
user_counts.append(int(row.metric_values[0].value))
116+
117+
# Days need to be sorted chronologically
118+
return zip(*sorted(zip(dates, user_counts), key=lambda x: x[0]))
119+
120+
121+
def plot_projects_this_year(PORTAL_ID, FOUNDATIONS_ID, COOKBOOKS_ID):
122+
"""
123+
Function for taking year-to-date active users by day and plotting it for each project.
124+
"""
125+
portal_dates, portal_users = _run_active_users_this_year(PORTAL_ID)
126+
foundations_dates, foundations_users = _run_active_users_this_year(FOUNDATIONS_ID)
127+
cookbooks_dates, cookbooks_users = _run_active_users_this_year(COOKBOOKS_ID)
128+
129+
# Plotting code
130+
plt.figure(figsize=(10, 5.5))
131+
plt.title('Year-to-Date Pythia Active Users', fontsize=15)
132+
133+
plt.plot(portal_dates, portal_users, color='purple', label='Portal')
134+
plt.plot(foundations_dates, foundations_users, color='royalblue', label='Foundations')
135+
plt.plot(cookbooks_dates, cookbooks_users, color='indianred', label='Cookbooks')
136+
137+
plt.legend(fontsize=12, loc='upper right')
138+
139+
plt.xlabel('Date', fontsize=12)
140+
plt.savefig('portal/metrics/thisyear.png', bbox_inches='tight')
141+
142+
143+
def _run_top_pages_report(property_id):
144+
"""
145+
Function for requesting top 5 pages from a project.
146+
"""
147+
request = RunReportRequest(
148+
property=f'properties/{property_id}',
149+
dimensions=[Dimension(name='pageTitle')],
150+
date_ranges=[DateRange(start_date=pre_project_date, end_date='today')],
151+
metrics=[Metric(name='screenPageViews')],
152+
)
153+
response = client.run_report(request)
154+
155+
views_dict = {}
156+
for row in response.rows:
157+
page = row.dimension_values[0].value
158+
views = int(row.metric_values[0].value)
159+
views_dict[page] = views
160+
161+
# Sort by views and grab the top 5
162+
top_pages = sorted(views_dict.items(), key=lambda item: item[1], reverse=True)[:5]
163+
# String manipulation on page titles "Cartopy - Pythia Foundations" -> "Cartopy"
164+
pages = [page.split('—')[0] for page, _ in top_pages]
165+
views = [views for _, views in top_pages]
166+
167+
# Reverse order of lists, so they'll plot with most visited page on top (i.e. last)
168+
return pages[::-1], views[::-1]
169+
170+
171+
def plot_top_pages(PORTAL_ID, FOUNDATIONS_ID, COOKBOOKS_ID):
172+
"""
173+
Function that takes the top 5 viewed pages for all 3 projects and plot them on a histogram.
174+
"""
175+
portal_pages, portal_views = _run_top_pages_report(PORTAL_ID)
176+
foundations_pages, foundations_views = _run_top_pages_report(FOUNDATIONS_ID)
177+
cookbooks_pages, cookbooks_views = _run_top_pages_report(COOKBOOKS_ID)
178+
179+
# Plotting code
180+
fig, ax = plt.subplots(figsize=(10, 5.5))
181+
plt.title('All-Time Top Pages', fontsize=15)
182+
183+
y = np.arange(5) # 0-4 for Cookbooks
184+
y2 = np.arange(6, 11) # 6-10 for Foundations
185+
y3 = np.arange(12, 17) # 12-16 for Portal
186+
187+
bar1 = ax.barh(y3, portal_views, align='center', label='Portal', color='purple')
188+
bar2 = ax.barh(y2, foundations_views, align='center', label='Foundations', color='royalblue')
189+
bar3 = ax.barh(y, cookbooks_views, align='center', label='Cookbooks', color='indianred')
190+
191+
y4 = np.append(y, y2)
192+
y4 = np.append(y4, y3) # 0-4,6-19,12-6 for page labels to have a gap between projects
193+
pages = cookbooks_pages + foundations_pages + portal_pages # List of all pages
194+
ax.set_yticks(y4, labels=pages, fontsize=12)
195+
196+
# Adds round-formatted views label to end of each bar
197+
ax.bar_label(bar1, fmt=_format_rounding, padding=5, fontsize=10)
198+
ax.bar_label(bar2, fmt=_format_rounding, padding=5, fontsize=10)
199+
ax.bar_label(bar3, fmt=_format_rounding, padding=5, fontsize=10)
200+
201+
ax.set_xscale('log')
202+
ax.set_xlim([10, 10**5]) # set_xlim must be after setting xscale to log
203+
ax.set_xlabel('Page Views', fontsize=12)
204+
205+
plt.legend(fontsize=12, loc='lower right')
206+
plt.savefig('portal/metrics/toppages.png', bbox_inches='tight')
207+
208+
209+
def _run_usersXcountry_report(property_id):
210+
"""
211+
Function for requesting users by country for a project.
212+
"""
213+
request = RunReportRequest(
214+
property=f'properties/{property_id}',
215+
dimensions=[Dimension(name='country')],
216+
metrics=[Metric(name='activeUsers')],
217+
date_ranges=[DateRange(start_date=pre_project_date, end_date='today')],
218+
)
219+
response = client.run_report(request)
220+
221+
user_by_country = {}
222+
for row in response.rows:
223+
country = row.dimension_values[0].value
224+
users = int(row.metric_values[0].value)
225+
user_by_country[country] = user_by_country.get(country, 0) + users
226+
227+
return user_by_country
228+
229+
230+
def plot_usersXcountry(FOUNDATIONS_ID):
231+
"""
232+
Function for taking users by country for Pythia Foundations and plotting them on a map.
233+
"""
234+
users_by_country = _run_usersXcountry_report(FOUNDATIONS_ID)
235+
236+
# Google API Country names do not match Cartopy Country Shapefile names
237+
dict_api2cartopy = {
238+
'Tanzania': 'United Republic of Tanzania',
239+
'United States': 'United States of America',
240+
'Congo - Kinshasa': 'Democratic Republic of the Congo',
241+
'Bahamas': 'The Bahamas',
242+
'Timor-Leste': 'East Timor',
243+
'C\u00f4te d\u2019Ivoire': 'Ivory Coast',
244+
'Bosnia & Herzegovina': 'Bosnia and Herzegovina',
245+
'Serbia': 'Republic of Serbia',
246+
'Trinidad & Tobago': 'Trinidad and Tobago',
247+
}
248+
249+
for key in dict_api2cartopy:
250+
users_by_country[dict_api2cartopy[key]] = users_by_country.pop(key)
251+
252+
# Sort by views and grab the top 10 countries for a text box
253+
top_10_countries = sorted(users_by_country.items(), key=lambda item: item[1], reverse=True)[:10]
254+
top_10_text = '\n'.join(
255+
f'{country}: {_format_rounding(value)}' for i, (country, value) in enumerate(top_10_countries)
256+
)
257+
258+
# Plotting code
259+
fig = plt.figure(figsize=(10, 4))
260+
ax = plt.axes(projection=cartopy.crs.PlateCarree(), frameon=False)
261+
ax.set_title('Pythia Foundations Users by Country', fontsize=15)
262+
263+
shapefile = cartopy.io.shapereader.natural_earth(category='cultural', resolution='110m', name='admin_0_countries')
264+
reader = cartopy.io.shapereader.Reader(shapefile)
265+
countries = reader.records()
266+
267+
colormap = plt.get_cmap('Blues')
268+
newcmp = colors.ListedColormap(colormap(np.linspace(0.2, 1, 128))) # Truncate colormap to remove white hues
269+
newcmp.set_extremes(under='grey')
270+
271+
norm = colors.LogNorm(vmin=1, vmax=max(users_by_country.values())) # Plot on log scale
272+
mappable = cm.ScalarMappable(norm=norm, cmap=newcmp)
273+
274+
# Loop through countries and plot their color
275+
for country in countries:
276+
country_name = country.attributes['SOVEREIGNT']
277+
if country_name in users_by_country.keys():
278+
facecolor = newcmp(norm(users_by_country[country_name]))
279+
ax.add_geometries(
280+
[country.geometry],
281+
cartopy.crs.PlateCarree(),
282+
facecolor=facecolor,
283+
edgecolor='white',
284+
linewidth=0.7,
285+
norm=matplotlib.colors.LogNorm(),
286+
)
287+
else:
288+
ax.add_geometries(
289+
[country.geometry], cartopy.crs.PlateCarree(), facecolor='grey', edgecolor='white', linewidth=0.7
290+
)
291+
292+
# Add colorbar
293+
cax = fig.add_axes([0.05, -0.015, 0.7, 0.03]) # [x0, y0, width, height]
294+
cbar = fig.colorbar(mappable=mappable, cax=cax, spacing='uniform', orientation='horizontal', extend='min')
295+
cbar.set_label('Unique Users')
296+
297+
# Add top 10 countries text
298+
props = dict(boxstyle='round', facecolor='white', edgecolor='white')
299+
ax.text(1.01, 0.5, top_10_text, transform=ax.transAxes, fontsize=12, verticalalignment='center', bbox=props)
300+
301+
plt.tight_layout()
302+
plt.savefig('portal/metrics/bycountry.png', bbox_inches='tight')
303+
304+
305+
if __name__ == '__main__':
306+
get_total_users(PORTAL_ID, FOUNDATIONS_ID, COOKBOOKS_ID)
307+
plot_projects_this_year(PORTAL_ID, FOUNDATIONS_ID, COOKBOOKS_ID)
308+
plot_top_pages(PORTAL_ID, FOUNDATIONS_ID, COOKBOOKS_ID)
309+
plot_usersXcountry(FOUNDATIONS_ID)

.github/workflows/publish-site.yaml

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,43 @@ on:
66
branches:
77
- main
88
workflow_dispatch:
9+
schedule:
10+
- cron: '0 0 * * 1' # Weekly on Monday
911

1012
jobs:
13+
automate-metrics:
14+
runs-on: macos-latest
15+
steps:
16+
- uses: actions/checkout@v3
17+
- name: Automate Metrics
18+
env:
19+
PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
20+
PRIVATE_KEY_ID: ${{ secrets.PRIVATE_KEY_ID }}
21+
run: |
22+
python -m venv analytics-api
23+
source analytics-api/bin/activate
24+
pip install google-analytics-data cartopy matplotlib
25+
26+
python .github/workflows/get-metrics.py
27+
python .github/workflows/write-metrics-md.py
28+
- name: Upload zip
29+
uses: actions/upload-artifact@v4
30+
with:
31+
name: repo-zip
32+
path: .
33+
1134
build:
35+
needs: automate-metrics
1236
uses: ProjectPythia/cookbook-actions/.github/workflows/build-book.yaml@main
1337
with:
1438
environment_file: 'environment.yml'
1539
environment_name: pythia
1640
path_to_notebooks: 'portal'
1741
build_command: 'make -j4 html'
18-
42+
build_from_code_artifact: 'true'
43+
code_artifact_name: 'repo-zip'
44+
workflow: ''
45+
workflow_conclusion: ''
1946
deploy:
2047
needs: build
2148
uses: ProjectPythia/cookbook-actions/.github/workflows/deploy-book.yaml@main

.github/workflows/sphinx-link-checker.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ jobs:
5959
if: |
6060
(inputs.use_cached_environment != 'true'
6161
|| steps.cache.outputs.cache-hit != 'true')
62-
run: mamba env update -n ${{ inputs.environment_name }} -f ${{ inputs.environment_file }}
62+
run: |
63+
mamba env update -n ${{ inputs.environment_name }} -f ${{ inputs.environment_file }}
64+
mamba install -c conda-forge sphinxcontrib-applehelp=1.0.4 sphinxcontrib-devhelp=1.0.2 sphinxcontrib-htmlhelp=2.0.1 sphinxcontrib-qthelp=1.0.3 sphinxcontrib-serializinghtml=1.1.5
6365
6466
- name: Check external links
6567
run: |

.github/workflows/trigger-preview.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ on:
1010
jobs:
1111
find-pull-request:
1212
uses: ProjectPythia/cookbook-actions/.github/workflows/find-pull-request.yaml@main
13+
1314
deploy-preview:
1415
needs: find-pull-request
1516
if: github.event.workflow_run.conclusion == 'success'

0 commit comments

Comments
 (0)