Skip to content

Commit 1a7ebdd

Browse files
committed
Upgrade server dependencies
1 parent b9cf779 commit 1a7ebdd

18 files changed

Lines changed: 329 additions & 116 deletions

server/aicon.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import base64
22
import binascii
33
from google import auth
4+
from google.auth.exceptions import RefreshError
5+
from google.auth.transport.requests import Request as GoogleAuthRequest
46
from io import BytesIO
57
from os import environ
68
from PIL import Image
@@ -48,8 +50,8 @@ def _access_token(self):
4850
# Refresh the credentials, if needed.
4951
if not self._credentials.valid:
5052
try:
51-
self._credentials.refresh(auth.transport.requests.Request())
52-
except auth.exceptions.RefreshError as e:
53+
self._credentials.refresh(GoogleAuthRequest())
54+
except RefreshError as e:
5355
raise ContentError(f'Failed to refresh credentials: {e}')
5456

5557
# Return the access token.
@@ -112,8 +114,9 @@ def calculate_crop_ratio(ratio_tuple):
112114
scale = max(width / image.width, height / image.height)
113115
scaled_width = int(image.width * scale)
114116
scaled_height = int(image.height * scale)
115-
with image.resize((scaled_width, scaled_height),
116-
resample=Image.LANCZOS) as scaled_image:
117+
with image.resize(
118+
(scaled_width, scaled_height),
119+
resample=Image.Resampling.LANCZOS) as scaled_image:
117120

118121
# Center crop.
119122
left = (scaled_width - width) // 2

server/artwork.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,7 @@ def image(self, user, width, height, variant):
3333
with image.crop((x, y, x + width, y + height)) as cropped_image:
3434

3535
# The source artwork is already quantized (no dithering).
36-
return cropped_image.convert('P', dither=None,
37-
palette=Image.ADAPTIVE)
36+
return cropped_image.convert(
37+
'P',
38+
dither=Image.Dither.NONE,
39+
palette=Image.Palette.ADAPTIVE)

server/auth.py

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,24 @@
11
from flask import request
22
from flask import url_for
33
from functools import wraps
4-
from googleapiclient.http import build_http
4+
from google_auth_oauthlib.flow import Flow
55
from logging import error
6-
from oauth2client.client import HttpAccessTokenRefreshError
7-
from oauth2client.client import OAuth2WebServerFlow
86
from re import compile as re_compile
97

108
from firestore import Firestore
9+
from firestore import GOOGLE_CALENDAR_SCOPE
10+
from firestore import GOOGLE_TOKEN_URI
1111
from firestore import GoogleCalendarStorage
1212
from response import display_metadata
1313
from response import forbidden_response
1414
from response import settings_response
1515
from response import text_response
1616

17-
# The scope to request for the Google Calendar API.
18-
GOOGLE_CALENDAR_SCOPE = 'https://www.googleapis.com/auth/calendar.readonly'
17+
# The Google OAuth authorization endpoint.
18+
GOOGLE_AUTH_URI = 'https://accounts.google.com/o/oauth2/v2/auth'
19+
20+
# The OAuth client type expected by google-auth-oauthlib.
21+
OAUTH_CLIENT_TYPE = 'web'
1922

2023
# The URL where Google Calendar access can be revoked.
2124
ACCOUNT_ACCESS_URL = 'https://myaccount.google.com/permissions'
@@ -107,40 +110,49 @@ def _google_calendar_flow(key):
107110
"""Creates the OAuth flow."""
108111

109112
secrets = Firestore().google_calendar_secrets()
110-
return OAuth2WebServerFlow(client_id=secrets['client_id'],
111-
client_secret=secrets['client_secret'],
112-
scope=GOOGLE_CALENDAR_SCOPE,
113-
state=key,
114-
redirect_uri=_oauth_url())
113+
client_config = {
114+
OAUTH_CLIENT_TYPE: {
115+
'client_id': secrets['client_id'],
116+
'client_secret': secrets['client_secret'],
117+
'auth_uri': GOOGLE_AUTH_URI,
118+
'token_uri': GOOGLE_TOKEN_URI}}
119+
120+
return Flow.from_client_config(client_config,
121+
scopes=[GOOGLE_CALENDAR_SCOPE],
122+
state=key,
123+
redirect_uri=_oauth_url())
115124

116125

117126
def google_calendar_step1(key):
118127
"""Creates the URL for the first OAuth step."""
119128

120129
# The user key is passed through the flow as state.
121130
flow = _google_calendar_flow(key)
122-
return flow.step1_get_authorize_url(state=key)
131+
authorization_url, _ = flow.authorization_url(
132+
access_type='offline',
133+
include_granted_scopes='true',
134+
prompt='consent',
135+
state=key)
136+
137+
return authorization_url
123138

124139

125140
def _google_calendar_step2(key, code):
126-
"""Creates the URL for the second OAuth step."""
141+
"""Exchanges an authorization code for credentials."""
127142

