Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Emailer #10

Merged
merged 2 commits into from
Dec 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -269,3 +269,4 @@ $RECYCLE.BIN/
# End of https://www.toptal.com/developers/gitignore/api/python,visualstudiocode,windows,linux,macos
config.json
config.toml
volumes/
4 changes: 2 additions & 2 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"module": "label_studio_slack_reporter.service",
"args": [
"--config",
"${workspaceFolder}/config.toml",
"${workspaceFolder}/volumes/config/config.toml",
"--debug"
]
},
Expand All @@ -22,7 +22,7 @@
"module": "label_studio_slack_reporter.main",
"args": [
"--config",
"${workspaceFolder}/config.toml"
"${workspaceFolder}/volumes/config/config.toml"
]
}
]
Expand Down
16 changes: 16 additions & 0 deletions example_service_config.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,28 @@
[prometheus]
port = 9100

[label_studio]
key = "abcdef1234"
url = "https://labeler.e4e.ucsd.edu"
project_ids = [10]
report_days = 1

[api.google]
credentials = "gcloud_credentials.json"
token = "volumes/cache/gapp_token.json"

[output.slack]
type = "slack"
secret = "xoxb-abcdef1234"
channel_id = "abcdef1234"
project_ids = [10]
schedule = "0 9 * * *"

[output.email]
type = "email"
project_ids = [10]
schedule = "0 9 * * *"
to = [
"[email protected]"
]
subject = "UCSD E4E Label Studio Progress Report"
12 changes: 12 additions & 0 deletions label_studio_slack_reporter/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
'''Label Studio Reporter Exceptions
'''


class GoogleAppCredentialsNotFound(BaseException):
"""Google App Credentials Not Found
"""


class GmailServiceCreateFail(BaseException):
"""Gmail Service Creation failure
"""
115 changes: 115 additions & 0 deletions label_studio_slack_reporter/gapp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
'''Google Application Interface
'''
from __future__ import annotations

import logging
from argparse import ArgumentParser
from pathlib import Path
from typing import Optional

from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import Resource, build
from googleapiclient.errors import HttpError

from label_studio_slack_reporter.exceptions import (
GmailServiceCreateFail, GoogleAppCredentialsNotFound)


class GoogleAppService:
"""Google App Service
"""
GOOGLE_API_SCOPES = [
'https://www.googleapis.com/auth/gmail.send',
]

__instance: Optional[GoogleAppService] = None

@classmethod
def get_instance(cls) -> GoogleAppService:
"""Retrieves the singleton instance

Raises:
RuntimeError: Singleton is not initialized

Returns:
GoogleAppService: Singleton instance
"""
if not cls.__instance:
raise RuntimeError
return cls.__instance

def __init__(self,
credentials: Path,
token: Path):
if self.__instance is not None:
raise RuntimeError('Singleton violation')
if not credentials.is_file():
raise GoogleAppCredentialsNotFound(credentials.as_posix())

self.__creds_path = credentials
self.__token_path = token
self.__token: Optional[Credentials] = None
self.__log = logging.getLogger('GoogleAppService')

self.load()
GoogleAppService.__instance = self

def load(self):
"""Loads and refreshes the tokens
"""
if self.__token_path.is_file():
self.__token = Credentials.from_authorized_user_file(
filename=self.__token_path.as_posix(),
scopes=self.GOOGLE_API_SCOPES)
if not self.__token or not self.__token.valid:
if (self.__token and
self.__token.expired and
self.__token.refresh_token):
self.__token.refresh(request=Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
client_secrets_file=self.__creds_path.as_posix(),
scopes=self.GOOGLE_API_SCOPES
)
self.__token = flow.run_local_server()
with open(self.__token_path, 'w', encoding='utf-8') as handle:
handle.write(self.__token.to_json())

def get_gmail_service(self) -> Resource:
"""Retrieves the gmail service

Raises:
GmailServiceCreateFail: On service creation failure

Returns:
Resource: Gmail Service Resource
"""
self.load()
try:
service: Resource = build(
serviceName='gmail',
version='v1',
credentials=self.__token
)
except HttpError as exc:
self.__log.exception(
'Failed to retrieve gmail service due to %s', exc)
raise GmailServiceCreateFail from exc
return service


