Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/test_python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
working-directory: ./Python
run: pip install -r requirements.txt
run: |
pip install uv
uv pip install --system -r requirements.txt
- name: Lint
working-directory: ./Python
run: python pyfmt.py --check_only --exclude "**/venv/**/*.py" **/*.py
21 changes: 13 additions & 8 deletions Python/alerts-to-discord/functions/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@
DISCORD_WEBHOOK_URL = params.SecretParam("DISCORD_WEBHOOK_URL")


def post_message_to_discord(bot_name: str, message_body: str,
webhook_url: str) -> requests.Response:
def post_message_to_discord(
bot_name: str, message_body: str, webhook_url: str
) -> requests.Response:
"""Posts a message to Discord with Discord's Webhook API.

Params:
Expand All @@ -36,16 +37,18 @@ def post_message_to_discord(bot_name: str, message_body: str,
raise EnvironmentError(
"No webhook URL found. Set the Discord Webhook URL before deploying. "
"Learn more about Discord webhooks here: "
"https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks")
"https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks"
)

return requests.post(
url=webhook_url,
Comment on lines 43 to 44
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The requests.post call is missing a timeout. It's a best practice to always specify a timeout to prevent the function from hanging indefinitely if the external service is unresponsive.

    return requests.post(
        url=webhook_url,
        timeout=10,

json={
# Here's what the Discord API supports in the payload:
# https://discord.com/developers/docs/resources/webhook#execute-webhook-jsonform-params
"username": bot_name,
"content": message_body
})
"content": message_body,
},
)


# [START v2Alerts]
Expand Down Expand Up @@ -110,7 +113,8 @@ def post_new_udid_to_discord(event: app_distribution_fn.NewTesterDeviceEvent) ->
except (EnvironmentError, requests.HTTPError) as error:
print(
f"Unable to post iOS device registration alert for {app_dist.tester_email} to Discord.",
error)
error,
)


# [START v2PerformanceAlertTrigger]
Expand Down Expand Up @@ -139,8 +143,9 @@ def post_performance_alert_to_discord(event: performance_fn.PerformanceThreshold

try:
# [START v2SendPerformanceAlertToDiscord]
response = post_message_to_discord("App Performance Bot", message,
DISCORD_WEBHOOK_URL.value)
response = post_message_to_discord(
"App Performance Bot", message, DISCORD_WEBHOOK_URL.value
)
if response.ok:
print(f"Posted Firebase Performance alert {perf.event_name} to Discord.")
pprint.pp(event.data.payload)
Expand Down
8 changes: 5 additions & 3 deletions Python/fcm-notifications/functions/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def send_follower_notification(event: db_fn.Event[db_fn.Change]) -> None:
print(f"User {follower_uid} is now following user {followed_uid}")
tokens_ref = db.reference(f"users/{followed_uid}/notificationTokens")
notification_tokens = tokens_ref.get()
if (not isinstance(notification_tokens, dict) or len(notification_tokens) < 1):
if not isinstance(notification_tokens, dict) or len(notification_tokens) < 1:
print("There are no tokens to send notifications to.")
return
print(f"There are {len(notification_tokens)} tokens to send notifications to.")
Expand All @@ -52,6 +52,8 @@ def send_follower_notification(event: db_fn.Event[db_fn.Change]) -> None:
if not isinstance(exception, exceptions.FirebaseError):
continue
message = exception.http_response.json()["error"]["message"]
if (isinstance(exception, messaging.UnregisteredError) or
message == "The registration token is not a valid FCM registration token"):
if (
isinstance(exception, messaging.UnregisteredError)
or message == "The registration token is not a valid FCM registration token"
):
tokens_ref.child(msgs[i].token).delete()
75 changes: 38 additions & 37 deletions Python/post-signup-event/functions/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
# [START savegoogletoken]
@identity_fn.before_user_created()
def savegoogletoken(
event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeCreateResponse | None:
event: identity_fn.AuthBlockingEvent,
) -> identity_fn.BeforeCreateResponse | None:
"""During sign-up, save the Google OAuth2 access token and queue up a task
to schedule an onboarding session on the user's Google Calendar.

Expand All @@ -48,24 +49,19 @@ def savegoogletoken(
doc_ref.set({"calendar_access_token": event.credential.access_token}, merge=True)

tasks_client = google.cloud.tasks_v2.CloudTasksClient()
task_queue = tasks_client.queue_path(params.PROJECT_ID.value,
options.SupportedRegion.US_CENTRAL1,
"scheduleonboarding")
task_queue = tasks_client.queue_path(
params.PROJECT_ID.value, options.SupportedRegion.US_CENTRAL1, "scheduleonboarding"
)
target_uri = get_function_url("scheduleonboarding")
calendar_task = google.cloud.tasks_v2.Task(http_request={
"http_method": google.cloud.tasks_v2.HttpMethod.POST,
"url": target_uri,
"headers": {
"Content-type": "application/json"
calendar_task = google.cloud.tasks_v2.Task(
http_request={
"http_method": google.cloud.tasks_v2.HttpMethod.POST,
"url": target_uri,
"headers": {"Content-type": "application/json"},
"body": json.dumps({"data": {"uid": event.data.uid}}).encode(),
},
"body": json.dumps({
"data": {
"uid": event.data.uid
}
}).encode()
},
schedule_time=datetime.now() +
timedelta(minutes=1))
schedule_time=datetime.now() + timedelta(minutes=1),
)
tasks_client.create_task(parent=task_queue, task=calendar_task)
# [END savegoogletoken]

Expand All @@ -79,46 +75,48 @@ def scheduleonboarding(request: tasks_fn.CallableRequest) -> https_fn.Response:
"""

if "uid" not in request.data:
return https_fn.Response(status=https_fn.FunctionsErrorCode.INVALID_ARGUMENT,
response="No user specified.")
return https_fn.Response(
status=https_fn.FunctionsErrorCode.INVALID_ARGUMENT, response="No user specified."
)
uid = request.data["uid"]

user_record: auth.UserRecord = auth.get_user(uid)
if user_record.email is None:
return https_fn.Response(status=https_fn.FunctionsErrorCode.INVALID_ARGUMENT,
response="No email address on record.")
return https_fn.Response(
status=https_fn.FunctionsErrorCode.INVALID_ARGUMENT,
response="No email address on record.",
)

firestore_client: google.cloud.firestore.Client = firestore.client()
user_info = firestore_client.collection("user_info").document(uid).get().to_dict()
if not isinstance(user_info, dict) or "calendar_access_token" not in user_info:
return https_fn.Response(status=https_fn.FunctionsErrorCode.PERMISSION_DENIED,
response="No Google OAuth token found.")
return https_fn.Response(
status=https_fn.FunctionsErrorCode.PERMISSION_DENIED,
response="No Google OAuth token found.",
)
calendar_access_token = user_info["calendar_access_token"]
firestore_client.collection("user_info").document(uid).update(
{"calendar_access_token": google.cloud.firestore.DELETE_FIELD})
{"calendar_access_token": google.cloud.firestore.DELETE_FIELD}
)

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

calendar_client = googleapiclient.discovery.build("calendar",
"v3",
credentials=google_credentials)
calendar_client = googleapiclient.discovery.build(
"calendar", "v3", credentials=google_credentials
)
calendar_event = {
"summary": "Onboarding with ExampleCo",
"location": "Video call",
"description": "Walk through onboarding tasks with an ExampleCo engineer.",
"start": {
"dateTime": (datetime.now() + timedelta(days=3)).isoformat(),
"timeZone": "America/Los_Angeles"
"timeZone": "America/Los_Angeles",
Comment on lines 112 to +113
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In Cloud Functions, datetime.now() typically returns the time in UTC. Labeling this time with the America/Los_Angeles timezone in the Google Calendar API will result in the event being scheduled at the wrong time (shifted by the UTC offset). It is safer and more correct to use UTC as the timezone if you are using the system's current time.

Suggested change
"dateTime": (datetime.now() + timedelta(days=3)).isoformat(),
"timeZone": "America/Los_Angeles"
"timeZone": "America/Los_Angeles",
"dateTime": (datetime.now() + timedelta(days=3)).isoformat(),
"timeZone": "UTC",

},
"end": {
"dateTime": (datetime.now() + timedelta(days=3, hours=1)).isoformat(),
"timeZone": "America/Los_Angeles"
"timeZone": "America/Los_Angeles",
Comment on lines 116 to +117
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to the start time, the end time should also use UTC to avoid a logic error caused by the mismatch between the UTC timestamp from datetime.now() and the hardcoded LA timezone.

Suggested change
"dateTime": (datetime.now() + timedelta(days=3, hours=1)).isoformat(),
"timeZone": "America/Los_Angeles"
"timeZone": "America/Los_Angeles",
"dateTime": (datetime.now() + timedelta(days=3, hours=1)).isoformat(),
"timeZone": "UTC",

},
"attendees": [{
"email": user_record.email
}, {
"email": "onboarding@example.com"
}]
"attendees": [{"email": user_record.email}, {"email": "onboarding@example.com"}],
}
calendar_client.events().insert(calendarId="primary", body=calendar_event).execute()

Expand All @@ -137,10 +135,13 @@ def get_function_url(name: str, location: str = options.SupportedRegion.US_CENTR
The URL of the function
"""
credentials, project_id = google.auth.default(
scopes=["https://www.googleapis.com/auth/cloud-platform"])
scopes=["https://www.googleapis.com/auth/cloud-platform"]
)
authed_session = google.auth.transport.requests.AuthorizedSession(credentials)
url = ("https://cloudfunctions.googleapis.com/v2beta/" +
f"projects/{project_id}/locations/{location}/functions/{name}")
url = (
"https://cloudfunctions.googleapis.com/v2beta/"
+ f"projects/{project_id}/locations/{location}/functions/{name}"
)
response = authed_session.get(url)
data = response.json()
function_url = data["serviceConfig"]["uri"]
Expand Down
43 changes: 26 additions & 17 deletions Python/pyfmt.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@
import difflib
import pathlib
import re

from yapf.yapflib import yapf_api
import subprocess

start_tag_re = re.compile(r"^([ \t]*)#\s*\[START\s+(\w+).*\]\s*\n", flags=re.MULTILINE)
end_tag_re = re.compile(r"^\s*#\s*\[END\s+(\w+).*\][ \t]*$", flags=re.MULTILINE)
Expand All @@ -41,19 +40,25 @@ def check_and_diff(files: list[str]) -> int:
orig = f.read()
fmt = format(orig)
diff = list(
difflib.unified_diff(orig.splitlines(),
fmt.splitlines(),
fromfile=file,
tofile=f"{file} (reformatted)",
lineterm=""))
difflib.unified_diff(
orig.splitlines(),
fmt.splitlines(),
fromfile=file,
tofile=f"{file} (reformatted)",
lineterm="",
)
)
if len(diff) > 0:
diff_count += 1
print("\n".join(diff), end="\n\n")
return diff_count


def format(src: str) -> str:
out, _ = yapf_api.FormatCode(src, style_config=pyproject_toml)
result = subprocess.run(
["ruff", "format", "-"], input=src.encode("utf-8"), capture_output=True, check=True
)
Comment on lines +58 to +60
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The ruff command is called without specifying the configuration file. Since pyproject_toml is already defined in this script (and currently unused), it should be passed to ruff to ensure the formatting follows the project's rules. Additionally, consider using stdout=subprocess.PIPE instead of capture_output=True if you want to allow stderr (like warnings or errors) to be visible in the console instead of being swallowed.

Suggested change
result = subprocess.run(
["ruff", "format", "-"], input=src.encode("utf-8"), capture_output=True, check=True
)
result = subprocess.run(
["ruff", "format", "--config", pyproject_toml, "-"],
input=src.encode("utf-8"),
capture_output=True,
check=True,
)

out = result.stdout.decode("utf-8")
out = fix_region_tags(out)
return out

Expand Down Expand Up @@ -83,15 +88,19 @@ def fix_end_tag(m: re.Match) -> str:
import argparse

argparser = argparse.ArgumentParser()
argparser.add_argument("--check_only",
"-c",
action="store_true",
help="check files and print diffs, but don't modify files")
argparser.add_argument("--exclude",
"-e",
action="append",
default=[],
help="exclude file or glob (can specify multiple times)")
argparser.add_argument(
"--check_only",
"-c",
action="store_true",
help="check files and print diffs, but don't modify files",
)
argparser.add_argument(
"--exclude",
"-e",
action="append",
default=[],
help="exclude file or glob (can specify multiple times)",
)
argparser.add_argument("file_or_glob", nargs="+")
args = argparser.parse_args()

Expand Down
5 changes: 2 additions & 3 deletions Python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@
[project]
name = "functions_samples"
version = "1.0.0"
[tool.yapf]
based_on_style = "google"
column_limit = 100
[tool.ruff]
line-length = 100
[tool.mypy]
python_version = "3.10"
Loading
Loading