|
| 1 | +# |
| 2 | +# Licensed to the Apache Software Foundation (ASF) under one |
| 3 | +# or more contributor license agreements. See the NOTICE file |
| 4 | +# distributed with this work for additional information |
| 5 | +# regarding copyright ownership. The ASF licenses this file |
| 6 | +# to you under the Apache License, Version 2.0 (the |
| 7 | +# "License"); you may not use this file except in compliance |
| 8 | +# with the License. You may obtain a copy of the License at |
| 9 | +# |
| 10 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 11 | +# |
| 12 | +# Unless required by applicable law or agreed to in writing, |
| 13 | +# software distributed under the License is distributed on an |
| 14 | +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| 15 | +# KIND, either express or implied. See the License for the |
| 16 | +# specific language governing permissions and limitations |
| 17 | +# under the License. |
| 18 | +# |
| 19 | +"""This module contains a Google Calendar API hook""" |
| 20 | + |
| 21 | +from datetime import datetime |
| 22 | +from typing import Any, Dict, Optional, Sequence, Union |
| 23 | + |
| 24 | +from googleapiclient.discovery import build |
| 25 | + |
| 26 | +from airflow.exceptions import AirflowException |
| 27 | +from airflow.providers.google.common.hooks.base_google import GoogleBaseHook |
| 28 | + |
| 29 | + |
| 30 | +class GoogleCalendarHook(GoogleBaseHook): |
| 31 | + """ |
| 32 | + Interact with Google Calendar via Google Cloud connection |
| 33 | + Reading and writing cells in Google Sheet: |
| 34 | + https://developers.google.com/calendar/api/v3/reference |
| 35 | +
|
| 36 | + :param gcp_conn_id: The connection ID to use when fetching connection info. |
| 37 | + :type gcp_conn_id: str |
| 38 | + :param api_version: API Version. For example v3 |
| 39 | + :type api_version: str |
| 40 | + :param delegate_to: The account to impersonate using domain-wide delegation of authority, |
| 41 | + if any. For this to work, the service account making the request must have |
| 42 | + domain-wide delegation enabled. |
| 43 | + :type delegate_to: str |
| 44 | + :param impersonation_chain: Optional service account to impersonate using short-term |
| 45 | + credentials, or chained list of accounts required to get the access_token |
| 46 | + of the last account in the list, which will be impersonated in the request. |
| 47 | + If set as a string, the account must grant the originating account |
| 48 | + the Service Account Token Creator IAM role. |
| 49 | + If set as a sequence, the identities from the list must grant |
| 50 | + Service Account Token Creator IAM role to the directly preceding identity, with first |
| 51 | + account from the list granting this role to the originating account. |
| 52 | + :type impersonation_chain: Union[str, Sequence[str]] |
| 53 | + """ |
| 54 | + |
| 55 | + def __init__( |
| 56 | + self, |
| 57 | + api_version: str, |
| 58 | + gcp_conn_id: str = 'google_cloud_default', |
| 59 | + delegate_to: Optional[str] = None, |
| 60 | + impersonation_chain: Optional[Union[str, Sequence[str]]] = None, |
| 61 | + ) -> None: |
| 62 | + super().__init__( |
| 63 | + gcp_conn_id=gcp_conn_id, |
| 64 | + delegate_to=delegate_to, |
| 65 | + impersonation_chain=impersonation_chain, |
| 66 | + ) |
| 67 | + self.gcp_conn_id = gcp_conn_id |
| 68 | + self.api_version = api_version |
| 69 | + self.delegate_to = delegate_to |
| 70 | + self._conn = None |
| 71 | + |
| 72 | + def get_conn(self) -> Any: |
| 73 | + """ |
| 74 | + Retrieves connection to Google Calendar. |
| 75 | +
|
| 76 | + :return: Google Calendar services object. |
| 77 | + :rtype: Any |
| 78 | + """ |
| 79 | + if not self._conn: |
| 80 | + http_authorized = self._authorize() |
| 81 | + self._conn = build('calendar', self.api_version, http=http_authorized, cache_discovery=False) |
| 82 | + |
| 83 | + return self._conn |
| 84 | + |
| 85 | + def get_events( |
| 86 | + self, |
| 87 | + calendar_id: str = 'primary', |
| 88 | + i_cal_uid: Optional[str] = None, |
| 89 | + max_attendees: Optional[int] = None, |
| 90 | + max_results: Optional[int] = None, |
| 91 | + order_by: Optional[str] = None, |
| 92 | + private_extended_property: Optional[str] = None, |
| 93 | + q: Optional[str] = None, |
| 94 | + shared_extended_property: Optional[str] = None, |
| 95 | + show_deleted: Optional[bool] = False, |
| 96 | + show_hidden_invitation: Optional[bool] = False, |
| 97 | + single_events: Optional[bool] = False, |
| 98 | + sync_token: Optional[str] = None, |
| 99 | + time_max: Optional[datetime] = None, |
| 100 | + time_min: Optional[datetime] = None, |
| 101 | + time_zone: Optional[str] = None, |
| 102 | + updated_min: Optional[datetime] = None, |
| 103 | + ) -> list: |
| 104 | + """ |
| 105 | + Gets events from Google Calendar from a single calendar_id |
| 106 | + https://developers.google.com/calendar/api/v3/reference/events/list |
| 107 | +
|
| 108 | + :param calendar_id: The Google Calendar ID to interact with |
| 109 | + :type calendar_id: str |
| 110 | + :param i_cal_uid: Optional. Specifies event ID in the ``iCalendar`` format in the response. |
| 111 | + :type i_cal_uid: str |
| 112 | + :param max_attendees: Optional. If there are more than the specified number of attendees, |
| 113 | + only the participant is returned. |
| 114 | + :type max_attendees: int |
| 115 | + :param max_results: Optional. Maximum number of events returned on one result page. |
| 116 | + Incomplete pages can be detected by a non-empty ``nextPageToken`` field in the response. |
| 117 | + By default the value is 250 events. The page size can never be larger than 2500 events |
| 118 | + :type max_results: int |
| 119 | + :param order_by: Optional. Acceptable values are ``"startTime"`` or "updated" |
| 120 | + :type order_by: str |
| 121 | + :param private_extended_property: Optional. Extended properties constraint specified as |
| 122 | + ``propertyName=value``. Matches only private properties. This parameter might be repeated |
| 123 | + multiple times to return events that match all given constraints. |
| 124 | + :type private_extended_property: str |
| 125 | + :param q: Optional. Free text search. |
| 126 | + :type q: str |
| 127 | + :param shared_extended_property: Optional. Extended properties constraint specified as |
| 128 | + ``propertyName=value``. Matches only shared properties. This parameter might be repeated |
| 129 | + multiple times to return events that match all given constraints. |
| 130 | + :type shared_extended_property: str |
| 131 | + :param show_deleted: Optional. False by default |
| 132 | + :type show_deleted: bool |
| 133 | + :param show_hidden_invitation: Optional. False by default |
| 134 | + :type show_hidden_invitation: bool |
| 135 | + :param single_events: Optional. False by default |
| 136 | + :type single_events: bool |
| 137 | + :param sync_token: Optional. Token obtained from the ``nextSyncToken`` field returned |
| 138 | + :type sync_token: str |
| 139 | + :param time_max: Optional. Upper bound (exclusive) for an event's start time to filter by. |
| 140 | + Default is no filter |
| 141 | + :type time_max: datetime |
| 142 | + :param time_min: Optional. Lower bound (exclusive) for an event's end time to filter by. |
| 143 | + Default is no filter |
| 144 | + :type time_min: datetime |
| 145 | + :param time_zone: Optional. Time zone used in response. Default is calendars time zone. |
| 146 | + :type time_zone: str |
| 147 | + :param updated_min: Optional. Lower bound for an event's last modification time |
| 148 | + :type updated_min: datetime |
| 149 | + :rtype: List |
| 150 | + """ |
| 151 | + service = self.get_conn() |
| 152 | + page_token = None |
| 153 | + events = [] |
| 154 | + while True: |
| 155 | + response = ( |
| 156 | + service.events() |
| 157 | + .list( |
| 158 | + calendarId=calendar_id, |
| 159 | + iCalUID=i_cal_uid, |
| 160 | + maxAttendees=max_attendees, |
| 161 | + maxResults=max_results, |
| 162 | + orderBy=order_by, |
| 163 | + pageToken=page_token, |
| 164 | + privateExtendedProperty=private_extended_property, |
| 165 | + q=q, |
| 166 | + sharedExtendedProperty=shared_extended_property, |
| 167 | + showDeleted=show_deleted, |
| 168 | + showHiddenInvitations=show_hidden_invitation, |
| 169 | + singleEvents=single_events, |
| 170 | + syncToken=sync_token, |
| 171 | + timeMax=time_max, |
| 172 | + timeMin=time_min, |
| 173 | + timeZone=time_zone, |
| 174 | + updatedMin=updated_min, |
| 175 | + ) |
| 176 | + .execute(num_retries=self.num_retries) |
| 177 | + ) |
| 178 | + events.extend(response["items"]) |
| 179 | + page_token = response.get("nextPageToken") |
| 180 | + if not page_token: |
| 181 | + break |
| 182 | + return events |
| 183 | + |
| 184 | + def create_event( |
| 185 | + self, |
| 186 | + event: Dict[str, Any], |
| 187 | + calendar_id: str = 'primary', |
| 188 | + conference_data_version: Optional[int] = 0, |
| 189 | + max_attendees: Optional[int] = None, |
| 190 | + send_notifications: Optional[bool] = False, |
| 191 | + send_updates: Optional[str] = 'false', |
| 192 | + supports_attachments: Optional[bool] = False, |
| 193 | + ) -> dict: |
| 194 | + """ |
| 195 | + Create event on the specified calendar |
| 196 | + https://developers.google.com/calendar/api/v3/reference/events/insert |
| 197 | +
|
| 198 | + :param calendar_id: The Google Calendar ID to interact with |
| 199 | + :type calendar_id: str |
| 200 | + :param conference_data_version: Optional. Version number of conference data |
| 201 | + supported by the API client. |
| 202 | + :type conference_data_version: int |
| 203 | + :param max_attendees: Optional. If there are more than the specified number of attendees, |
| 204 | + only the participant is returned. |
| 205 | + :type max_attendees: int |
| 206 | + :param send_notifications: Optional. Default is False |
| 207 | + :type send_notifications: bool |
| 208 | + :param send_updates: Optional. Default is "false". Acceptable values as "all", "none", |
| 209 | + ``"externalOnly"`` |
| 210 | + :type send_updates: str |
| 211 | + :type supports_attachments: Optional. Default is False |
| 212 | + :type supports_attachments: bool |
| 213 | + :type event: Required. Request body of Events resource. Start and End are required |
| 214 | + https://developers.google.com/calendar/api/v3/reference/events#resource |
| 215 | + :type event: dict |
| 216 | + :rtype: Dict |
| 217 | + """ |
| 218 | + if "start" not in event or "end" not in event: |
| 219 | + raise AirflowException( |
| 220 | + f"start and end must be specified in the event body while creating an event. API docs:" |
| 221 | + f"https://developers.google.com/calendar/api/{self.api_version}/reference/events/insert " |
| 222 | + ) |
| 223 | + service = self.get_conn() |
| 224 | + |
| 225 | + response = ( |
| 226 | + service.events() |
| 227 | + .insert( |
| 228 | + calendarId=calendar_id, |
| 229 | + conferenceDataVersion=conference_data_version, |
| 230 | + maxAttendees=max_attendees, |
| 231 | + sendNotifications=send_notifications, |
| 232 | + sendUpdates=send_updates, |
| 233 | + supportsAttachments=supports_attachments, |
| 234 | + body=event, |
| 235 | + ) |
| 236 | + .execute(num_retries=self.num_retries) |
| 237 | + ) |
| 238 | + |
| 239 | + return response |
0 commit comments