Skip to content

Commit 14340cb

Browse files
feat(python): switch to uv and ruff for build tooling
Switched the Python samples' build tooling to uv and ruff. - Updated `pyproject.toml` to use ruff for linting and formatting. - Updated `requirements.txt` to include uv and ruff. - Deleted the custom `pyfmt.py` script. - Updated the GitHub Actions workflow to use uv and ruff. - Fixed all linting errors in the Python samples. - Maintained pip compatibility for deployment.
1 parent b5aa45d commit 14340cb

File tree

22 files changed

+404
-347
lines changed

22 files changed

+404
-347
lines changed

.github/workflows/test_python.yml

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,18 @@ jobs:
4242
uses: actions/setup-python@v4
4343
with:
4444
python-version: ${{ matrix.python-version }}
45-
- name: Install dependencies
45+
- uses: astral-sh/setup-uv@v1
46+
- name: Install dev dependencies
4647
working-directory: ./Python
47-
run: pip install -r requirements.txt
48+
run: uv pip install -r requirements.txt
4849
- name: Lint
4950
working-directory: ./Python
50-
run: python pyfmt.py --check_only --exclude "**/venv/**/*.py" **/*.py
51+
run: |
52+
ruff format --check .
53+
ruff check .
54+
- name: Check pip compatibility by installing sample dependencies
55+
working-directory: ./Python
56+
run: |
57+
find . -mindepth 2 -name "requirements.txt" -print0 | while IFS= read -r -d $'\0' file; do
58+
python -m pip install -r "$file"
59+
done

Python/alerts-to-discord/functions/main.py

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,19 @@
1414

1515
import pprint
1616

17+
# [END v2import]
18+
import requests
19+
1720
# [START v2import]
1821
from firebase_functions import params
1922
from firebase_functions.alerts import app_distribution_fn, crashlytics_fn, performance_fn
20-
# [END v2import]
21-
22-
import requests
2323

2424
DISCORD_WEBHOOK_URL = params.SecretParam("DISCORD_WEBHOOK_URL")
2525

2626

27-
def post_message_to_discord(bot_name: str, message_body: str,
28-
webhook_url: str) -> requests.Response:
27+
def post_message_to_discord(
28+
bot_name: str, message_body: str, webhook_url: str
29+
) -> requests.Response:
2930
"""Posts a message to Discord with Discord's Webhook API.
3031
3132
Params:
@@ -36,24 +37,26 @@ def post_message_to_discord(bot_name: str, message_body: str,
3637
raise EnvironmentError(
3738
"No webhook URL found. Set the Discord Webhook URL before deploying. "
3839
"Learn more about Discord webhooks here: "
39-
"https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks")
40+
"https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks"
41+
)
4042

4143
return requests.post(
4244
url=webhook_url,
4345
json={
4446
# Here's what the Discord API supports in the payload:
4547
# https://discord.com/developers/docs/resources/webhook#execute-webhook-jsonform-params
4648
"username": bot_name,
47-
"content": message_body
48-
})
49+
"content": message_body,
50+
},
51+
)
4952

5053

5154
# [START v2Alerts]
5255
# [START v2CrashlyticsAlertTrigger]
5356
@crashlytics_fn.on_new_fatal_issue_published(secrets=["DISCORD_WEBHOOK_URL"])
5457
def post_fatal_issue_to_discord(event: crashlytics_fn.CrashlyticsNewFatalIssueEvent) -> None:
5558
"""Publishes a message to Discord whenever a new Crashlytics fatal issue occurs."""
56-
# [END v2CrashlyticsAlertTrigger]
59+
# [END v2CrashlyticsAlertTrigger]
5760
# [START v2CrashlyticsEventPayload]
5861
# Construct a helpful message to send to Discord.
5962
app_id = event.app_id
@@ -86,7 +89,7 @@ def post_fatal_issue_to_discord(event: crashlytics_fn.CrashlyticsNewFatalIssueEv
8689
@app_distribution_fn.on_new_tester_ios_device_published(secrets=["DISCORD_WEBHOOK_URL"])
8790
def post_new_udid_to_discord(event: app_distribution_fn.NewTesterDeviceEvent) -> None:
8891
"""Publishes a message to Discord whenever someone registers a new iOS test device."""
89-
# [END v2AppDistributionAlertTrigger]
92+
# [END v2AppDistributionAlertTrigger]
9093
# [START v2AppDistributionEventPayload]
9194
# Construct a helpful message to send to Discord.
9295
app_id = event.app_id
@@ -110,14 +113,15 @@ def post_new_udid_to_discord(event: app_distribution_fn.NewTesterDeviceEvent) ->
110113
except (EnvironmentError, requests.HTTPError) as error:
111114
print(
112115
f"Unable to post iOS device registration alert for {app_dist.tester_email} to Discord.",
113-
error)
116+
error,
117+
)
114118

115119

116120
# [START v2PerformanceAlertTrigger]
117121
@performance_fn.on_threshold_alert_published(secrets=["DISCORD_WEBHOOK_URL"])
118122
def post_performance_alert_to_discord(event: performance_fn.PerformanceThresholdAlertEvent) -> None:
119123
"""Publishes a message to Discord whenever a performance threshold alert is fired."""
120-
# [END v2PerformanceAlertTrigger]
124+
# [END v2PerformanceAlertTrigger]
121125
# [START v2PerformanceEventPayload]
122126
# Construct a helpful message to send to Discord.
123127
app_id = event.app_id
@@ -139,8 +143,9 @@ def post_performance_alert_to_discord(event: performance_fn.PerformanceThreshold
139143

140144
try:
141145
# [START v2SendPerformanceAlertToDiscord]
142-
response = post_message_to_discord("App Performance Bot", message,
143-
DISCORD_WEBHOOK_URL.value)
146+
response = post_message_to_discord(
147+
"App Performance Bot", message, DISCORD_WEBHOOK_URL.value
148+
)
144149
if response.ok:
145150
print(f"Posted Firebase Performance alert {perf.event_name} to Discord.")
146151
pprint.pp(event.data.payload)
@@ -149,4 +154,6 @@ def post_performance_alert_to_discord(event: performance_fn.PerformanceThreshold
149154
# [END v2SendPerformanceAlertToDiscord]
150155
except (EnvironmentError, requests.HTTPError) as error:
151156
print(f"Unable to post Firebase Performance alert {perf.event_name} to Discord.", error)
157+
158+
152159
# [END v2Alerts]

Python/delete-unused-accounts-cron/functions/main.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@
1515
# [START all]
1616
from datetime import datetime, timedelta
1717

18-
# [START import]
19-
# The Cloud Functions for Firebase SDK to set up triggers and logging.
20-
from firebase_functions import scheduler_fn
21-
2218
# The Firebase Admin SDK to delete users.
2319
import firebase_admin
2420
from firebase_admin import auth
2521

22+
# [START import]
23+
# The Cloud Functions for Firebase SDK to set up triggers and logging.
24+
from firebase_functions import scheduler_fn
25+
2626
firebase_admin.initialize_app()
2727
# [END import]
2828

@@ -40,6 +40,8 @@ def accountcleanup(event: scheduler_fn.ScheduledEvent) -> None:
4040
]
4141
auth.delete_users(inactive_uids)
4242
user_page = user_page.get_next_page()
43+
44+
4345
# [END accountcleanup]
4446

4547

@@ -55,4 +57,6 @@ def is_inactive(user: auth.UserRecord, inactive_limit: timedelta) -> bool:
5557
last_seen = datetime.fromtimestamp(last_seen_timestamp)
5658
inactive_time = datetime.now() - last_seen
5759
return inactive_time >= inactive_limit
60+
61+
5862
# [END all]

Python/fcm-notifications/functions/main.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import firebase_admin
2-
from firebase_admin import auth, db, messaging, exceptions
2+
from firebase_admin import auth, db, exceptions, messaging
33
from firebase_functions import db_fn
44

55
firebase_admin.initialize_app()
@@ -25,7 +25,7 @@ def send_follower_notification(event: db_fn.Event[db_fn.Change]) -> None:
2525
print(f"User {follower_uid} is now following user {followed_uid}")
2626
tokens_ref = db.reference(f"users/{followed_uid}/notificationTokens")
2727
notification_tokens = tokens_ref.get()
28-
if (not isinstance(notification_tokens, dict) or len(notification_tokens) < 1):
28+
if not isinstance(notification_tokens, dict) or len(notification_tokens) < 1:
2929
print("There are no tokens to send notifications to.")
3030
return
3131
print(f"There are {len(notification_tokens)} tokens to send notifications to.")
@@ -52,6 +52,8 @@ def send_follower_notification(event: db_fn.Event[db_fn.Change]) -> None:
5252
if not isinstance(exception, exceptions.FirebaseError):
5353
continue
5454
message = exception.http_response.json()["error"]["message"]
55-
if (isinstance(exception, messaging.UnregisteredError) or
56-
message == "The registration token is not a valid FCM registration token"):
55+
if (
56+
isinstance(exception, messaging.UnregisteredError)
57+
or message == "The registration token is not a valid FCM registration token"
58+
):
5759
tokens_ref.child(msgs[i].token).delete()

Python/http-flask/functions/main.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
# limitations under the License.
1414

1515
# [START httpflaskexample]
16-
from firebase_admin import initialize_app, db
17-
from firebase_functions import https_fn
1816
import flask
17+
from firebase_admin import db, initialize_app
18+
from firebase_functions import https_fn
1919

2020
initialize_app()
2121
app = flask.Flask(__name__)
@@ -46,4 +46,6 @@ def add_widget():
4646
def httpsflaskexample(req: https_fn.Request) -> https_fn.Response:
4747
with app.request_context(req.environ):
4848
return app.full_dispatch_request()
49+
50+
4951
# [END httpflaskexample]

Python/post-signup-event/functions/main.py

Lines changed: 45 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -12,26 +12,26 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
from datetime import datetime, timedelta
1615
import json
17-
18-
from firebase_admin import auth, firestore, initialize_app
19-
from firebase_functions import https_fn, identity_fn, tasks_fn, options, params
16+
from datetime import datetime, timedelta
2017

2118
import google.auth
2219
import google.auth.transport.requests
2320
import google.cloud.firestore
2421
import google.cloud.tasks_v2
2522
import google.oauth2.credentials
2623
import googleapiclient.discovery
24+
from firebase_admin import auth, firestore, initialize_app
25+
from firebase_functions import https_fn, identity_fn, options, params, tasks_fn
2726

2827
initialize_app()
2928

3029

3130
# [START savegoogletoken]
3231
@identity_fn.before_user_created()
3332
def savegoogletoken(
34-
event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeCreateResponse | None:
33+
event: identity_fn.AuthBlockingEvent,
34+
) -> identity_fn.BeforeCreateResponse | None:
3535
"""During sign-up, save the Google OAuth2 access token and queue up a task
3636
to schedule an onboarding session on the user's Google Calendar.
3737
@@ -48,25 +48,22 @@ def savegoogletoken(
4848
doc_ref.set({"calendar_access_token": event.credential.access_token}, merge=True)
4949

5050
tasks_client = google.cloud.tasks_v2.CloudTasksClient()
51-
task_queue = tasks_client.queue_path(params.PROJECT_ID.value,
52-
options.SupportedRegion.US_CENTRAL1,
53-
"scheduleonboarding")
51+
task_queue = tasks_client.queue_path(
52+
params.PROJECT_ID.value, options.SupportedRegion.US_CENTRAL1, "scheduleonboarding"
53+
)
5454
target_uri = get_function_url("scheduleonboarding")
55-
calendar_task = google.cloud.tasks_v2.Task(http_request={
56-
"http_method": google.cloud.tasks_v2.HttpMethod.POST,
57-
"url": target_uri,
58-
"headers": {
59-
"Content-type": "application/json"
55+
calendar_task = google.cloud.tasks_v2.Task(
56+
http_request={
57+
"http_method": google.cloud.tasks_v2.HttpMethod.POST,
58+
"url": target_uri,
59+
"headers": {"Content-type": "application/json"},
60+
"body": json.dumps({"data": {"uid": event.data.uid}}).encode(),
6061
},
61-
"body": json.dumps({
62-
"data": {
63-
"uid": event.data.uid
64-
}
65-
}).encode()
66-
},
67-
schedule_time=datetime.now() +
68-
timedelta(minutes=1))
62+
schedule_time=datetime.now() + timedelta(minutes=1),
63+
)
6964
tasks_client.create_task(parent=task_queue, task=calendar_task)
65+
66+
7067
# [END savegoogletoken]
7168

7269

@@ -79,50 +76,54 @@ def scheduleonboarding(request: tasks_fn.CallableRequest) -> https_fn.Response:
7976
"""
8077

8178
if "uid" not in request.data:
82-
return https_fn.Response(status=https_fn.FunctionsErrorCode.INVALID_ARGUMENT,
83-
response="No user specified.")
79+
return https_fn.Response(
80+
status=https_fn.FunctionsErrorCode.INVALID_ARGUMENT, response="No user specified."
81+
)
8482
uid = request.data["uid"]
8583

8684
user_record: auth.UserRecord = auth.get_user(uid)
8785
if user_record.email is None:
88-
return https_fn.Response(status=https_fn.FunctionsErrorCode.INVALID_ARGUMENT,
89-
response="No email address on record.")
86+
return https_fn.Response(
87+
status=https_fn.FunctionsErrorCode.INVALID_ARGUMENT,
88+
response="No email address on record.",
89+
)
9090

9191
firestore_client: google.cloud.firestore.Client = firestore.client()
9292
user_info = firestore_client.collection("user_info").document(uid).get().to_dict()
9393
if not isinstance(user_info, dict) or "calendar_access_token" not in user_info:
94-
return https_fn.Response(status=https_fn.FunctionsErrorCode.PERMISSION_DENIED,
95-
response="No Google OAuth token found.")
94+
return https_fn.Response(
95+
status=https_fn.FunctionsErrorCode.PERMISSION_DENIED,
96+
response="No Google OAuth token found.",
97+
)
9698
calendar_access_token = user_info["calendar_access_token"]
9799
firestore_client.collection("user_info").document(uid).update(
98-
{"calendar_access_token": google.cloud.firestore.DELETE_FIELD})
100+
{"calendar_access_token": google.cloud.firestore.DELETE_FIELD}
101+
)
99102

100103
google_credentials = google.oauth2.credentials.Credentials(token=calendar_access_token)
101104

102-
calendar_client = googleapiclient.discovery.build("calendar",
103-
"v3",
104-
credentials=google_credentials)
105+
calendar_client = googleapiclient.discovery.build(
106+
"calendar", "v3", credentials=google_credentials
107+
)
105108
calendar_event = {
106109
"summary": "Onboarding with ExampleCo",
107110
"location": "Video call",
108111
"description": "Walk through onboarding tasks with an ExampleCo engineer.",
109112
"start": {
110113
"dateTime": (datetime.now() + timedelta(days=3)).isoformat(),
111-
"timeZone": "America/Los_Angeles"
114+
"timeZone": "America/Los_Angeles",
112115
},
113116
"end": {
114117
"dateTime": (datetime.now() + timedelta(days=3, hours=1)).isoformat(),
115-
"timeZone": "America/Los_Angeles"
118+
"timeZone": "America/Los_Angeles",
116119
},
117-
"attendees": [{
118-
"email": user_record.email
119-
}, {
120-
"email": "[email protected]"
121-
}]
120+
"attendees": [{"email": user_record.email}, {"email": "[email protected]"}],
122121
}
123122
calendar_client.events().insert(calendarId="primary", body=calendar_event).execute()
124123

125124
return https_fn.Response("Success")
125+
126+
126127
# [END scheduleonboarding]
127128

128129

@@ -137,10 +138,13 @@ def get_function_url(name: str, location: str = options.SupportedRegion.US_CENTR
137138
The URL of the function
138139
"""
139140
credentials, project_id = google.auth.default(
140-
scopes=["https://www.googleapis.com/auth/cloud-platform"])
141+
scopes=["https://www.googleapis.com/auth/cloud-platform"]
142+
)
141143
authed_session = google.auth.transport.requests.AuthorizedSession(credentials)
142-
url = ("https://cloudfunctions.googleapis.com/v2beta/" +
143-
f"projects/{project_id}/locations/{location}/functions/{name}")
144+
url = (
145+
"https://cloudfunctions.googleapis.com/v2beta/"
146+
+ f"projects/{project_id}/locations/{location}/functions/{name}"
147+
)
144148
response = authed_session.get(url)
145149
data = response.json()
146150
function_url = data["serviceConfig"]["uri"]

0 commit comments

Comments
 (0)