def run_cli_gapp():
"""Run Gapp CLI Load
"""
parser = ArgumentParser()
parser.add_argument('credentials', type=Path)
parser.add_argument('token', type=Path)

args = vars(parser.parse_args())
GoogleAppService(**args)


if __name__ == '__main__':
run_cli_gapp()
49 changes: 49 additions & 0 deletions label_studio_slack_reporter/output.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
'''Reporters
'''
import base64
from abc import ABC, abstractmethod
from email.mime.text import MIMEText
from typing import List

from googleapiclient.discovery import Resource
from googleapiclient.errors import HttpError
from slack_sdk import WebClient

from label_studio_slack_reporter.gapp import GoogleAppService


class AbstractOutput(ABC):
"""Abstract Output Job
Expand Down Expand Up @@ -50,3 +57,45 @@ def execute(self, message):
channel=self.__channel_id,
text=message
)


class EmailOutput(AbstractOutput):
"""Email Output
"""
# pylint: disable=too-few-public-methods

def __init__(self,
schedule: str,
job_name: str,
subject: str,
to: List[str],
cc: List[str] = None,
bcc: List[str] = None,
**kwargs):
# pylint: disable=too-many-arguments,too-many-positional-arguments
super().__init__(schedule, job_name, **kwargs)
self.__subject = subject
self.__to = to
self.__cc = cc
self.__bcc = bcc

def execute(self, message):
gmail_service = GoogleAppService.get_instance().get_gmail_service()
message_service: Resource = gmail_service.users().messages()
email_message = MIMEText(message, 'plain')
email_message['from'] = '[email protected]'
if len(self.__to) > 0:
email_message['to'] = '; '.join(self.__to)
if self.__cc and len(self.__cc) > 0:
email_message['cc'] = '; '.join(self.__cc)
if self.__bcc and len(self.__bcc) > 0:
email_message['bcc'] = '; '.join(self.__bcc)
email_message['subject'] = self.__subject
try:
message_service.send(
userId='me',
body={'raw': base64.urlsafe_b64encode(
email_message.as_bytes()).decode()}
).execute()
except HttpError as exc:
raise exc
16 changes: 12 additions & 4 deletions label_studio_slack_reporter/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,19 @@

from label_studio_slack_reporter.config import configure_logging
from label_studio_slack_reporter.label_studio import Reporter
from label_studio_slack_reporter.metrics import (get_summary, get_counter,
from label_studio_slack_reporter.metrics import (get_counter, get_summary,
system_monitor_thread)
from label_studio_slack_reporter.output import AbstractOutput, SlackOutput

from label_studio_slack_reporter.output import (AbstractOutput, EmailOutput,
SlackOutput)
from label_studio_slack_reporter.gapp import GoogleAppService

class Service:
"""Main service
"""
# pylint: disable=too-many-instance-attributes
OUTPUT_TYPE_MAPPING = {
'slack': SlackOutput
'slack': SlackOutput,
'email': EmailOutput
}

def __init__(self,
Expand Down Expand Up @@ -82,6 +84,10 @@ def __init__(self,
name='scheduler_errors',
documentation='Scheduler error count'
)
GoogleAppService(
credentials=Path(self.__config['api']['google']['credentials']),
token=Path(self.__config['api']['google']['token'])
)

def __configure_schedule(self):
current_tz = get_localzone()
Expand Down Expand Up @@ -127,6 +133,8 @@ def do_jobs(self):
with self.__output_timer.labels(job=job.name).time():
job.execute(message=message)
self.__log.info('Executed %s', job.name)
else:
self.__log.warning('Debug mode - no output executed!')
except Exception: # pylint: disable=broad-exception-caught
self.__log.exception('Failed to execute %s', job.name)
get_counter('job_execute_errors').labels(
Expand Down
Loading
Loading