diff --git a/.github/workflows/test_python.yml b/.github/workflows/test_python.yml index c0414c4734..ebf4298beb 100644 --- a/.github/workflows/test_python.yml +++ b/.github/workflows/test_python.yml @@ -42,9 +42,18 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - - name: Install dependencies + - uses: astral-sh/setup-uv@v1 + - name: Install dev dependencies working-directory: ./Python - run: pip install -r requirements.txt + run: uv pip install --system -r requirements.txt - name: Lint working-directory: ./Python - run: python pyfmt.py --check_only --exclude "**/venv/**/*.py" **/*.py + run: | + ruff format --check . + ruff check . + - name: Check pip compatibility by installing sample dependencies + working-directory: ./Python + run: | + find . -mindepth 2 -name "requirements.txt" -print0 | while IFS= read -r -d $'\0' file; do + python -m pip install -r "$file" + done diff --git a/Python/alerts-to-discord/functions/main.py b/Python/alerts-to-discord/functions/main.py index d8eba42135..3d4f910ca2 100644 --- a/Python/alerts-to-discord/functions/main.py +++ b/Python/alerts-to-discord/functions/main.py @@ -14,18 +14,19 @@ import pprint +# [END v2import] +import requests + # [START v2import] from firebase_functions import params from firebase_functions.alerts import app_distribution_fn, crashlytics_fn, performance_fn -# [END v2import] - -import requests 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: @@ -36,7 +37,8 @@ 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, @@ -44,8 +46,9 @@ def post_message_to_discord(bot_name: str, message_body: str, # 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] @@ -53,7 +56,7 @@ def post_message_to_discord(bot_name: str, message_body: str, @crashlytics_fn.on_new_fatal_issue_published(secrets=["DISCORD_WEBHOOK_URL"]) def post_fatal_issue_to_discord(event: crashlytics_fn.CrashlyticsNewFatalIssueEvent) -> None: """Publishes a message to Discord whenever a new Crashlytics fatal issue occurs.""" -# [END v2CrashlyticsAlertTrigger] + # [END v2CrashlyticsAlertTrigger] # [START v2CrashlyticsEventPayload] # Construct a helpful message to send to Discord. app_id = event.app_id @@ -86,7 +89,7 @@ def post_fatal_issue_to_discord(event: crashlytics_fn.CrashlyticsNewFatalIssueEv @app_distribution_fn.on_new_tester_ios_device_published(secrets=["DISCORD_WEBHOOK_URL"]) def post_new_udid_to_discord(event: app_distribution_fn.NewTesterDeviceEvent) -> None: """Publishes a message to Discord whenever someone registers a new iOS test device.""" -# [END v2AppDistributionAlertTrigger] + # [END v2AppDistributionAlertTrigger] # [START v2AppDistributionEventPayload] # Construct a helpful message to send to Discord. app_id = event.app_id @@ -110,14 +113,15 @@ 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] @performance_fn.on_threshold_alert_published(secrets=["DISCORD_WEBHOOK_URL"]) def post_performance_alert_to_discord(event: performance_fn.PerformanceThresholdAlertEvent) -> None: """Publishes a message to Discord whenever a performance threshold alert is fired.""" -# [END v2PerformanceAlertTrigger] + # [END v2PerformanceAlertTrigger] # [START v2PerformanceEventPayload] # Construct a helpful message to send to Discord. app_id = event.app_id @@ -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) @@ -149,4 +154,6 @@ def post_performance_alert_to_discord(event: performance_fn.PerformanceThreshold # [END v2SendPerformanceAlertToDiscord] except (EnvironmentError, requests.HTTPError) as error: print(f"Unable to post Firebase Performance alert {perf.event_name} to Discord.", error) + + # [END v2Alerts] diff --git a/Python/delete-unused-accounts-cron/functions/main.py b/Python/delete-unused-accounts-cron/functions/main.py index 2fe4dc6f87..20be2ffd07 100644 --- a/Python/delete-unused-accounts-cron/functions/main.py +++ b/Python/delete-unused-accounts-cron/functions/main.py @@ -15,14 +15,14 @@ # [START all] from datetime import datetime, timedelta -# [START import] -# The Cloud Functions for Firebase SDK to set up triggers and logging. -from firebase_functions import scheduler_fn - # The Firebase Admin SDK to delete users. import firebase_admin from firebase_admin import auth +# [START import] +# The Cloud Functions for Firebase SDK to set up triggers and logging. +from firebase_functions import scheduler_fn + firebase_admin.initialize_app() # [END import] @@ -40,6 +40,8 @@ def accountcleanup(event: scheduler_fn.ScheduledEvent) -> None: ] auth.delete_users(inactive_uids) user_page = user_page.get_next_page() + + # [END accountcleanup] @@ -55,4 +57,6 @@ def is_inactive(user: auth.UserRecord, inactive_limit: timedelta) -> bool: last_seen = datetime.fromtimestamp(last_seen_timestamp) inactive_time = datetime.now() - last_seen return inactive_time >= inactive_limit + + # [END all] diff --git a/Python/fcm-notifications/functions/main.py b/Python/fcm-notifications/functions/main.py index 362ad47911..bed2d22d6b 100644 --- a/Python/fcm-notifications/functions/main.py +++ b/Python/fcm-notifications/functions/main.py @@ -1,5 +1,5 @@ import firebase_admin -from firebase_admin import auth, db, messaging, exceptions +from firebase_admin import auth, db, exceptions, messaging from firebase_functions import db_fn firebase_admin.initialize_app() @@ -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.") @@ -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() diff --git a/Python/http-flask/functions/main.py b/Python/http-flask/functions/main.py index 52a9901d57..a28fccf2f7 100644 --- a/Python/http-flask/functions/main.py +++ b/Python/http-flask/functions/main.py @@ -13,9 +13,9 @@ # limitations under the License. # [START httpflaskexample] -from firebase_admin import initialize_app, db -from firebase_functions import https_fn import flask +from firebase_admin import db, initialize_app +from firebase_functions import https_fn initialize_app() app = flask.Flask(__name__) @@ -46,4 +46,6 @@ def add_widget(): def httpsflaskexample(req: https_fn.Request) -> https_fn.Response: with app.request_context(req.environ): return app.full_dispatch_request() + + # [END httpflaskexample] diff --git a/Python/post-signup-event/functions/main.py b/Python/post-signup-event/functions/main.py index 02f4e8cd5a..be0384583a 100644 --- a/Python/post-signup-event/functions/main.py +++ b/Python/post-signup-event/functions/main.py @@ -12,11 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from datetime import datetime, timedelta import json - -from firebase_admin import auth, firestore, initialize_app -from firebase_functions import https_fn, identity_fn, tasks_fn, options, params +from datetime import datetime, timedelta import google.auth import google.auth.transport.requests @@ -24,6 +21,8 @@ import google.cloud.tasks_v2 import google.oauth2.credentials import googleapiclient.discovery +from firebase_admin import auth, firestore, initialize_app +from firebase_functions import https_fn, identity_fn, options, params, tasks_fn initialize_app() @@ -31,7 +30,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. @@ -48,25 +48,22 @@ 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] @@ -79,50 +76,54 @@ 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", }, "end": { "dateTime": (datetime.now() + timedelta(days=3, hours=1)).isoformat(), - "timeZone": "America/Los_Angeles" + "timeZone": "America/Los_Angeles", }, - "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() return https_fn.Response("Success") + + # [END scheduleonboarding] @@ -137,10 +138,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"] diff --git a/Python/pyfmt.py b/Python/pyfmt.py deleted file mode 100644 index b9b5f414fc..0000000000 --- a/Python/pyfmt.py +++ /dev/null @@ -1,109 +0,0 @@ -"""Utility to format Python source files for Firebase docs.""" - -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import difflib -import pathlib -import re - -from yapf.yapflib import yapf_api - -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) - -pyproject_toml = str(pathlib.Path(__file__).parent / "pyproject.toml") - - -def reformat_in_place(files: list[str]) -> None: - for file in files: - with open(file, "rt", encoding="utf-8") as f: - src = format(f.read()) - with open(file, "wt", encoding="utf-8") as f: - f.write(src) - - -def check_and_diff(files: list[str]) -> int: - diff_count = 0 - for file in files: - with open(file, "rt", encoding="utf-8") as f: - orig = f.read() - fmt = format(orig) - diff = list( - 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) - out = fix_region_tags(out) - return out - - -def fix_region_tags(src: str) -> str: - """Fix formattiing of region tags. - - - Remove extra blank lines after START tags. - - Remove extra blank lines before END tags. - - Matches indentation of END tags to their START tags. - """ - src = start_tag_re.sub(r"\1# [START \2]\n", src) - - tag_indentation = {m.group(2): m.group(1) for m in start_tag_re.finditer(src)} - - def fix_end_tag(m: re.Match) -> str: - name = m.group(1) - indentation = tag_indentation[name] - return f"{indentation}# [END {name}]" - - src = end_tag_re.sub(fix_end_tag, src) - - return src - - -if __name__ == "__main__": - 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("file_or_glob", nargs="+") - args = argparser.parse_args() - - files = {str(f) for fs in [pathlib.Path(".").glob(fg) for fg in args.file_or_glob] for f in fs} - excludes = {str(f) for fs in [pathlib.Path(".").glob(fg) for fg in args.exclude] for f in fs} - files = files - excludes - - if args.check_only: - diff_count = check_and_diff(files) - if diff_count != 0: - print(f"{diff_count} files would be reformatted.") - print(f"Run {argparser.prog} to reformat in place.") - exit(diff_count) - else: - reformat_in_place(files) diff --git a/Python/pyproject.toml b/Python/pyproject.toml index b2db2e3432..b36e1a3c11 100644 --- a/Python/pyproject.toml +++ b/Python/pyproject.toml @@ -15,8 +15,23 @@ [project] name = "functions_samples" version = "1.0.0" -[tool.yapf] -based_on_style = "google" -column_limit = 100 -[tool.mypy] -python_version = "3.10" \ No newline at end of file +requires-python = ">=3.10" +dependencies = [ + "uv", +] + +[tool.ruff] +line-length = 100 + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # Pyflakes + "I", # isort + "C4", # flake8-comprehensions + "B", # flake8-bugbear +] +ignore = [ + "E501", # Line too long, handled by formatter +] diff --git a/Python/quickstarts/auth-blocking-functions/functions/main.py b/Python/quickstarts/auth-blocking-functions/functions/main.py index 17abd3e254..41e48b79ef 100644 --- a/Python/quickstarts/auth-blocking-functions/functions/main.py +++ b/Python/quickstarts/auth-blocking-functions/functions/main.py @@ -12,10 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -from firebase_admin import auth, firestore, initialize_app -from firebase_functions import identity_fn, https_fn - import google.cloud.firestore +from firebase_admin import auth, firestore, initialize_app +from firebase_functions import https_fn, identity_fn initialize_app() @@ -24,6 +23,8 @@ @identity_fn.before_user_created() def created_noop(event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeCreateResponse | None: return + + # [END created_noop] @@ -31,6 +32,8 @@ def created_noop(event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeCrea @identity_fn.before_user_signed_in() def signedin_noop(event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeSignInResponse | None: return + + # [END signedin_noop] @@ -39,8 +42,9 @@ def signedin_noop(event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeSig # Block account creation with any non-acme email address. @identity_fn.before_user_created() def validatenewuser( - event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeCreateResponse | None: -# [END v2beforeCreateFunctionTrigger] + event: identity_fn.AuthBlockingEvent, +) -> identity_fn.BeforeCreateResponse | None: + # [END v2beforeCreateFunctionTrigger] # [START v2readUserData] # User data passed in from the CloudEvent. user = event.data @@ -50,9 +54,12 @@ def validatenewuser( # Only users of a specific domain can sign up. if user.email is None or "@acme.com" not in user.email: # Return None so that Firebase Auth rejects the account creation. - raise https_fn.HttpsError(code=https_fn.FunctionsErrorCode.INVALID_ARGUMENT, - message="Unauthorized email") + raise https_fn.HttpsError( + code=https_fn.FunctionsErrorCode.INVALID_ARGUMENT, message="Unauthorized email" + ) # [END v2domainHttpsError] + + # [END v2ValidateNewUser] @@ -61,17 +68,25 @@ def validatenewuser( def setdefaultname(event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeCreateResponse | None: return identity_fn.BeforeCreateResponse( # If no display name is provided, set it to "Guest". - display_name=event.data.display_name if event.data.display_name is not None else "Guest") + display_name=event.data.display_name if event.data.display_name is not None else "Guest" + ) + + # [END setdefaultname] # [START requireverified] @identity_fn.before_user_created() def requireverified( - event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeCreateResponse | None: + event: identity_fn.AuthBlockingEvent, +) -> identity_fn.BeforeCreateResponse | None: if event.data.email is not None and not event.data.email_verified: - raise https_fn.HttpsError(code=https_fn.FunctionsErrorCode.INVALID_ARGUMENT, - message="You must register using a trusted provider.") + raise https_fn.HttpsError( + code=https_fn.FunctionsErrorCode.INVALID_ARGUMENT, + message="You must register using a trusted provider.", + ) + + # [END requireverified] @@ -83,20 +98,28 @@ def send_verification_email_using_your_smtp_server(email, link): # [START sendverification] @identity_fn.before_user_created() def sendverification( - event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeCreateResponse | None: + event: identity_fn.AuthBlockingEvent, +) -> identity_fn.BeforeCreateResponse | None: if event.data.email is not None and not event.data.email_verified: link = auth.generate_email_verification_link(event.data.email) send_verification_email_using_your_smtp_server(event.data.email, link) + + # [END sendverification] # [START requireverifiedsignin] @identity_fn.before_user_signed_in() def requireverifiedsignin( - event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeSignInResponse | None: + event: identity_fn.AuthBlockingEvent, +) -> identity_fn.BeforeSignInResponse | None: if event.data.email is not None and not event.data.email_verified: - raise https_fn.HttpsError(code=https_fn.FunctionsErrorCode.INVALID_ARGUMENT, - message="You must verify your email address before signing in.") + raise https_fn.HttpsError( + code=https_fn.FunctionsErrorCode.INVALID_ARGUMENT, + message="You must verify your email address before signing in.", + ) + + # [END requireverifiedsignin] @@ -105,6 +128,8 @@ def requireverifiedsignin( def markverified(event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeCreateResponse | None: if event.data.email is not None and "@facebook.com" in event.data.email: return identity_fn.BeforeSignInResponse(email_verified=True) + + # [END trustfacebook] @@ -116,29 +141,44 @@ def is_suspicious(ip_address): @identity_fn.before_user_signed_in() def ipban(event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeSignInResponse | None: if is_suspicious(event.ip_address): - raise https_fn.HttpsError(code=https_fn.FunctionsErrorCode.PERMISSION_DENIED, - message="IP banned.") + raise https_fn.HttpsError( + code=https_fn.FunctionsErrorCode.PERMISSION_DENIED, message="IP banned." + ) + + # [END ipban] # [START customclaims] @identity_fn.before_user_created() def setemployeeid(event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeCreateResponse | None: - if (event.credential is not None and event.credential.claims is not None and - event.credential.provider_id == "saml.my-provider-id"): + if ( + event.credential is not None + and event.credential.claims is not None + and event.credential.provider_id == "saml.my-provider-id" + ): return identity_fn.BeforeCreateResponse( - custom_claims={"eid": event.credential.claims["employeeid"]}) + custom_claims={"eid": event.credential.claims["employeeid"]} + ) @identity_fn.before_user_signed_in() def copyclaimstosession( - event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeSignInResponse | None: - if (event.credential is not None and event.credential.claims is not None and - event.credential.provider_id == "saml.my-provider-id"): - return identity_fn.BeforeSignInResponse(session_claims={ - "role": event.credential.claims["role"], - "groups": event.credential.claims["groups"] - }) + event: identity_fn.AuthBlockingEvent, +) -> identity_fn.BeforeSignInResponse | None: + if ( + event.credential is not None + and event.credential.claims is not None + and event.credential.provider_id == "saml.my-provider-id" + ): + return identity_fn.BeforeSignInResponse( + session_claims={ + "role": event.credential.claims["role"], + "groups": event.credential.claims["groups"], + } + ) + + # [END customclaims] @@ -146,6 +186,8 @@ def copyclaimstosession( @identity_fn.before_user_signed_in() def logip(event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeSignInResponse | None: return identity_fn.BeforeSignInResponse(session_claims={"signInIpAddress": event.ip_address}) + + # [END logip] @@ -160,11 +202,14 @@ def analyze_photo_with_ml(url): # [START sanitizeprofilephoto] @identity_fn.before_user_created() def sanitizeprofilephoto( - event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeCreateResponse | None: + event: identity_fn.AuthBlockingEvent, +) -> identity_fn.BeforeCreateResponse | None: if event.data.photo_url is not None: score = analyze_photo_with_ml(event.data.photo_url) if score > THRESHOLD: return identity_fn.BeforeCreateResponse(photo_url=PLACEHOLDER_URL) + + # [END sanitizeprofilephoto] @@ -173,7 +218,7 @@ def sanitizeprofilephoto( # Block account sign in with any banned account. @identity_fn.before_user_signed_in() def checkforban(event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeSignInResponse | None: -# [END v2beforeSignInFunctionTrigger] + # [END v2beforeSignInFunctionTrigger] # [START v2readEmailData] # Email passed from the CloudEvent. email = event.data.email if event.data.email is not None else "" @@ -189,7 +234,10 @@ def checkforban(event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeSignI # Checking that the document exists for the email address. if doc.exists: # Throw an HttpsError so that Firebase Auth rejects the account sign in. - raise https_fn.HttpsError(code=https_fn.FunctionsErrorCode.INVALID_ARGUMENT, - message="Unauthorized email") + raise https_fn.HttpsError( + code=https_fn.FunctionsErrorCode.INVALID_ARGUMENT, message="Unauthorized email" + ) # [END v2bannedHttpsError] + + # [END v2CheckForBan] diff --git a/Python/quickstarts/callable-functions/functions/main.py b/Python/quickstarts/callable-functions/functions/main.py index 5e2e1e43d2..47e4cb9b25 100644 --- a/Python/quickstarts/callable-functions/functions/main.py +++ b/Python/quickstarts/callable-functions/functions/main.py @@ -15,12 +15,13 @@ import re from typing import Any +# Dependencies for writing to Realtime Database. +from firebase_admin import db, initialize_app + # [START v2imports] # Dependencies for callable functions. -from firebase_functions import https_fn, options +from firebase_functions import https_fn -# Dependencies for writing to Realtime Database. -from firebase_admin import db, initialize_app # [END v2imports] initialize_app() @@ -31,7 +32,7 @@ @https_fn.on_call() def addnumbers(req: https_fn.CallableRequest) -> Any: """Adds two numbers to each other.""" -# [END v2addFunctionTrigger] + # [END v2addFunctionTrigger] # [START v2addHttpsError] # Checking that attributes are present and are numbers. try: @@ -42,12 +43,15 @@ def addnumbers(req: https_fn.CallableRequest) -> Any: # [END v2readAddData] first_number = int(first_number_param) second_number = int(second_number_param) - except (ValueError, KeyError): + except (ValueError, KeyError) as e: # Throwing an HttpsError so that the client gets the error details. raise https_fn.HttpsError( code=https_fn.FunctionsErrorCode.INVALID_ARGUMENT, - message=('The function must be called with two arguments, "firstNumber"' - ' and "secondNumber", which must both be numbers.')) + message=( + 'The function must be called with two arguments, "firstNumber"' + ' and "secondNumber", which must both be numbers.' + ), + ) from e # [END v2addHttpsError] # [START v2returnAddData] @@ -55,9 +59,11 @@ def addnumbers(req: https_fn.CallableRequest) -> Any: "firstNumber": first_number, "secondNumber": second_number, "operator": "+", - "operationResult": first_number + second_number + "operationResult": first_number + second_number, } # [END v2returnAddData] + + # [END v2allAdd] @@ -66,31 +72,41 @@ def addnumbers(req: https_fn.CallableRequest) -> Any: def addmessage(req: https_fn.CallableRequest) -> Any: """Saves a message to the Firebase Realtime Database but sanitizes the text by removing swear words.""" -# [END v2messageFunctionTrigger] + # [END v2messageFunctionTrigger] try: # [START v2readMessageData] # Message text passed from the client. text = req.data["text"] # [END v2readMessageData] - except KeyError: + except KeyError as e: # Throwing an HttpsError so that the client gets the error details. - raise https_fn.HttpsError(code=https_fn.FunctionsErrorCode.INVALID_ARGUMENT, - message=('The function must be called with one argument, "text",' - " containing the message text to add.")) + raise https_fn.HttpsError( + code=https_fn.FunctionsErrorCode.INVALID_ARGUMENT, + message=( + 'The function must be called with one argument, "text",' + " containing the message text to add." + ), + ) from e # [START v2messageHttpsErrors] # Checking attribute. if not isinstance(text, str) or len(text) < 1: # Throwing an HttpsError so that the client gets the error details. - raise https_fn.HttpsError(code=https_fn.FunctionsErrorCode.INVALID_ARGUMENT, - message=('The function must be called with one argument, "text",' - " containing the message text to add.")) + raise https_fn.HttpsError( + code=https_fn.FunctionsErrorCode.INVALID_ARGUMENT, + message=( + 'The function must be called with one argument, "text",' + " containing the message text to add." + ), + ) # Checking that the user is authenticated. if req.auth is None: # Throwing an HttpsError so that the client gets the error details. - raise https_fn.HttpsError(code=https_fn.FunctionsErrorCode.FAILED_PRECONDITION, - message="The function must be called while authenticated.") + raise https_fn.HttpsError( + code=https_fn.FunctionsErrorCode.FAILED_PRECONDITION, + message="The function must be called while authenticated.", + ) # [END v2messageHttpsErrors] # [START v2authIntegration] @@ -105,15 +121,12 @@ def addmessage(req: https_fn.CallableRequest) -> Any: # [START v2returnMessage] # Saving the new message to the Realtime Database. sanitized_message = sanitize_text(text) # Sanitize message. - db.reference("/messages").push({ # type: ignore - "text": sanitized_message, - "author": { - "uid": uid, - "name": name, - "picture": picture, - "email": email + db.reference("/messages").push( + { # type: ignore + "text": sanitized_message, + "author": {"uid": uid, "name": name, "picture": picture, "email": email}, } - }) + ) print("New message written") # Returning the sanitized message to the client. @@ -122,9 +135,9 @@ def addmessage(req: https_fn.CallableRequest) -> Any: except Exception as e: # Re-throwing the error as an HttpsError so that the client gets # the error details. - raise https_fn.HttpsError(code=https_fn.FunctionsErrorCode.UNKNOWN, - message="Error", - details=e) + raise https_fn.HttpsError( + code=https_fn.FunctionsErrorCode.UNKNOWN, message="Error", details=e + ) from e def sanitize_text(text: str) -> str: diff --git a/Python/quickstarts/custom-events/functions/main.py b/Python/quickstarts/custom-events/functions/main.py index 39490cb128..1ea8da77a3 100644 --- a/Python/quickstarts/custom-events/functions/main.py +++ b/Python/quickstarts/custom-events/functions/main.py @@ -13,17 +13,18 @@ # limitations under the License. # [START import] -from firebase_admin import firestore, initialize_app -from firebase_functions import eventarc_fn # [END import] import google.cloud.firestore +from firebase_admin import firestore, initialize_app +from firebase_functions import eventarc_fn initialize_app() # [START imageresizedEvent] @eventarc_fn.on_custom_event_published( - event_type="firebase.extensions.storage-resize-images.v1.complete") + event_type="firebase.extensions.storage-resize-images.v1.complete" +) def onimageresized(event: eventarc_fn.CloudEvent) -> None: print("Received image resize completed event: ", event.type) @@ -36,6 +37,8 @@ def onimageresized(event: eventarc_fn.CloudEvent) -> None: collection = firestore_client.collection("images") doc = collection.document(event.subject.replace("/", "_")) # original file path doc.set(event.data) # resized images paths and sizes + + # [END imageresizedEvent] @@ -43,7 +46,8 @@ def onimageresized(event: eventarc_fn.CloudEvent) -> None: @eventarc_fn.on_custom_event_published( event_type="firebase.extensions.storage-resize-images.v1.complete", channel="locations/us-west1/channels/firebase", - region="us-west1") + region="us-west1", +) def onimageresizedwest(event: eventarc_fn.CloudEvent) -> None: print("Received image resize completed event: ", event.type) # [START_EXCLUDE] @@ -57,4 +61,6 @@ def onimageresizedwest(event: eventarc_fn.CloudEvent) -> None: doc = collection.document(event.subject.replace("/", "_")) # original file path doc.set(event.data) # resized images paths and sizes # [END_EXCLUDE] + + # [END nondefaultchannel] diff --git a/Python/quickstarts/firestore-sync-auth/functions/main.py b/Python/quickstarts/firestore-sync-auth/functions/main.py index c897f63a32..5a788d968e 100644 --- a/Python/quickstarts/firestore-sync-auth/functions/main.py +++ b/Python/quickstarts/firestore-sync-auth/functions/main.py @@ -14,15 +14,14 @@ # [START all] from firebase_functions import firestore_fn +from firebase_functions.firestore_fn import Change, DocumentSnapshot, Event # [START verifyComment] @firestore_fn.on_document_updated_with_auth_context(document="comments/{comment_id}") def verify_comment(event: Event[Change[DocumentSnapshot]]) -> None: - # Get the current and previous document values. new_value = event.data.after - prev_value = event.data.before # Get the auth context from the event user_auth_type = event.auth_type @@ -39,5 +38,7 @@ def verify_comment(event: Event[Change[DocumentSnapshot]]) -> None: # add auth medadata to the document new_value.reference.update({"created_by": user_auth_id, "verified": verified}) + + # [END verifyComment] # [END all] diff --git a/Python/quickstarts/https-time-server/functions/main.py b/Python/quickstarts/https-time-server/functions/main.py index 0c090776ec..1f65bcbccf 100644 --- a/Python/quickstarts/https-time-server/functions/main.py +++ b/Python/quickstarts/https-time-server/functions/main.py @@ -14,10 +14,11 @@ # [START additionalimports] from datetime import datetime -# [END additionalimports] +# [END additionalimports] # [START functionsimport] from firebase_functions import https_fn, options + # [END functionsimport] @@ -42,8 +43,8 @@ @https_fn.on_request(cors=options.CorsOptions(cors_origins="*", cors_methods=["get", "post"])) def date(req: https_fn.Request) -> https_fn.Response: """Get the server's local date and time.""" -# [END usingMiddleware] -# [END trigger] + # [END usingMiddleware] + # [END trigger] # [START sendError] # Forbidding PUT requests. if req.method == "PUT": @@ -68,4 +69,6 @@ def date(req: https_fn.Request) -> https_fn.Response: print(f"Sending Formatted date: {formatted_date}") return https_fn.Response(formatted_date) # [END sendResponse] + + # [END all] diff --git a/Python/quickstarts/monitor-cloud-logging/functions/main.py b/Python/quickstarts/monitor-cloud-logging/functions/main.py index dff4f87d34..dae13bc867 100644 --- a/Python/quickstarts/monitor-cloud-logging/functions/main.py +++ b/Python/quickstarts/monitor-cloud-logging/functions/main.py @@ -1,11 +1,11 @@ -from firebase_functions import https_fn -from firebase_functions.alerts import crashlytics_fn -from firebase_admin import initialize_app, firestore import datetime -import sys + +from firebase_admin import firestore, initialize_app # [START loggerImport] -from firebase_functions import logger +from firebase_functions import https_fn, logger +from firebase_functions.alerts import crashlytics_fn + # [END loggerImport] app = initialize_app() @@ -18,6 +18,8 @@ def hello_world(req: https_fn.Request) -> https_fn.Response: logger.log("Hello logs!") return https_fn.Response("Hello from Firebase!") + + # [END helloLogs] @@ -26,8 +28,12 @@ def hello_world(req: https_fn.Request) -> https_fn.Response: def get_inspirational_quote(req: https_fn.Request) -> https_fn.Response: firestore_client = firestore.client() today = datetime.date.today() - quote_of_the_month_ref = (firestore_client.collection("quotes").doc(str( - today.year)).collection("months").doc(str(today.month))) + quote_of_the_month_ref = ( + firestore_client.collection("quotes") + .doc(str(today.year)) + .collection("months") + .doc(str(today.month)) + ) default_quote = "Python has been an important part of Google since the beginning, and remains so as the system grows and evolves." @@ -53,8 +59,7 @@ def get_inspirational_quote(req: https_fn.Request) -> https_fn.Response: date_requested=today.strftime("%Y-%m-%d"), ) quote = default_quote - except: - e = sys.exc_info()[0] + except Exception as e: # [START logError] # Attach an error object as the second argument logger.error("Unable to read quote from Firestore, sending default instead", error=e) @@ -64,6 +69,8 @@ def get_inspirational_quote(req: https_fn.Request) -> https_fn.Response: # Attach relevant structured data to any log logger.info("Sending a quote!", quote=quote) return https_fn.Response("Hello from Firebase!") + + # [END logsKitchenSink] @@ -77,4 +84,6 @@ def app_has_regression(alert: crashlytics_fn.CrashlyticsRegressionAlertEvent) -> last_occurred=alert.data.payload.resolve_time, ) print(alert) + + # [END customLogWrite] diff --git a/Python/quickstarts/pubsub-helloworld/functions/main.py b/Python/quickstarts/pubsub-helloworld/functions/main.py index 297bd7195b..a6c68cd8f1 100644 --- a/Python/quickstarts/pubsub-helloworld/functions/main.py +++ b/Python/quickstarts/pubsub-helloworld/functions/main.py @@ -16,6 +16,7 @@ # [START import] from firebase_functions import pubsub_fn + # [END import] @@ -24,7 +25,7 @@ @pubsub_fn.on_message_published(topic="topic-name") def hellopubsub(event: pubsub_fn.CloudEvent[pubsub_fn.MessagePublishedData]) -> None: """Log a message using data published to a Pub/Sub topic.""" -# [END trigger] + # [END trigger] # [START readBase64] # Decode the PubSub message body. message_body = base64.b64decode(event.data.message.data) @@ -32,6 +33,8 @@ def hellopubsub(event: pubsub_fn.CloudEvent[pubsub_fn.MessagePublishedData]) -> # Print the message. print(f"Hello, {message_body.decode('utf-8') if message_body else 'World'}") + + # [END helloWorld] diff --git a/Python/quickstarts/uppercase-firestore/functions/main.py b/Python/quickstarts/uppercase-firestore/functions/main.py index 765c8363c7..48de842530 100644 --- a/Python/quickstarts/uppercase-firestore/functions/main.py +++ b/Python/quickstarts/uppercase-firestore/functions/main.py @@ -15,11 +15,11 @@ # [START all] # [START import] # The Cloud Functions for Firebase SDK to create Cloud Functions and set up triggers. -from firebase_functions import firestore_fn, https_fn +import google.cloud.firestore # The Firebase Admin SDK to access Cloud Firestore. -from firebase_admin import initialize_app, firestore -import google.cloud.firestore +from firebase_admin import firestore, initialize_app +from firebase_functions import firestore_fn, https_fn app = initialize_app() # [END import] @@ -31,7 +31,7 @@ def addmessage(req: https_fn.Request) -> https_fn.Response: """Take the text parameter passed to this HTTP endpoint and insert it into a new document in the messages collection.""" -# [END addMessageTrigger] + # [END addMessageTrigger] # Grab the text parameter. original = req.args.get("text") if original is None: @@ -46,6 +46,8 @@ def addmessage(req: https_fn.Request) -> https_fn.Response: # Send back a message that we've successfully written the message return https_fn.Response(f"Message with ID {doc_ref.id} added.") # [END adminSdkPush] + + # [END addMessage] @@ -69,6 +71,8 @@ def makeuppercase(event: firestore_fn.Event[firestore_fn.DocumentSnapshot | None print(f"Uppercasing {event.params['pushId']}: {original}") upper = original.upper() event.data.reference.update({"uppercase": upper}) + + # [END makeUppercase] # [END all] @@ -76,7 +80,7 @@ def makeuppercase(event: firestore_fn.Event[firestore_fn.DocumentSnapshot | None # [START makeUppercase2] @firestore_fn.on_document_written(document="messages/{pushId}") def makeuppercase2( - event: firestore_fn.Event[firestore_fn.Change[firestore_fn.DocumentSnapshot | None]] + event: firestore_fn.Event[firestore_fn.Change[firestore_fn.DocumentSnapshot | None]], ) -> None: """Listens for new documents to be added to /messages. If the document has an "original" field, creates an "uppercase" field containg the contents of @@ -101,4 +105,6 @@ def makeuppercase2( print(f"Uppercasing {event.params['pushId']}: {original}") upper = original.upper() event.data.after.reference.update({"uppercase": upper}) + + # [END makeUppercase2] diff --git a/Python/quickstarts/uppercase-rtdb/functions/main.py b/Python/quickstarts/uppercase-rtdb/functions/main.py index 538a5f4f98..44f6d847d6 100644 --- a/Python/quickstarts/uppercase-rtdb/functions/main.py +++ b/Python/quickstarts/uppercase-rtdb/functions/main.py @@ -16,13 +16,13 @@ from typing import Any from urllib import parse as urllib_parse +# The Firebase Admin SDK to access the Firebase Realtime Database. +from firebase_admin import db, initialize_app + # [START import] # The Cloud Functions for Firebase SDK to create Cloud Functions and set up triggers. from firebase_functions import db_fn, https_fn -# The Firebase Admin SDK to access the Firebase Realtime Database. -from firebase_admin import initialize_app, db - app = initialize_app() # [END import] @@ -33,7 +33,7 @@ def addmessage(req: https_fn.Request) -> https_fn.Response: """Take the text parameter passed to this HTTP endpoint and insert it into the Realtime Database under the path /messages/{pushId}/original""" -# [END addMessageTrigger] + # [END addMessageTrigger] # Grab the text parameter. original = req.args.get("text") if original is None: @@ -44,12 +44,16 @@ def addmessage(req: https_fn.Request) -> https_fn.Response: # Redirect with 303 SEE OTHER to the URL of the pushed object. scheme, location, path, query, fragment = ( - b.decode() for b in urllib_parse.urlsplit(app.options.get("databaseURL"))) + b.decode() for b in urllib_parse.urlsplit(app.options.get("databaseURL")) + ) path = f"{ref.path}.json" return https_fn.Response( status=303, - headers={"Location": urllib_parse.urlunsplit((scheme, location, path, query, fragment))}) + headers={"Location": urllib_parse.urlunsplit((scheme, location, path, query, fragment))}, + ) # [END adminSdkPush] + + # [END addMessage] @@ -74,6 +78,8 @@ def makeuppercase(event: db_fn.Event[Any]) -> None: print("Message can't be root node.") return parent.child("uppercase").set(upper) + + # [END makeUppercase] @@ -106,5 +112,7 @@ def makeuppercase2(event: db_fn.Event[db_fn.Change]) -> None: print("Message can't be root node.") return parent.child("uppercase").set(upper) + + # [END makeUppercase2] # [END all] diff --git a/Python/remote-config-diff/functions/main.py b/Python/remote-config-diff/functions/main.py index 907dc805b0..9088dfb06a 100644 --- a/Python/remote-config-diff/functions/main.py +++ b/Python/remote-config-diff/functions/main.py @@ -15,15 +15,15 @@ # [START all] # [START import] # The Cloud Functions for Firebase SDK to set up triggers and logging. -from firebase_functions import remote_config_fn - # The Firebase Admin SDK to obtain access tokens. +import deepdiff import firebase_admin +import requests +from firebase_functions import remote_config_fn app = firebase_admin.initialize_app() -import deepdiff -import requests + # [END import] @@ -39,17 +39,24 @@ def showconfigdiff(event: remote_config_fn.CloudEvent[remote_config_fn.ConfigUpd current_version = int(event.data.version_number) # Figure out the differences between templates - remote_config_api = ("https://firebaseremoteconfig.googleapis.com/v1/" - f"projects/{app.project_id}/remoteConfig") - current_template = requests.get(remote_config_api, - params={"versionNumber": current_version}, - headers={"Authorization": f"Bearer {access_token}"}) - previous_template = requests.get(remote_config_api, - params={"versionNumber": current_version - 1}, - headers={"Authorization": f"Bearer {access_token}"}) + remote_config_api = ( + f"https://firebaseremoteconfig.googleapis.com/v1/projects/{app.project_id}/remoteConfig" + ) + current_template = requests.get( + remote_config_api, + params={"versionNumber": current_version}, + headers={"Authorization": f"Bearer {access_token}"}, + ) + previous_template = requests.get( + remote_config_api, + params={"versionNumber": current_version - 1}, + headers={"Authorization": f"Bearer {access_token}"}, + ) diff = deepdiff.DeepDiff(previous_template, current_template) # Log the difference print(diff.pretty()) + + # [END showconfigdiff] # [END all] diff --git a/Python/requirements.txt b/Python/requirements.txt index a26557a303..1751ee5e5c 100644 --- a/Python/requirements.txt +++ b/Python/requirements.txt @@ -1 +1,2 @@ -yapf~=0.40.2 +ruff +uv diff --git a/Python/taskqueues-backup-images/functions/main.py b/Python/taskqueues-backup-images/functions/main.py index dfd06b270e..ba5c7458da 100644 --- a/Python/taskqueues-backup-images/functions/main.py +++ b/Python/taskqueues-backup-images/functions/main.py @@ -14,19 +14,19 @@ # [START v2imports] # Dependencies for task queue functions. -from google.cloud import tasks_v2 -import requests -from firebase_functions.options import RetryConfig, RateLimits, SupportedRegion +import pathlib # Dependencies for image backup. from datetime import datetime, timedelta -import json -import pathlib from urllib.parse import urlparse -from firebase_admin import initialize_app, storage, functions -from firebase_functions import https_fn, tasks_fn, params + import google.auth +import requests +from firebase_admin import functions, initialize_app, storage +from firebase_functions import https_fn, params, tasks_fn +from firebase_functions.options import RateLimits, RetryConfig, SupportedRegion from google.auth.transport.requests import AuthorizedSession + # [END v2imports] app = initialize_app() @@ -35,28 +35,31 @@ BACKUP_COUNT = params.IntParam("BACKUP_COUNT", default=100).value HOURLY_BATCH_SIZE = params.IntParam("HOURLY_BATCH_SIZE", default=600).value BACKUP_BUCKET = params.StringParam( - "BACKUP_BUCKET", input=params.ResourceInput(type=params.ResourceType.STORAGE_BUCKET)).value + "BACKUP_BUCKET", input=params.ResourceInput(type=params.ResourceType.STORAGE_BUCKET) +).value NASA_API_KEY = params.StringParam("NASA_API_KEY").value # [START v2TaskFunctionSetup] -@tasks_fn.on_task_dispatched(retry_config=RetryConfig(max_attempts=5, min_backoff_seconds=60), - rate_limits=RateLimits(max_concurrent_dispatches=10)) +@tasks_fn.on_task_dispatched( + retry_config=RetryConfig(max_attempts=5, min_backoff_seconds=60), + rate_limits=RateLimits(max_concurrent_dispatches=10), +) def backupapod(req: tasks_fn.CallableRequest) -> str: """Grabs Astronomy Photo of the Day (APOD) using NASA's API.""" -# [END v2TaskFunctionSetup] + # [END v2TaskFunctionSetup] try: date = req.data["date"] - except KeyError: - raise https_fn.HttpsError(code=https_fn.FunctionsErrorCode.INVALID_ARGUMENT, - message="Invalid payload. Must include date.") + except KeyError as e: + raise https_fn.HttpsError( + code=https_fn.FunctionsErrorCode.INVALID_ARGUMENT, + message="Invalid payload. Must include date.", + ) from e print(f"Requesting data from APOD API for date {date}") - api_resp = requests.get(url="https://api.nasa.gov/planetary/apod", - params={ - "date": date, - "api_key": NASA_API_KEY - }) + api_resp = requests.get( + url="https://api.nasa.gov/planetary/apod", params={"date": date, "api_key": NASA_API_KEY} + ) if not api_resp.ok: print(f"Request to NASA APOD API failed with reponse {api_resp.status_code}") match api_resp.status_code: @@ -64,11 +67,14 @@ def backupapod(req: tasks_fn.CallableRequest) -> str: print("No APOD today.") return "No APOD today." case 500: - raise https_fn.HttpsError(code=https_fn.FunctionsErrorCode.UNAVAILABLE, - message="APOD API temporarily not available.") + raise https_fn.HttpsError( + code=https_fn.FunctionsErrorCode.UNAVAILABLE, + message="APOD API temporarily not available.", + ) case _: - raise https_fn.HttpsError(code=https_fn.FunctionsErrorCode.INTERNAL, - message="Uh-oh. Something broke.") + raise https_fn.HttpsError( + code=https_fn.FunctionsErrorCode.INTERNAL, message="Uh-oh. Something broke." + ) apod = api_resp.json() pic_url = apod["hdurl"] @@ -84,9 +90,10 @@ def backupapod(req: tasks_fn.CallableRequest) -> str: pic_blob = bucket.blob(f"apod/{date}{ext}") try: pic_blob.upload_from_string(pic_resp.content, content_type=pic_type) - except: - raise https_fn.HttpsError(code=https_fn.FunctionsErrorCode.INTERNAL, - message="Uh-oh. Something broke.") + except Exception as e: + raise https_fn.HttpsError( + code=https_fn.FunctionsErrorCode.INTERNAL, message="Uh-oh. Something broke." + ) from e print(f"Saved {pic_url}") return f"Saved {pic_url}" @@ -110,11 +117,15 @@ def enqueuebackuptasks(_: https_fn.Request) -> https_fn.Response: backup_date = BACKUP_START_DATE + timedelta(days=i) body = {"data": {"date": backup_date.isoformat()[:10]}} - task_options = functions.TaskOptions(schedule_time=schedule_time, - dispatch_deadline_seconds=dispatch_deadline_seconds, - uri=target_uri) + task_options = functions.TaskOptions( + schedule_time=schedule_time, + dispatch_deadline_seconds=dispatch_deadline_seconds, + uri=target_uri, + ) task_queue.enqueue(body, task_options) return https_fn.Response(status=200, response=f"Enqueued {BACKUP_COUNT} tasks") + + # [END v2EnqueueTasks] @@ -129,12 +140,17 @@ def get_function_url(name: str, location: str = SupportedRegion.US_CENTRAL1) -> Returns: 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 = 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"] return function_url + + # [END v2GetFunctionUri] diff --git a/Python/testlab-to-slack/functions/main.py b/Python/testlab-to-slack/functions/main.py index 42b935d232..9bbda11565 100644 --- a/Python/testlab-to-slack/functions/main.py +++ b/Python/testlab-to-slack/functions/main.py @@ -15,10 +15,10 @@ # [START all] # [START import] # The Cloud Functions for Firebase SDK to set up triggers and logging. -from firebase_functions import test_lab_fn, params - # The requests library to send web requests to Slack. import requests +from firebase_functions import params, test_lab_fn + # [END import] # [START postToSlack] @@ -27,24 +27,18 @@ def post_to_slack(title: str, details: str) -> requests.Response: """Posts a message to Slack via a Webhook.""" - return requests.post(SLACK_WEBHOOK_URL.value, - json={ - "blocks": [{ - "type": "section", - "text": { - "type": "mrkdwn", - "text": title - } - }, { - "type": "divider" - }, { - "type": "section", - "text": { - "type": "mrkdwn", - "text": details - } - }] - }) + return requests.post( + SLACK_WEBHOOK_URL.value, + json={ + "blocks": [ + {"type": "section", "text": {"type": "mrkdwn", "text": title}}, + {"type": "divider"}, + {"type": "section", "text": {"type": "mrkdwn", "text": details}}, + ] + }, + ) + + # [END postToSlack] @@ -60,16 +54,19 @@ def slackmoji(status: test_lab_fn.TestState | test_lab_fn.OutcomeSummary) -> str test_lab_fn.TestState.PENDING: ":soon:", test_lab_fn.TestState.FINISHED: ":white_check_mark:", test_lab_fn.TestState.ERROR: ":red_circle:", - test_lab_fn.TestState.INVALID: ":large_orange_diamond:" + test_lab_fn.TestState.INVALID: ":large_orange_diamond:", } return status_slackmoji[status] if status in status_slackmoji else "" + + # [END getSlackmoji] # [START posttestresultstoslack] @test_lab_fn.on_test_matrix_completed(secrets=["SLACK_WEBHOOK_URL"]) def posttestresultstoslack( - event: test_lab_fn.CloudEvent[test_lab_fn.TestMatrixCompletedData]) -> None: + event: test_lab_fn.CloudEvent[test_lab_fn.TestMatrixCompletedData], +) -> None: """Posts a test matrix result to Slack.""" # Obtain Test Matrix properties from the CloudEvent @@ -81,13 +78,17 @@ def posttestresultstoslack( title = f"{slackmoji(state)} {slackmoji(outcome_summary)} {test_matrix_id}" # Create the details of the message - details = (f"Status: *{state}* {slackmoji(state)}\n" - f"Outcome: *{outcome_summary}* {slackmoji(outcome_summary)}") + details = ( + f"Status: *{state}* {slackmoji(state)}\n" + f"Outcome: *{outcome_summary}* {slackmoji(outcome_summary)}" + ) # Post the message to Slack response = post_to_slack(title, details) # Log the response print(response.status_code, response.text) + + # [END posttestresultstoslack] # [END all] diff --git a/Python/thumbnails/functions/main.py b/Python/thumbnails/functions/main.py index 957adde4b7..9c0c796344 100644 --- a/Python/thumbnails/functions/main.py +++ b/Python/thumbnails/functions/main.py @@ -17,16 +17,15 @@ import io import pathlib +from firebase_admin import initialize_app, storage +from firebase_functions import storage_fn from PIL import Image -from firebase_admin import initialize_app - initialize_app() -from firebase_admin import storage -# [END storageAdditionalImports] + +# [END storageAdditionalImports] # [START storageSDKImport] -from firebase_functions import storage_fn # [END storageSDKImport] # [END storageImports] @@ -37,7 +36,7 @@ def generatethumbnail(event: storage_fn.CloudEvent[storage_fn.StorageObjectData]): """When an image is uploaded in the Storage bucket, generate a thumbnail automatically using Pillow.""" -# [END storageGenerateThumbnailTrigger] + # [END storageGenerateThumbnailTrigger] # [START storageEventAttributes] bucket_name = event.data.bucket @@ -71,4 +70,6 @@ def generatethumbnail(event: storage_fn.CloudEvent[storage_fn.StorageObjectData] thumbnail_blob = bucket.blob(str(thumbnail_path)) thumbnail_blob.upload_from_string(thumbnail_io.getvalue(), content_type="image/png") # [END storageThumbnailGeneration] + + # [END storageGenerateThumbnail]