diff --git a/auth0_to_hubspot/program.py b/auth0_to_hubspot/program.py index 7ebc4eee..f8920a59 100644 --- a/auth0_to_hubspot/program.py +++ b/auth0_to_hubspot/program.py @@ -1,6 +1,6 @@ """This program adds new Auth0 users to HubSpot as contacts.""" -from datetime import UTC, datetime +from datetime import datetime, UTC import os from autokitteh.auth0 import auth0_client @@ -17,7 +17,8 @@ def check_for_new_users(event): """Workflow entrypoint. - Looks up new Auth0 users in the last `HOURS` hours and adds them to HubSpot as contacts. + Looks up new Auth0 users in the last `HOURS` hours, + and adds them to HubSpot as contacts. """ start, end = _get_time_range(LOOKUP_HOURS) query = f"created_at:[{start} TO {end}]" diff --git a/aws_health_to_slack/program.py b/aws_health_to_slack/program.py index 0d6fe4d6..e797950d 100644 --- a/aws_health_to_slack/program.py +++ b/aws_health_to_slack/program.py @@ -5,7 +5,7 @@ https://aws.amazon.com/blogs/mt/tag/aws-health-api/ """ -from datetime import UTC, datetime, timedelta +from datetime import datetime, timedelta, UTC import json import os import re @@ -67,11 +67,11 @@ def _aws_health_events() -> list[dict]: resp = aws.describe_events(filter=filter) events = resp.get("events", []) - nextToken = resp.get("nextToken") - while nextToken: - resp = aws.describe_events(filter=filter, nextToken=nextToken) + next_token = resp.get("nextToken") + while next_token: + resp = aws.describe_events(filter=filter, nextToken=next_token) events += resp.get("events", []) - nextToken = resp.get("nextToken") + next_token = resp.get("nextToken") return events @@ -97,11 +97,11 @@ def _affected_aws_entities(events: list[dict]) -> list[dict]: resp = aws.describe_affected_entities(filter=filter) entities = resp.get("entities", []) - nextToken = resp.get("nextToken") - while nextToken: - resp = aws.describe_affected_entities(filter=filter, nextToken=nextToken) + next_token = resp.get("nextToken") + while next_token: + resp = aws.describe_affected_entities(filter=filter, nextToken=next_token) entities += resp.get("entities", []) - nextToken = resp.get("nextToken") + next_token = resp.get("nextToken") return entities except Exception as e: diff --git a/break_glass/program.py b/break_glass/program.py index 50864f35..5661a2cc 100644 --- a/break_glass/program.py +++ b/break_glass/program.py @@ -12,11 +12,12 @@ and justification for the request. 4. AutoKitteh sends a notification to the SRE (Site Reliability Engineering) team with an approve/deny message, including the details of the request. - 5. The SRE team reviews the request and makes a decision to approve or deny the request. + 5. The SRE team reviews the request and makes a decision to approve or deny the + request. 6. AutoKitteh sends a message to the developer with the decision, notifying them whether the request was approved or denied. -The program integrates with Jira to verify ticket existence and ensure that the requester +The program integrates with Jira to verify ticket existence and ensure the requester is the assignee of the ticket. It also uses Slack for communication and notifications throughout the process. """ @@ -25,7 +26,7 @@ from pathlib import Path import autokitteh -from autokitteh.atlassian import jira_client, get_base_url +from autokitteh.atlassian import get_base_url, jira_client from autokitteh.slack import slack_client from requests.exceptions import HTTPError diff --git a/categorize_emails/program.py b/categorize_emails/program.py index 1225e7c1..b3ffcc3c 100644 --- a/categorize_emails/program.py +++ b/categorize_emails/program.py @@ -1,30 +1,31 @@ -""" -This program demonstrates a real-life workflow that integrates Gmail, ChatGPT, and Slack. +"""A real-life workflow that integrates Gmail, ChatGPT, and Slack: -Workflow: 1. Trigger: Detect a new email in Gmail. -2. Categorize: Use ChatGPT to read and categorize the email (e.g., technical work, marketing, sales). +2. Categorize: Use ChatGPT to read and categorize the email + (e.g., technical work, marketing, sales). 3. Notify: Send a message to the corresponding Slack channel based on the category. 4. Label: Add a label to the email in Gmail. """ import base64 -from datetime import datetime, timezone +from datetime import datetime, UTC import os import time import autokitteh -from autokitteh import google, openai, slack +from autokitteh.google import gmail_client +from autokitteh.openai import openai_client +from autokitteh.slack import slack_client POLL_INTERVAL = float(os.getenv("POLL_INTERVAL")) SLACK_CHANNELS = ["demos", "engineering", "ui"] -gmail_client = google.gmail_client("my_gmail").users() -slack_client = slack.slack_client("my_slack") +gmail = gmail_client("my_gmail").users() +slack = slack_client("my_slack") processed_message_ids = set() -start_time = datetime.now(timezone.utc).timestamp() +start_time = datetime.now(UTC).timestamp() def on_http_get(event): @@ -46,7 +47,7 @@ def _poll_inbox(): def _process_email(message_id: str, start_time: datetime): - message = gmail_client.messages().get(userId="me", id=message_id).execute() + message = gmail.messages().get(userId="me", id=message_id).execute() email_timestamp = float(message["internalDate"]) / 1000 if email_timestamp < start_time: @@ -64,7 +65,7 @@ def _process_email(message_id: str, start_time: datetime): print("Could not categorize email.") return - slack_client.chat_postMessage(channel=channel, text=email_content) + slack.chat_postMessage(channel=channel, text=email_content) # Add label to email label_id = _get_label_id(channel) or _create_label(channel) @@ -72,7 +73,7 @@ def _process_email(message_id: str, start_time: datetime): return body = {"addLabelIds": [label_id]} - gmail_client.messages().modify(userId="me", id=message_id, body=body).execute() + gmail.messages().modify(userId="me", id=message_id, body=body).execute() def _parse_email(message: dict): @@ -95,12 +96,12 @@ def _create_label(label_name: str) -> str: "messageListVisibility": "show", "name": label_name, } - created_label = gmail_client.labels().create(userId="me", body=label).execute() + created_label = gmail.labels().create(userId="me", body=label).execute() return created_label.get("id", None) def _get_label_id(label_name: str) -> str: - labels_response = gmail_client.labels().list(userId="me").execute() + labels_response = gmail.labels().list(userId="me").execute() labels = labels_response.get("labels", []) for label in labels: if label["name"] == label_name: @@ -110,7 +111,7 @@ def _get_label_id(label_name: str) -> str: def get_new_inbox_messages(): query = "in:inbox -in:drafts" - return gmail_client.messages().list(userId="me", q=query, maxResults=10).execute() + return gmail.messages().list(userId="me", q=query, maxResults=10).execute() @autokitteh.activity @@ -121,19 +122,19 @@ def _categorize_email(email_content: str) -> str: The name of the Slack channel to send a message to as a string. If the channel is not in the provided list, returns None. """ - client = openai.openai_client("my_chatgpt") + client = openai_client("my_chatgpt") response = client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": "You are a helpful assistant."}, { "role": "user", - "content": f"""Categorize the following email based on its - topic and suggest a channel to post it in from the - provided list. The output should be one of the provided - channels and nothing else. - Email Content: {email_content} Channels: {SLACK_CHANNELS} - Output example: {SLACK_CHANNELS[0]}""", + "content": ( + "Categorize the following email based on its topic and suggest a " + "channel to post it in from the provided list. The output should " + "be one of the channels in {SLACK_CHANNELS} and nothing else, " + "for example: {SLACK_CHANNELS[0]}\n\nEmail content: {email_content}" + ), }, ], ) diff --git a/data_pipeline/pipeline.py b/data_pipeline/pipeline.py index afb56f1c..3c9b297a 100644 --- a/data_pipeline/pipeline.py +++ b/data_pipeline/pipeline.py @@ -1,11 +1,11 @@ -"""Parse GPX files when uploaded to an S3 bucket, and insert the data into a SQLite database.""" +"""Parse GPX files when uploaded to an S3 bucket, and insert into a SQLite database.""" from contextlib import closing from io import BytesIO import json import os import sqlite3 -import xml.etree.ElementTree as xml +import xml.etree.ElementTree as Xml import autokitteh from autokitteh.aws import boto3_client @@ -64,7 +64,7 @@ def insert_records(db_dsn, records): @autokitteh.activity def parse_gpx(track_id, data): io = BytesIO(data) - root = xml.parse(io).getroot() + root = Xml.parse(io).getroot() return [ { "track_id": track_id, diff --git a/discord_to_spreadsheet/program.py b/discord_to_spreadsheet/program.py index d13377fa..cf5e05d6 100644 --- a/discord_to_spreadsheet/program.py +++ b/discord_to_spreadsheet/program.py @@ -1,6 +1,4 @@ -"""This program logs Discord message events and the author's username -into a Google Sheets document. -""" +"""Log Discord message events and the author's username into a Google Sheet.""" import os @@ -15,7 +13,6 @@ def on_discord_message(event): values = [[event.data["author"]["username"], event.data["content"]]] - sheet.append( spreadsheetId=SPREADSHEET_ID, range=RANGE_NAME, diff --git a/github_actions/program.py b/github_actions/program.py index eef69184..22989a16 100644 --- a/github_actions/program.py +++ b/github_actions/program.py @@ -1,18 +1,25 @@ -"""This program provides functions to handle GitHub workflows that interact across multiple repositories. -It defines triggers that automatically start workflows in specific repositories when workflows in other -repositories are completed. +"""Handle GitHub workflows that interact across multiple repositories. + +This program defines triggers that automatically start workflows in specific +repositories when workflows in other repositories are completed. This program supports several types of triggers: -1. Cross-repo trigger: Initiates a workflow in repository B when a workflow in repository A completes. -2. Fan-out trigger: Initiates workflows in repositories B and C upon the completion of a workflow in repository A. -3. OR trigger: Initiates a workflow in repository C if a workflow in either repository A or B completes. -4. Fan-in trigger: Initiates a workflow in repository C only when workflows in both repositories A and B complete. +1. Cross-repo trigger: Initiates a workflow in repository B when a workflow in + repository A completes. +2. Fan-out trigger: Initiates workflows in repositories B and C upon the + completion of a workflow in repository A. +3. OR trigger: Initiates a workflow in repository C if a workflow in either + repository A or B completes. +4. Fan-in trigger: Initiates a workflow in repository C only when workflows in + both repositories A and B complete. """ import os + import autokitteh from autokitteh.github import github_client + REPO_A = os.getenv("REPO_A") REPO_B = os.getenv("REPO_B") REPO_C = os.getenv("REPO_C") @@ -24,7 +31,7 @@ def on_cross_repo(_): - """Cross-repo trigger (completion of workflow in repo A triggers workflow in repo B).""" + """Cross-repo trigger (workflow completion in repo A triggers workflow in B).""" subscribe_to_event([REPO_A]) trigger_workflow(REPO_B, B_WORKFLOW_FILE) diff --git a/google_cal_to_asana/program.py b/google_cal_to_asana/program.py index 13ac4e86..ab5294aa 100644 --- a/google_cal_to_asana/program.py +++ b/google_cal_to_asana/program.py @@ -1,4 +1,4 @@ -"""This program creates a new Asana task when a new event is created in Google Calendar.""" +"""Create a new Asana task when a new event is created in Google Calendar.""" import os @@ -6,6 +6,7 @@ from asana.rest import ApiException from autokitteh.asana import asana_client + ASANA_PROJECT_GID = os.getenv("ASANA_PROJECT_GID") api_client = asana_client("asana_conn") diff --git a/hackernews/program.py b/hackernews/program.py index f5512c61..e50fbb67 100644 --- a/hackernews/program.py +++ b/hackernews/program.py @@ -1,11 +1,11 @@ -"""Monitor Hacker News for new articles on a specific topic, and post updates to a Slack channel.""" +"""Monitor Hacker News for new articles on a specific topic, post updates to Slack.""" import os -import requests import time import urllib.parse from autokitteh.slack import slack_client +import requests API_URL = "http://hn.algolia.com/api/v1/search_by_date?tags=story&page=0&query=" @@ -16,6 +16,7 @@ def on_slack_command(event): """Workflow's entry-point. + Extracts a topic from a Slack command, monitors for new articles, and posts updates to `SLACK_CHANNEL`. """ @@ -27,8 +28,8 @@ def on_slack_command(event): current_articles = set() fetch_articles(topic, current_articles) - # NOTE: For low-traffic topics, it might take a while for new articles to be published, - # so users may experience delays in receiving notifications. + # NOTE: For low-traffic topics, it might take a while for new articles to + # be published, so users may experience delays in receiving notifications. while True: all_articles = set(current_articles) fetch_articles(topic, all_articles) diff --git a/jira_google_calendar/assignee_from_schedule/program.py b/jira_google_calendar/assignee_from_schedule/program.py index 3ced8a38..4b13ebfa 100644 --- a/jira_google_calendar/assignee_from_schedule/program.py +++ b/jira_google_calendar/assignee_from_schedule/program.py @@ -1,5 +1,4 @@ -""" -This program assigns Atlassian Jira issues based on a shared Google Calendar. +"""This program assigns Atlassian Jira issues based on a shared Google Calendar. The shared Google Calendar defines a 27/4 on-call rotation. How to create it: https://support.google.com/calendar/answer/37095 @@ -9,7 +8,7 @@ - Description: their Atlassian account ID """ -from datetime import UTC, datetime, timedelta +from datetime import datetime, timedelta, UTC import os import autokitteh diff --git a/jira_google_calendar/deadline_to_event/program.py b/jira_google_calendar/deadline_to_event/program.py index f9ac6a60..52688d7c 100644 --- a/jira_google_calendar/deadline_to_event/program.py +++ b/jira_google_calendar/deadline_to_event/program.py @@ -1,5 +1,4 @@ -""" -This program receives Jira events and creates Google Calendar events. +"""This program receives Jira events and creates Google Calendar events. Scenario: Initiating a procedure that requires collaboration and coordination, diff --git a/purrr/data_helper.py b/purrr/data_helper.py index 94309417..2587bb3c 100644 --- a/purrr/data_helper.py +++ b/purrr/data_helper.py @@ -20,15 +20,15 @@ def cache_github_reference(slack_user_id: str, github_ref: str) -> None: """Map a Slack user ID to a GitHub user reference/name, for a day. - This helps reduce the amount of GitHub and Slack lookup API calls, to avoid throttling. + This helps reduce the amount of lookup API calls, to avoid throttling. """ return # TODO: Implement this function. def cached_github_reference(slack_user_id: str) -> str: - """Return the GitHub user reference/name mapped to a Slack user ID, or "" if not cached yet. + """Return the GitHub user reference/name mapped to a Slack user ID, or "". - This helps reduce the amount of GitHub and Slack lookup API calls, to avoid throttling. + This helps reduce the amount of lookup API calls, to avoid throttling. """ return "" # TODO: Implement this function. diff --git a/purrr/github_pr.py b/purrr/github_pr.py index 1cf1f2ed..9c1d19b2 100644 --- a/purrr/github_pr.py +++ b/purrr/github_pr.py @@ -59,7 +59,8 @@ def _parse_github_pr_event(data) -> None: case "unassigned": _on_pr_unassigned(data) - # The title or body of a pull request was edited, or the base branch was changed. + # The title or body of a pull request was edited, + # or the base branch was changed. case "edited": _on_pr_edited(data) # A pull request's head branch was updated. diff --git a/purrr/markdown_test.py b/purrr/markdown_test.py index a25ecb47..e16aca97 100644 --- a/purrr/markdown_test.py +++ b/purrr/markdown_test.py @@ -1,13 +1,15 @@ +"""Unit tests for the "markdown" module.""" + import collections import unittest from unittest.mock import MagicMock -import users import markdown +import users class MarkdownGithubToSlackTest(unittest.TestCase): - """Unit tests for the "markdown" module's "github_to_slack" function.""" + """Unit tests for the "github_to_slack" function.""" def test_trivial(self): self.assertEqual(markdown.github_to_slack("", ""), "") @@ -127,7 +129,7 @@ def test_html_comments(self): class MarkdownSlackToGithubTest(unittest.TestCase): - """Unit tests for the "markdown" module's "slack_to_github" function.""" + """Unit tests for the "slack_to_github" function.""" def test_trivial(self): self.assertEqual(markdown.slack_to_github(""), "") @@ -204,7 +206,7 @@ def test_channel(self): class MarkdownSlackToGithubUserMentionsTest(unittest.TestCase): - """Unit tests for user mentions in the "markdown" module's "slack_to_github" function.""" + """Unit tests for user mentions in the "slack_to_github" function.""" def setUp(self): super().setUp() diff --git a/purrr/slack_channel.py b/purrr/slack_channel.py index 80b15d4e..3690c9ab 100644 --- a/purrr/slack_channel.py +++ b/purrr/slack_channel.py @@ -137,8 +137,8 @@ def add_users(channel_id: str, github_users: list[str]) -> None: error = f"Failed to add {len(slack_users)} Slack user(s) to channel " error += f"<#{channel_id}>: `{e.response['error']}`" - for e in e.response.get("errors", []): - error += f"\n- <@{e.user}> - `{e.error}`" + for err in e.response.get("errors", []): + error += f"\n- <@{err.user}> - `{err.error}`" debug.log(error) diff --git a/purrr/slack_cmd.py b/purrr/slack_cmd.py index 694aa1d1..4f03139f 100644 --- a/purrr/slack_cmd.py +++ b/purrr/slack_cmd.py @@ -1,6 +1,7 @@ """Handler for Slack slash-command events.""" import collections + import data_helper import slack_helper @@ -113,9 +114,9 @@ def _status(data, args: list[str]): """PR status command handler.""" # TODO: If the Slack channel belongs to a PR, the arg is optional. if len(args) != 2: - msg = "when called outside of a PR channel, this command requires exactly 1 argument - " - msg += "an ID of a GitHub PR (`//`), or the PR's full URL" - _error(data, args[0], msg) + msg = "when called outside of a PR channel, this command requires exactly " + msg += "1 argument - an ID of a GitHub PR (`//`), " + _error(data, args[0], msg + "or the PR's full URL") return error = "Sorry, this command is not implemented yet" @@ -126,9 +127,9 @@ def _approve(data, args: list[str]): """Approve command.""" # TODO: If the Slack channel belongs to a PR, the arg is optional. if len(args) != 2: - msg = "when called outside of a PR channel, this command requires exactly 1 argument - " - msg += "an ID of a GitHub PR (`//`), or the PR's full URL" - _error(data, args[0], msg) + msg = "when called outside of a PR channel, this command requires exactly " + msg += "1 argument - an ID of a GitHub PR (`//`), " + _error(data, args[0], msg + "or the PR's full URL") return error = "Sorry, this command is not implemented yet" diff --git a/purrr/slack_cmd_test.py b/purrr/slack_cmd_test.py index bb87f5fd..78c62509 100644 --- a/purrr/slack_cmd_test.py +++ b/purrr/slack_cmd_test.py @@ -1,3 +1,5 @@ +"""Unit tests for the "slack_cmd" module.""" + from datetime import datetime import unittest from unittest.mock import MagicMock @@ -10,8 +12,8 @@ class SlackCmdTest(unittest.TestCase): """Unit tests for the "slack_cmd" module.""" - def __init__(self, methodName="runTest"): - super().__init__(methodName) + def __init__(self, method_name="runTest"): + super().__init__(method_name) self.data = { "channel_id": "purr-debug", "user_id": "user", @@ -105,7 +107,9 @@ def test_on_slack_slash_command_with_noop_opt_out(self): slack_cmd.slack.chat_postEphemeral.assert_called_once_with( channel=event.data.channel_id, user=event.data.user_id, - text=":no_bell: You're already opted out of Purrr since: 0001-01-01 00:00:00", + text=( + ":no_bell: You're already opted out of Purrr since: 0001-01-01 00:00:00" + ), ) def test_on_slack_slash_command_with_second_opt_out(self): diff --git a/purrr/slack_helper_test.py b/purrr/slack_helper_test.py index e0c8ab78..407019bb 100644 --- a/purrr/slack_helper_test.py +++ b/purrr/slack_helper_test.py @@ -1,3 +1,5 @@ +"""Unit tests for the "slack_helper" module.""" + import unittest import slack_helper diff --git a/purrr/users.py b/purrr/users.py index c14f6f53..0ce697e3 100644 --- a/purrr/users.py +++ b/purrr/users.py @@ -4,8 +4,8 @@ import github from slack_sdk.errors import SlackApiError -import debug import data_helper +import debug import github_helper diff --git a/reviewkitteh/program.py b/reviewkitteh/program.py index 32779043..fe27c2f0 100644 --- a/reviewkitteh/program.py +++ b/reviewkitteh/program.py @@ -7,10 +7,10 @@ name from a Google Sheet and pages that person in the Slack channel. """ -from datetime import datetime +from datetime import datetime, UTC import os -import time import random +import time from autokitteh.github import github_client from autokitteh.google import google_sheets_client @@ -59,4 +59,4 @@ def on_github_pull_request(event): def log(msg): - print(f"[{datetime.now()}] {msg}") + print(f"[{datetime.now(UTC)}] {msg}") diff --git a/room_reservation/available_rooms.py b/room_reservation/available_rooms.py index 20827f6a..9eb716fd 100644 --- a/room_reservation/available_rooms.py +++ b/room_reservation/available_rooms.py @@ -1,12 +1,12 @@ """List all the available rooms for the next half hour.""" -from datetime import UTC, datetime, timedelta +from datetime import datetime, timedelta, UTC from autokitteh.google import google_calendar_client from autokitteh.slack import slack_client from googleapiclient.errors import HttpError -from util import get_room_list +import util def on_slack_slash_command(event): @@ -21,7 +21,7 @@ def on_slack_slash_command(event): # Iterate over the list of rooms, notify the user about # each room which is available in the next half hour. available = False - for room in sorted(get_room_list()): + for room in sorted(util.get_room_list()): print(f"Checking upcoming events in: {room}") try: events = gcal.list( diff --git a/room_reservation/reserve_room.py b/room_reservation/reserve_room.py index a580035c..cd9a91c8 100644 --- a/room_reservation/reserve_room.py +++ b/room_reservation/reserve_room.py @@ -1,36 +1,38 @@ """Reserve a specific room for the next half hour.""" -from datetime import UTC, datetime, timedelta +from datetime import datetime, timedelta, UTC import autokitteh from autokitteh.google import google_calendar_client from autokitteh.slack import slack_client from googleapiclient.errors import HttpError -from util import get_room_list, get_email_from_slack_command +import util def on_slack_slash_command(event): - """Entry point for the "/ reserveroom " Slack slash command.""" + """Entry point for the "reserveroom <room> <title>" Slack slash command.""" + data = event.data + slack = slack_client("slack_conn") - channel_id = event.data.user_id # event.data.channel_id + channel_id = data.user_id # DM the user who sent the command. - cmd_text = event.data.text.split(maxsplit=2) + cmd_text = data.text.split(maxsplit=2) if len(cmd_text) < 3: - err = "Error: please use the following format: `/<app-name> reserveroom <room> <title>`" + err = f"Error: use this format: `{data.command} reserveroom <room> <title>`" slack.chat_postMessage(channel=channel_id, text=err) return _, room, title = cmd_text - room = get_email_from_slack_command(room) + room = util.get_email_from_slack_command(room) - if room not in get_room_list(): + if room not in util.get_room_list(): err = f"Error: `{room}` not found in the list of rooms" slack.chat_postMessage(channel=channel_id, text=err) return - user = slack.users_profile_get(user=event.data.user_id).get("profile") + user = slack.users_profile_get(user=data.user_id).get("profile") now = datetime.now(UTC) in_5_minutes = now + timedelta(minutes=5) diff --git a/room_reservation/room_status.py b/room_reservation/room_status.py index cc1c9638..0cd0d6a5 100644 --- a/room_reservation/room_status.py +++ b/room_reservation/room_status.py @@ -1,13 +1,13 @@ """Check the status of a specific room for the next hour.""" -from datetime import UTC, datetime, timedelta +from datetime import datetime, timedelta, UTC import autokitteh from autokitteh.google import google_calendar_client from autokitteh.slack import slack_client from googleapiclient.errors import HttpError -from util import get_email_from_slack_command, get_room_list +import util def on_slack_slash_command(event): @@ -17,9 +17,9 @@ def on_slack_slash_command(event): # Extract the email address from the Slack command text, which is formatted like: # "<@USER_ID> <mailto:test@example.com|test@example.com>". - room = get_email_from_slack_command(event.data.text) + room = util.get_email_from_slack_command(event.data.text) - if room not in get_room_list(): + if room not in util.get_room_list(): err = f"Error: `{room}` not found in the list of rooms" slack.chat_postMessage(channel=channel_id, text=err) diff --git a/room_reservation/util.py b/room_reservation/util.py index b8b1064e..a41f92d1 100644 --- a/room_reservation/util.py +++ b/room_reservation/util.py @@ -11,5 +11,7 @@ def get_room_list(): def get_email_from_slack_command(text): """Extract the email address from the Slack command text, which is formatted like: - "<@USER_ID> <mailto:test@example.com|test@example.com>".""" + + "<@USER_ID> <mailto:test@example.com|test@example.com>". + """ return text.split("|")[-1].strip(">") diff --git a/samples/discord/discord_client/program.py b/samples/discord/discord_client/program.py index b6ae37d3..53cfaacd 100644 --- a/samples/discord/discord_client/program.py +++ b/samples/discord/discord_client/program.py @@ -1,33 +1,4 @@ -""" -This program demonstrates how to use Autokitteh's Discord integration to create a bot that performs basic operations. - -Modules: - - autokitteh.discord: Custom wrapper for the Discord API client. - - discord: The native Discord client API. - -Functions: - - on_ready: An asynchronous function that is triggered when the bot successfully connects to Discord. - It checks for the availability of a specified channel and sends a message if the channel is found. - Handles permission and HTTP-related exceptions when attempting to send the message. - - - on_http_get: An AutoKitteh activity that runs the bot when an HTTP GET request is received. - Retrieves the bot token and initiates the bot connection. - - - client.run(token): Starts the bot and connects it to the Discord gateway. - This function blocks execution until the bot is stopped. While running, the bot listens for and - responds to events, such as `on_ready`. - - The `run` method initializes the event loop for the bot, connecting it to Discord using the specified token. - Once the connection is established, it triggers the `on_ready` event, allowing the bot to perform any - startup actions, such as checking for available channels and sending messages. This combination of - `run` and `on_ready` forms the backbone of the bot's lifecycle: - - `run` starts the bot and maintains the connection to Discord. - - `on_ready` is triggered automatically when the connection to Discord is fully established. - -Notes: - - This bot listens for the `on_ready` event to confirm a successful connection. - - The bot sends a "Meow!" message to the specified channel once connected. -""" +"""Discprd bot that performs basic operations.""" import os @@ -44,12 +15,13 @@ @autokitteh.activity def start_event_loop(event): + """Starts the bot and connects it to the Discord gateway.""" client.run(ak_discord.bot_token("discord_conn")) - return @client.event async def on_ready(): + """Asynchronous function triggered when the bot successfully connects to Discord.""" print(f"We have logged in as {client.user}") channel_id = int(os.getenv("CHANNEL_ID")) channel = client.get_channel(channel_id) @@ -69,5 +41,6 @@ async def on_ready(): except discord.HTTPException as e: print(f"Failed to send message: {e}") finally: - # Closing the client to prevent duplicate messages or unexpected behavior in future workflows + # Closing the client to prevent duplicate messages + # or unexpected behavior in future workflows. await client.close() diff --git a/samples/discord/events/program.py b/samples/discord/events/program.py index d46da83a..bd6d50e6 100644 --- a/samples/discord/events/program.py +++ b/samples/discord/events/program.py @@ -1,5 +1,6 @@ -"""This program listens for message-related events in Discord and logs the corresponding information -using the `autokitteh.discord` client. +"""Handle message-related events in Discord. + +Also log the corresponding information using the `autokitteh.discord` client. """ diff --git a/samples/github/program.py b/samples/github/program.py index b8c2e51c..d8029b9c 100644 --- a/samples/github/program.py +++ b/samples/github/program.py @@ -16,6 +16,7 @@ from autokitteh.github import github_client + # https://docs.github.com/en/rest/reactions/reactions#about-reactions REACTIONS = ["+1", "-1", "laugh", "confused", "heart", "hooray", "rocket", "eyes"] diff --git a/samples/google/calendar/program.py b/samples/google/calendar/program.py index 7b5d0d26..cc8065af 100644 --- a/samples/google/calendar/program.py +++ b/samples/google/calendar/program.py @@ -5,7 +5,7 @@ - https://docs.autokitteh.com/integrations/google/calendar/events """ -from datetime import UTC, datetime +from datetime import datetime, UTC from autokitteh.google import google_calendar_client from googleapiclient.errors import HttpError diff --git a/samples/google/gemini/program.py b/samples/google/gemini/program.py index 6509d39d..9342235c 100644 --- a/samples/google/gemini/program.py +++ b/samples/google/gemini/program.py @@ -1,11 +1,12 @@ """Demonstration of AutoKitteh's Gemini integration. -A single entry-point function is implemented, it sends a couple of requests to the Gemini API, and prints the responses -in the AutoKitteh session log. +A single entry-point function is implemented, it sends a couple of requests +to the Gemini API, and prints the responses in the AutoKitteh session log. """ from autokitteh.google import gemini_client + MODEL = "gemini-1.5-flash" gemini = gemini_client("gemini_conn", model_name=MODEL) diff --git a/samples/google/sheets/program.py b/samples/google/sheets/program.py index be428c64..76dec312 100644 --- a/samples/google/sheets/program.py +++ b/samples/google/sheets/program.py @@ -71,15 +71,16 @@ def _read_values(id): """ # Default value render option: "FORMATTED_VALUE". resp = sheet.get(spreadsheetId=id, range="A1:B6").execute() - col_a, formatted_col_b = list(zip(*resp.get("values", []))) + col_a, formatted_col_b = list(zip(*resp.get("values", []), strict=True)) ufv = "UNFORMATTED_VALUE" resp = sheet.get(spreadsheetId=id, range="A1:B6", valueRenderOption=ufv).execute() - unformatted_col_b = [v for _, v in resp.get("values", [])] + unform_col_b = [v for _, v in resp.get("values", [])] - for i, row in enumerate(zip(col_a, formatted_col_b, unformatted_col_b)): + for i, row in enumerate(zip(col_a, formatted_col_b, unform_col_b, strict=True)): data_type, formatted, unformatted = row - text = f"Row {i + 1}: {data_type} = formatted `{formatted!r}`, unformatted `{unformatted!r}`" + text = f"Row {i + 1}: {data_type} = formatted " + text += f"`{formatted!r}`, unformatted `{unformatted!r}`" print(text) diff --git a/samples/http/bearer_token.py b/samples/http/bearer_token.py index 8a774aa9..6e99b306 100644 --- a/samples/http/bearer_token.py +++ b/samples/http/bearer_token.py @@ -13,7 +13,7 @@ def send_requests(base_url): print("\n>>> Sending HTTP requests with an OAuth bearer token") url = urljoin(base_url, "bearer") - token = "my_bearer_token" + token = "my_bearer_token" # noqa: S105 headers = {"Authorization": "Bearer " + token} resp = requests.get(url, headers=headers) _print_response_details(resp) diff --git a/samples/openai_chatgpt/program.py b/samples/openai_chatgpt/program.py index 418b7eaf..2f679d17 100644 --- a/samples/openai_chatgpt/program.py +++ b/samples/openai_chatgpt/program.py @@ -16,14 +16,14 @@ from autokitteh.openai import openai_client + MODEL = "gpt-4o-mini" chatgpt_client = openai_client("chatgpt_conn") def on_http_get(event): - """ - Entry-point function for handling HTTP GET requests in this workflow. + """Entry-point function for handling HTTP GET requests in this workflow. Example usage: - URL: "http://localhost:9980/webhooks/<webhook_slug>" @@ -46,13 +46,22 @@ def on_http_get(event): # Example 2: more verbose interaction with ChatGPT, # including the user's text as part of the conversation. - contents = [ - "You are a poetic assistant, skilled in explaining complex engineering concepts.", - "Compose a Shakespearean sonnet about the importance of reliability, scalability, and durability, in distributed workflows.", - ] msgs = [ - {"role": "system", "content": contents[0]}, - {"role": "user", "content": body or contents[1]}, + { + "role": "system", + "content": ( + "You are a poetic assistant, skilled in " + "explaining complex engineering concepts." + ), + }, + { + "role": "user", + "content": body + or ( + "Compose a Shakespearean sonnet about the importance of reliability, " + "scalability, and durability, in distributed workflows." + ), + }, ] resp = chatgpt_client.chat.completions.create(model=MODEL, messages=msgs) diff --git a/samples/scheduler/program.py b/samples/scheduler/program.py index 165dc15e..1394d848 100644 --- a/samples/scheduler/program.py +++ b/samples/scheduler/program.py @@ -1,15 +1,16 @@ -""" -This program demonstrates AutoKitteh's scheduler capabilities. +"""This program demonstrates AutoKitteh's scheduler capabilities. It implements a single entry-point function, configured in the "autokitteh.yaml" file to receive "scheduler" events, and uses constant values defined in the "autokitteh.yaml" manifest for each AutoKitteh environment. """ -from datetime import datetime, timedelta, timezone +from datetime import datetime, timedelta, UTC import os + from autokitteh.github import github_client + # Set in "autokitteh.yaml" GITHUB_OWNER = os.getenv("GITHUB_OWNER") GITHUB_REPO = os.getenv("GITHUB_REPO") @@ -21,7 +22,6 @@ def on_cron_trigger(_): """Handles the AutoKitteh cron schedule trigger.""" - # Fetch open pull requests that are not drafts or WIP repo = github.get_repo(f"{GITHUB_OWNER}/{GITHUB_REPO}") active_prs = [ @@ -32,7 +32,7 @@ def on_cron_trigger(_): and "wip" not in pr.title.lower() ] - now = datetime.now(timezone.utc) + now = datetime.now(UTC) opened_cutoff = now - timedelta(days=int(OPENED_CUTOFF)) update_cutoff = now - timedelta(days=int(UPDATE_CUTOFF)) diff --git a/samples/twilio/program.py b/samples/twilio/program.py index 7886333e..8f091f18 100644 --- a/samples/twilio/program.py +++ b/samples/twilio/program.py @@ -15,6 +15,7 @@ from autokitteh.twilio import twilio_client + FROM_PHONE_NUMBER = os.getenv("FROM_PHONE_NUMBER") t = twilio_client("twilio_conn") diff --git a/slack_discord_sync/program.py b/slack_discord_sync/program.py index b7bf5370..dbfb34f6 100644 --- a/slack_discord_sync/program.py +++ b/slack_discord_sync/program.py @@ -1,6 +1,4 @@ -""" -This script mirrors messages between Slack and Discord channels using -AutoKitteh's Slack and Discord clients. +"""Mirror messages between Slack and Discord channels using. Discord documentation: - https://discordpy.readthedocs.io/ @@ -45,10 +43,11 @@ def on_slack_message(event): @client.event async def on_ready(): - """An asynchronous event triggered when the Discord bot - successfully connects. It fetches the Discord channel by ID and sends - the latest message received from Slack to the channel, then closes the - client connection.""" + """An asynchronous event triggered when the Discord bot successfully connects. + + It fetches the Discord channel by ID and sends the latest message received + from Slack to the channel, then closes the client connection. + """ try: channel = await client.fetch_channel(DISCORD_CHANNEL_ID) except discord.DiscordException as e: diff --git a/slack_support/directory.py b/slack_support/directory.py index 93aaeb67..d2c2063d 100644 --- a/slack_support/directory.py +++ b/slack_support/directory.py @@ -1,7 +1,9 @@ -import os from collections import namedtuple +import os + from autokitteh.google import google_sheets_client + DIRECTORY_GOOGLE_SHEET_ID = os.getenv("DIRECTORY_GOOGLE_SHEET_ID") gsheets = google_sheets_client("mygsheets") @@ -21,7 +23,7 @@ def load() -> dict[str, list[Person]]: # topic -> list of people ppl = [Person(v[0], v[1], v[2].split(",")) for v in vs] - topics = set([topic for person in ppl for topic in person.topics]) + topics = {topic for person in ppl for topic in person.topics} return { topic: [person for person in ppl if topic in person.topics] for topic in topics diff --git a/slack_support/gemini.py b/slack_support/gemini.py index 6707f61d..92edd6df 100644 --- a/slack_support/gemini.py +++ b/slack_support/gemini.py @@ -1,10 +1,11 @@ -import os import json +import os import google.generativeai as genai -# How do i know if this is "picklable" or not? It has no return value? Do I need to run this every time? -# Is this running in an activity? + +# How do i know if this is "picklable" or not? It has no return value? +# Do I need to run this every time? Is this running in an activity? genai.configure(api_key=os.getenv("GEMINI_API_KEY")) # Make this run every time outside of an activity if it's in global scope? diff --git a/slack_support/main.py b/slack_support/main.py index bd88cc80..753f7add 100644 --- a/slack_support/main.py +++ b/slack_support/main.py @@ -1,11 +1,12 @@ +from datetime import datetime, UTC import os -from datetime import datetime import autokitteh from autokitteh.slack import slack_client -import gemini import directory +import gemini + HELP_REQUEST_TIMEOUT_MINUTES = int(os.getenv("HELP_REQUEST_TIMEOUT_MINUTES")) @@ -44,24 +45,23 @@ def send(text): # From this point on we are interested in any message that is added to the thread. # Further below we'll consume the messages and act on them using `next_event`. - s = autokitteh.subscribe( - "myslack", - f'data.type == "message" && data.thread_ts == "{event.data.ts}" && data.text.startsWith("!")', - ) + filter = "data.type == 'message' && data.thread_ts == " + filter += f"'{event.data.ts}' && data.text.startsWith('!')" + s = autokitteh.subscribe("myslack", filter) taken_by = None - start_time = datetime.now() + start_time = datetime.now(UTC) while True: msg = autokitteh.next_event(s, timeout=60) if not msg: # timeout - dt = (datetime.now() - start_time).total_seconds() + dt = (datetime.now(UTC) - start_time).total_seconds() print(f"timeout, dt={dt}") if not taken_by and dt >= HELP_REQUEST_TIMEOUT_MINUTES * 60: send(f"Reminder: {mentions}, please respond.") - start_time = datetime.now() + start_time = datetime.now(UTC) continue cmd = msg.text.strip()[1:] diff --git a/task_chain/event_driven/program.py b/task_chain/event_driven/program.py index 94b9ffaf..832d5eb8 100644 --- a/task_chain/event_driven/program.py +++ b/task_chain/event_driven/program.py @@ -79,6 +79,6 @@ def on_slack_interaction(event): if event.data.actions[0]["value"] == "abort": return - # This workflow's starting point is a retry of the failed task in the aborted workflow. + # This workflow's starting point is a retry of the failed task in the aborted one. i = int(event.data.actions[0]["action_id"].split()[-1]) run_tasks(i, event.data.user.id) diff --git a/update_projects_table.py b/update_projects_table.py index 98054c6e..8a57c93e 100644 --- a/update_projects_table.py +++ b/update_projects_table.py @@ -45,7 +45,8 @@ def to_table_row(project_dir: Path, metadata: dict) -> str: def insert_rows_to_table(readme_file: Path, new_rows: list[str]) -> None: """Insert rows into the table section of the README file.""" md = readme_file.read_text(encoding="utf-8") - table = "-->\n| Name | Description | Integration |\n| :--- | :---------- | :---------- |\n" + table = "-->\n| Name | Description | Integration |\n" + table += "| :--- | :---------- | :---------- |\n" for row in new_rows: table += row