From 06d015f6f95372b1ec502f3cf576942f0d07df5a Mon Sep 17 00:00:00 2001 From: Nathan Hui Date: Mon, 23 Dec 2024 22:22:35 -0800 Subject: [PATCH 1/2] chore: Ignores volumes --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 3d7e205..9e175fc 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ \ No newline at end of file From 1cfa93c706dcc56062cef805b48be58b4633ce52 Mon Sep 17 00:00:00 2001 From: Nathan Hui Date: Mon, 23 Dec 2024 22:27:02 -0800 Subject: [PATCH 2/2] feat: Adds email output --- .vscode/launch.json | 4 +- example_service_config.toml | 16 ++ label_studio_slack_reporter/exceptions.py | 12 + label_studio_slack_reporter/gapp.py | 115 +++++++++ label_studio_slack_reporter/output.py | 49 ++++ label_studio_slack_reporter/service.py | 16 +- poetry.lock | 278 +++++++++++++++++++++- pyproject.toml | 3 + 8 files changed, 486 insertions(+), 7 deletions(-) create mode 100644 label_studio_slack_reporter/exceptions.py create mode 100644 label_studio_slack_reporter/gapp.py diff --git a/.vscode/launch.json b/.vscode/launch.json index 88016cc..3fad034 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,7 +11,7 @@ "module": "label_studio_slack_reporter.service", "args": [ "--config", - "${workspaceFolder}/config.toml", + "${workspaceFolder}/volumes/config/config.toml", "--debug" ] }, @@ -22,7 +22,7 @@ "module": "label_studio_slack_reporter.main", "args": [ "--config", - "${workspaceFolder}/config.toml" + "${workspaceFolder}/volumes/config/config.toml" ] } ] diff --git a/example_service_config.toml b/example_service_config.toml index 8fbc19e..4774120 100644 --- a/example_service_config.toml +++ b/example_service_config.toml @@ -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 = [ + "nthui@ucsd.edu" +] +subject = "UCSD E4E Label Studio Progress Report" \ No newline at end of file diff --git a/label_studio_slack_reporter/exceptions.py b/label_studio_slack_reporter/exceptions.py new file mode 100644 index 0000000..509c550 --- /dev/null +++ b/label_studio_slack_reporter/exceptions.py @@ -0,0 +1,12 @@ +'''Label Studio Reporter Exceptions +''' + + +class GoogleAppCredentialsNotFound(BaseException): + """Google App Credentials Not Found + """ + + +class GmailServiceCreateFail(BaseException): + """Gmail Service Creation failure + """ diff --git a/label_studio_slack_reporter/gapp.py b/label_studio_slack_reporter/gapp.py new file mode 100644 index 0000000..7edd0e0 --- /dev/null +++ b/label_studio_slack_reporter/gapp.py @@ -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() diff --git a/label_studio_slack_reporter/output.py b/label_studio_slack_reporter/output.py index c130d15..58151a5 100644 --- a/label_studio_slack_reporter/output.py +++ b/label_studio_slack_reporter/output.py @@ -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 @@ -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'] = 'e4e@ucsd.edu' + 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 diff --git a/label_studio_slack_reporter/service.py b/label_studio_slack_reporter/service.py index 04e4e1c..eb7f4cb 100644 --- a/label_studio_slack_reporter/service.py +++ b/label_studio_slack_reporter/service.py @@ -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, @@ -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() @@ -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( diff --git a/poetry.lock b/poetry.lock index 882b88e..197102f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -310,6 +310,17 @@ webencodings = "*" [package.extras] css = ["tinycss2 (>=1.1.0,<1.5)"] +[[package]] +name = "cachetools" +version = "5.5.0" +description = "Extensible memoizing collections and decorators" +optional = false +python-versions = ">=3.7" +files = [ + {file = "cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292"}, + {file = "cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a"}, +] + [[package]] name = "certifi" version = "2024.8.30" @@ -892,6 +903,122 @@ gitdb = ">=4.0.1,<5" doc = ["sphinx (==4.3.2)", "sphinx-autodoc-typehints", "sphinx-rtd-theme", "sphinxcontrib-applehelp (>=1.0.2,<=1.0.4)", "sphinxcontrib-devhelp (==1.0.2)", "sphinxcontrib-htmlhelp (>=2.0.0,<=2.0.1)", "sphinxcontrib-qthelp (==1.0.3)", "sphinxcontrib-serializinghtml (==1.1.5)"] test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions"] +[[package]] +name = "google-api-core" +version = "2.24.0" +description = "Google API client core library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "google_api_core-2.24.0-py3-none-any.whl", hash = "sha256:10d82ac0fca69c82a25b3efdeefccf6f28e02ebb97925a8cce8edbfe379929d9"}, + {file = "google_api_core-2.24.0.tar.gz", hash = "sha256:e255640547a597a4da010876d333208ddac417d60add22b6851a0c66a831fcaf"}, +] + +[package.dependencies] +google-auth = ">=2.14.1,<3.0.dev0" +googleapis-common-protos = ">=1.56.2,<2.0.dev0" +proto-plus = ">=1.22.3,<2.0.0dev" +protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0" +requests = ">=2.18.0,<3.0.0.dev0" + +[package.extras] +async-rest = ["google-auth[aiohttp] (>=2.35.0,<3.0.dev0)"] +grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio (>=1.49.1,<2.0dev)", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0)"] +grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] +grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] + +[[package]] +name = "google-api-python-client" +version = "2.156.0" +description = "Google API Client Library for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "google_api_python_client-2.156.0-py2.py3-none-any.whl", hash = "sha256:6352185c505e1f311f11b0b96c1b636dcb0fec82cd04b80ac5a671ac4dcab339"}, + {file = "google_api_python_client-2.156.0.tar.gz", hash = "sha256:b809c111ded61716a9c1c7936e6899053f13bae3defcdfda904bd2ca68065b9c"}, +] + +[package.dependencies] +google-api-core = ">=1.31.5,<2.0.dev0 || >2.3.0,<3.0.0.dev0" +google-auth = ">=1.32.0,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0.dev0" +google-auth-httplib2 = ">=0.2.0,<1.0.0" +httplib2 = ">=0.19.0,<1.dev0" +uritemplate = ">=3.0.1,<5" + +[[package]] +name = "google-auth" +version = "2.37.0" +description = "Google Authentication Library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "google_auth-2.37.0-py2.py3-none-any.whl", hash = "sha256:42664f18290a6be591be5329a96fe30184be1a1badb7292a7f686a9659de9ca0"}, + {file = "google_auth-2.37.0.tar.gz", hash = "sha256:0054623abf1f9c83492c63d3f47e77f0a544caa3d40b2d98e099a611c2dd5d00"}, +] + +[package.dependencies] +cachetools = ">=2.0.0,<6.0" +pyasn1-modules = ">=0.2.1" +rsa = ">=3.1.4,<5" + +[package.extras] +aiohttp = ["aiohttp (>=3.6.2,<4.0.0.dev0)", "requests (>=2.20.0,<3.0.0.dev0)"] +enterprise-cert = ["cryptography", "pyopenssl"] +pyjwt = ["cryptography (>=38.0.3)", "pyjwt (>=2.0)"] +pyopenssl = ["cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] +reauth = ["pyu2f (>=0.1.5)"] +requests = ["requests (>=2.20.0,<3.0.0.dev0)"] + +[[package]] +name = "google-auth-httplib2" +version = "0.2.0" +description = "Google Authentication Library: httplib2 transport" +optional = false +python-versions = "*" +files = [ + {file = "google-auth-httplib2-0.2.0.tar.gz", hash = "sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05"}, + {file = "google_auth_httplib2-0.2.0-py2.py3-none-any.whl", hash = "sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d"}, +] + +[package.dependencies] +google-auth = "*" +httplib2 = ">=0.19.0" + +[[package]] +name = "google-auth-oauthlib" +version = "1.2.1" +description = "Google Authentication Library" +optional = false +python-versions = ">=3.6" +files = [ + {file = "google_auth_oauthlib-1.2.1-py2.py3-none-any.whl", hash = "sha256:2d58a27262d55aa1b87678c3ba7142a080098cbc2024f903c62355deb235d91f"}, + {file = "google_auth_oauthlib-1.2.1.tar.gz", hash = "sha256:afd0cad092a2eaa53cd8e8298557d6de1034c6cb4a740500b5357b648af97263"}, +] + +[package.dependencies] +google-auth = ">=2.15.0" +requests-oauthlib = ">=0.7.0" + +[package.extras] +tool = ["click (>=6.0.0)"] + +[[package]] +name = "googleapis-common-protos" +version = "1.66.0" +description = "Common protobufs used in Google APIs" +optional = false +python-versions = ">=3.7" +files = [ + {file = "googleapis_common_protos-1.66.0-py2.py3-none-any.whl", hash = "sha256:d7abcd75fabb2e0ec9f74466401f6c119a0b498e27370e9be4c94cb7e382b8ed"}, + {file = "googleapis_common_protos-1.66.0.tar.gz", hash = "sha256:c3e7b33d15fdca5374cc0a7346dd92ffa847425cc4ea941d970f13680052ec8c"}, +] + +[package.dependencies] +protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0" + +[package.extras] +grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"] + [[package]] name = "h11" version = "0.14.0" @@ -924,6 +1051,20 @@ http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] trio = ["trio (>=0.22.0,<1.0)"] +[[package]] +name = "httplib2" +version = "0.22.0" +description = "A comprehensive HTTP client library." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "httplib2-0.22.0-py3-none-any.whl", hash = "sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc"}, + {file = "httplib2-0.22.0.tar.gz", hash = "sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81"}, +] + +[package.dependencies] +pyparsing = {version = ">=2.4.2,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.0.2 || >3.0.2,<3.0.3 || >3.0.3,<4", markers = "python_version > \"3.0\""} + [[package]] name = "httpx" version = "0.28.1" @@ -2161,6 +2302,22 @@ files = [ {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, ] +[[package]] +name = "oauthlib" +version = "3.2.2" +description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" +optional = false +python-versions = ">=3.6" +files = [ + {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"}, + {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, +] + +[package.extras] +rsa = ["cryptography (>=3.0.0)"] +signals = ["blinker (>=1.4.0)"] +signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] + [[package]] name = "overrides" version = "7.7.0" @@ -2467,6 +2624,43 @@ files = [ [package.dependencies] wcwidth = "*" +[[package]] +name = "proto-plus" +version = "1.25.0" +description = "Beautiful, Pythonic protocol buffers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "proto_plus-1.25.0-py3-none-any.whl", hash = "sha256:c91fc4a65074ade8e458e95ef8bac34d4008daa7cce4a12d6707066fca648961"}, + {file = "proto_plus-1.25.0.tar.gz", hash = "sha256:fbb17f57f7bd05a68b7707e745e26528b0b3c34e378db91eef93912c54982d91"}, +] + +[package.dependencies] +protobuf = ">=3.19.0,<6.0.0dev" + +[package.extras] +testing = ["google-api-core (>=1.31.5)"] + +[[package]] +name = "protobuf" +version = "5.29.2" +description = "" +optional = false +python-versions = ">=3.8" +files = [ + {file = "protobuf-5.29.2-cp310-abi3-win32.whl", hash = "sha256:c12ba8249f5624300cf51c3d0bfe5be71a60c63e4dcf51ffe9a68771d958c851"}, + {file = "protobuf-5.29.2-cp310-abi3-win_amd64.whl", hash = "sha256:842de6d9241134a973aab719ab42b008a18a90f9f07f06ba480df268f86432f9"}, + {file = "protobuf-5.29.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a0c53d78383c851bfa97eb42e3703aefdc96d2036a41482ffd55dc5f529466eb"}, + {file = "protobuf-5.29.2-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:494229ecd8c9009dd71eda5fd57528395d1eacdf307dbece6c12ad0dd09e912e"}, + {file = "protobuf-5.29.2-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:b6b0d416bbbb9d4fbf9d0561dbfc4e324fd522f61f7af0fe0f282ab67b22477e"}, + {file = "protobuf-5.29.2-cp38-cp38-win32.whl", hash = "sha256:e621a98c0201a7c8afe89d9646859859be97cb22b8bf1d8eacfd90d5bda2eb19"}, + {file = "protobuf-5.29.2-cp38-cp38-win_amd64.whl", hash = "sha256:13d6d617a2a9e0e82a88113d7191a1baa1e42c2cc6f5f1398d3b054c8e7e714a"}, + {file = "protobuf-5.29.2-cp39-cp39-win32.whl", hash = "sha256:36000f97ea1e76e8398a3f02936aac2a5d2b111aae9920ec1b769fc4a222c4d9"}, + {file = "protobuf-5.29.2-cp39-cp39-win_amd64.whl", hash = "sha256:2d2e674c58a06311c8e99e74be43e7f3a8d1e2b2fdf845eaa347fbd866f23355"}, + {file = "protobuf-5.29.2-py3-none-any.whl", hash = "sha256:fde4554c0e578a5a0bcc9a276339594848d1e89f9ea47b4427c80e5d72f90181"}, + {file = "protobuf-5.29.2.tar.gz", hash = "sha256:b2cc8e8bb7c9326996f0e160137b0861f1a82162502658df2951209d0cb0309e"}, +] + [[package]] name = "psutil" version = "6.1.0" @@ -2522,6 +2716,31 @@ files = [ [package.extras] tests = ["pytest"] +[[package]] +name = "pyasn1" +version = "0.6.1" +description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"}, + {file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"}, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.1" +description = "A collection of ASN.1-based protocols modules" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd"}, + {file = "pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c"}, +] + +[package.dependencies] +pyasn1 = ">=0.4.6,<0.7.0" + [[package]] name = "pycodestyle" version = "2.12.1" @@ -2726,6 +2945,20 @@ tomlkit = ">=0.10.1" spelling = ["pyenchant (>=3.2,<4.0)"] testutils = ["gitpython (>3)"] +[[package]] +name = "pyparsing" +version = "3.2.0" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pyparsing-3.2.0-py3-none-any.whl", hash = "sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84"}, + {file = "pyparsing-3.2.0.tar.gz", hash = "sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + [[package]] name = "pytest" version = "8.3.4" @@ -3218,6 +3451,24 @@ requests = ">=2.22,<3" [package.extras] fixture = ["fixtures"] +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +description = "OAuthlib authentication support for Requests." +optional = false +python-versions = ">=3.4" +files = [ + {file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"}, + {file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"}, +] + +[package.dependencies] +oauthlib = ">=3.0.0" +requests = ">=2.0.0" + +[package.extras] +rsa = ["oauthlib[signedtoken] (>=3.0.0)"] + [[package]] name = "requests-toolbelt" version = "1.0.0" @@ -3387,6 +3638,20 @@ files = [ {file = "rpds_py-0.22.3.tar.gz", hash = "sha256:e32fee8ab45d3c2db6da19a5323bc3362237c8b653c70194414b892fd06a080d"}, ] +[[package]] +name = "rsa" +version = "4.9" +description = "Pure-Python RSA implementation" +optional = false +python-versions = ">=3.6,<4" +files = [ + {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, + {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, +] + +[package.dependencies] +pyasn1 = ">=0.1.3" + [[package]] name = "rstr" version = "3.2.2" @@ -3805,6 +4070,17 @@ files = [ [package.extras] dev = ["flake8", "flake8-annotations", "flake8-bandit", "flake8-bugbear", "flake8-commas", "flake8-comprehensions", "flake8-continuation", "flake8-datetimez", "flake8-docstrings", "flake8-import-order", "flake8-literal", "flake8-modern-annotations", "flake8-noqa", "flake8-pyproject", "flake8-requirements", "flake8-typechecking-import", "flake8-use-fstring", "mypy", "pep8-naming", "types-PyYAML"] +[[package]] +name = "uritemplate" +version = "4.1.1" +description = "Implementation of RFC 6570 URI Templates" +optional = false +python-versions = ">=3.6" +files = [ + {file = "uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e"}, + {file = "uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0"}, +] + [[package]] name = "urllib3" version = "2.2.3" @@ -3970,4 +4246,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = ">=3.12,<3.13" -content-hash = "0261cce0df7d2fe08fccfa814714417ae6baff516e7a67f02df2eedeb450f193" +content-hash = "ceb4c5491be5996ee10e49deb26a69500bed673991eba2b8464219c80a14f0bf" diff --git a/pyproject.toml b/pyproject.toml index 7002c0f..0588ba2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,9 @@ pycron = "^3.1.1" prometheus-client = "^0.21.1" pytz = "^2024.2" tzlocal = "^5.2" +google-api-python-client = "^2.156.0" +google-auth-httplib2 = "^0.2.0" +google-auth-oauthlib = "^1.2.1" [tool.poetry.group.dev.dependencies] pylint = "^3.2.7"