128143
flow = _google_calendar_flow(key)
129-
return flow.step2_exchange(code=code)
144+
flow.fetch_token(code=code)
145+
146+
return flow.credentials
130147

131148

132149
def oauth_step2(key, scope, code):
133150
"""Exchanges and saves the OAuth credentials."""
134151

135152
# Use scope-specific token exchange and storage steps.
136-
if scope == GOOGLE_CALENDAR_SCOPE:
153+
if GOOGLE_CALENDAR_SCOPE in (scope or '').split():
137154
credentials = _google_calendar_step2(key, code)
138155
storage = GoogleCalendarStorage(key)
139-
credentials.set_store(storage)
140-
try:
141-
credentials.refresh(build_http())
142-
except HttpAccessTokenRefreshError as e:
143-
storage.delete()
144-
error('Token refresh error: %s' % e)
156+
storage.put(credentials)
145157
else:
146158
error('Unknown OAuth scope: %s' % scope)

server/city.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1037,4 +1037,6 @@ def image(self, user, width, height, variant):
10371037
raise ContentError(e)
10381038

10391039
# The city image is already quantized (no dithering).
1040-
return image.convert('P', dither=None, palette=Image.ADAPTIVE)
1040+
return image.convert('P',
1041+
dither=Image.Dither.NONE,
1042+
palette=Image.Palette.ADAPTIVE)

server/commute.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,6 @@ def image(self, user, width, height, variant):
7777
image=image)
7878

7979
# The map looks better without dithering.
80-
return image.convert('P', dither=None, palette=Image.ADAPTIVE)
80+
return image.convert('P',
81+
dither=Image.Dither.NONE,
82+
palette=Image.Palette.ADAPTIVE)
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
[build-system]
2-
requires = ['setuptools==70.0.0', 'wheel==0.43.0', 'numpy==1.26.4']
2+
requires = ['setuptools==82.0.1', 'wheel==0.47.0', 'numpy==2.4.4']
33
build-backend = 'setuptools.build_meta'
44

55
[project]
66
name = 'dithering'
77
version = '1.0.0'
88
description = 'Floyd-Steinberg dithering written in C'
9-
dependencies = ['numpy==1.26.4']
9+
dependencies = ['numpy==2.4.4']

server/epd.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,9 @@ def color_indices(image, variant):
7272
# Quantize the image using the palette.
7373
with Image.new('P', (1, 1)) as palette_image:
7474
palette_image.putpalette(palette_data)
75-
with rgb_image.quantize(palette=palette_image,
76-
dither=Image.NONE) as quantized_image:
75+
with rgb_image.quantize(
76+
palette=palette_image,
77+
dither=Image.Dither.NONE) as quantized_image:
7778
return np.array(quantized_image)
7879

7980

server/everyone.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from astral import AstralError
21
from cachetools import cached
32
from cachetools import TTLCache
43
from content import ContentError
@@ -41,7 +40,7 @@ def _markers(self):
4140

4241
markers += '|%f,%f' % (anonymized.latitude,
4342
anonymized.longitude)
44-
except (KeyError, AstralError):
43+
except (KeyError, DataError):
4544
# Skip users with address errors.
4645
pass
4746

@@ -56,6 +55,8 @@ def image(self, user, width, height, variant):
5655
marker_icon=MARKER_ICON_URL)
5756

5857
# The map looks better without dithering.
59-
return image.convert('P', dither=None, palette=Image.ADAPTIVE)
58+
return image.convert('P',
59+
dither=Image.Dither.NONE,
60+
palette=Image.Palette.ADAPTIVE)
6061
except DataError as e:
6162
raise ContentError(e)

server/firestore.py

Lines changed: 75 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,36 @@
1-
from firebase_admin import _apps as firebase_apps
1+
from datetime import datetime
22
from firebase_admin import initialize_app
3+
from firebase_admin import get_app
34
from firebase_admin.credentials import ApplicationDefault
45
from firebase_admin.firestore import client as firestore_client
5-
from googleapiclient.http import build_http
6+
from google.auth.exceptions import RefreshError
7+
from google.auth.transport.requests import Request as GoogleAuthRequest
68
from google.cloud.firestore import DELETE_FIELD
9+
from google.oauth2.credentials import Credentials
10+
from json import loads
711
from logging import error
812
from logging import info
913
from logging import warning
10-
from oauth2client.client import HttpAccessTokenRefreshError
11-
from oauth2client.client import OAuth2Credentials
12-
from oauth2client.client import Storage
1314
from os import environ
14-
from threading import Lock
15+
16+
# The scope to request for the Google Calendar API.
17+
GOOGLE_CALENDAR_SCOPE = 'https://www.googleapis.com/auth/calendar.readonly'
18+
19+
# The token endpoint for Google OAuth.
20+
GOOGLE_TOKEN_URI = 'https://oauth2.googleapis.com/token'
21+
22+
# The timestamp format used by oauth2client credential JSON.
23+
OAUTH2CLIENT_EXPIRY_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
1524

1625

1726
class Firestore(object):
1827
"""A wrapper around the Cloud Firestore database."""
1928

2029
def __init__(self):
2130
# Only initialize Firebase once.
22-
if not len(firebase_apps):
31+
try:
32+
get_app()
33+
except ValueError:
2334
initialize_app(ApplicationDefault(), {
2435
'projectId': environ['GOOGLE_CLOUD_PROJECT']
2536
})
@@ -54,6 +65,37 @@ def google_calendar_secrets(self):
5465

5566
return secrets.to_dict()
5667

68+
def _google_calendar_credentials_from_json(self, credentials_json):
69+
"""Loads Google Calendar credentials from current or legacy JSON."""
70+
71+
info = loads(credentials_json)
72+
if info.get('invalid'):
73+
return None
74+
75+
scopes = info.get('scopes') or [GOOGLE_CALENDAR_SCOPE]
76+
77+
# Migrate JSON written by the deprecated oauth2client package.
78+
if 'access_token' in info:
79+
return Credentials(
80+
token=info.get('access_token'),
81+
refresh_token=info.get('refresh_token'),
82+
token_uri=info.get('token_uri') or GOOGLE_TOKEN_URI,
83+
client_id=info.get('client_id'),
84+
client_secret=info.get('client_secret'),
85+
scopes=scopes,
86+
expiry=self._parse_oauth2client_expiry(
87+
info.get('token_expiry')))
88+
89+
return Credentials.from_authorized_user_info(info, scopes=scopes)
90+
91+
def _parse_oauth2client_expiry(self, expiry):
92+
"""Parses oauth2client's naive UTC expiry timestamp."""
93+
94+
if not expiry:
95+
return None
96+
97+
return datetime.strptime(expiry, OAUTH2CLIENT_EXPIRY_FORMAT)
98+
5799
def google_calendar_credentials(self, key):
58100
"""Loads and refreshes Google Calendar API credentials."""
59101

@@ -64,23 +106,31 @@ def google_calendar_credentials(self, key):
64106

65107
# Load the credentials from storage.
66108
try:
67-
json = user.get('google_calendar_credentials')
109+
credentials_json = user.get('google_calendar_credentials')
68110
except KeyError:
69111
warning('Failed to load Google Calendar credentials.')
70112
return None
71113

72114
# Use the valid credentials.
73-
credentials = OAuth2Credentials.from_json(json)
74-
if credentials and not credentials.invalid:
115+
try:
116+
credentials = self._google_calendar_credentials_from_json(
117+
credentials_json)
118+
except (TypeError, ValueError) as e:
119+
warning('Failed to parse Google Calendar credentials: %s' % e)
120+
self.delete_google_calendar_credentials(key)
121+
return None
122+
123+
if credentials and credentials.valid:
75124
return credentials
76125

77126
# Handle invalidation and expiration.
78-
if credentials and credentials.access_token_expired:
127+
if credentials and credentials.refresh_token:
79128
try:
80129
info('Refreshing Google Calendar credentials.')
81-
credentials.refresh(build_http())
130+
credentials.refresh(GoogleAuthRequest())
131+
self.update_google_calendar_credentials(key, credentials)
82132
return credentials
83-
except HttpAccessTokenRefreshError as e:
133+
except RefreshError as e:
84134
warning('Google Calendar refresh failed: %s' % e)
85135

86136
# Credentials are missing or refresh failed.
@@ -136,30 +186,31 @@ def update_user(self, key, fields):
136186
user.update(fields)
137187

138188

139-
class GoogleCalendarStorage(Storage):
189+
class GoogleCalendarStorage(object):
140190
"""Credentials storage for the Google Calendar API using Firestore."""
141191

142192
def __init__(self, key):
143-
super(GoogleCalendarStorage, self).__init__(lock=Lock())
144193
self._firestore = Firestore()
145194
self._key = key
146195

147-
def locked_get(self):
148-
"""Loads credentials from Firestore and attaches this storage."""
196+
def get(self):
197+
"""Loads credentials from Firestore."""
149198

150-
credentials = self._firestore.google_calendar_credentials(self._key)
151-
if not credentials:
152-
return None
153-
credentials.set_store(self)
154-
return credentials
199+
return self._firestore.google_calendar_credentials(self._key)
155200

156-
def locked_put(self, credentials):
201+
def put(self, credentials):
157202
"""Saves credentials to Firestore."""
158203

159204
self._firestore.update_google_calendar_credentials(self._key,
160205
credentials)
161206

162-
def locked_delete(self):
207+
def refresh(self, credentials):
208+
"""Refreshes credentials and saves the refreshed token."""
209+
210+
credentials.refresh(GoogleAuthRequest())
211+
self.put(credentials)
212+
213+
def delete(self):
163214
"""Deletes credentials from Firestore."""
164215

165216
self._firestore.delete_google_calendar_credentials(self._key)

0 commit comments

Comments
 (0)