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

Switch from zdesk to zenpy in ZendeskHook #21349

Merged
merged 4 commits into from
Feb 6, 2022
Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 1 addition & 1 deletion airflow/hooks/zendesk_hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

import warnings

from airflow.providers.zendesk.hooks.zendesk import Zendesk, ZendeskError, ZendeskHook # noqa
from airflow.providers.zendesk.hooks.zendesk import ZendeskHook # noqa

warnings.warn(
"This module is deprecated. Please use `airflow.providers.zendesk.hooks.zendesk`.",
Expand Down
13 changes: 13 additions & 0 deletions airflow/providers/zendesk/CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,19 @@
Changelog
---------

3.0.0
.....

Misc
~~~
``ZendeskHook`` moved from using ``zdesk`` to ``zenpy`` package.

Breaking changes
~~~~~~~~~~~~~~~~
Changed the return type of ``ZendeskHook.get_conn`` to return a ``zenpy.Zenpy`` object instead of a ``zdesk.Zendesk`` object.
Deleted the ``ZendeskHook.call``, alternatively you can use the ``ZendeskHook.get`` method to make custom get calls to Zendesk API.
``Zendesk`` and ``ZendeskError`` classes are removed from ``airflow.hooks.zendesk_hook`` imports.

2.0.1
.....

Expand Down
16 changes: 16 additions & 0 deletions airflow/providers/zendesk/example_dags/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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
#
# http://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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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
#
# http://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.

from datetime import datetime
from typing import Dict, List

from airflow import DAG
from airflow.operators.python import PythonOperator
from airflow.providers.zendesk.hooks.zendesk import ZendeskHook


def zendesk_custom_get_request() -> List[Dict]:
hook = ZendeskHook()
response = hook.get(
url="https://yourdomain.zendesk.com/api/v2/organizations.json",
)
return [org.to_dict() for org in response]


with DAG(
dag_id="zendesk_custom_get_dag",
schedule_interval=None,
start_date=datetime(2021, 1, 1),
catchup=False,
) as dag:
fetch_organizations = PythonOperator(
task_id="trigger_zendesk_hook",
python_callable=zendesk_custom_get_request,
)
168 changes: 84 additions & 84 deletions airflow/providers/zendesk/hooks/zendesk.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from typing import List, Optional, Tuple, Union

import time
from typing import Optional

from zdesk import RateLimitError, Zendesk, ZendeskError
from zenpy import Zenpy
from zenpy.lib.api import BaseApi
from zenpy.lib.api_objects import JobStatus, Ticket, TicketAudit
from zenpy.lib.generator import SearchResultGenerator

from airflow.hooks.base import BaseHook

Expand All @@ -31,90 +32,89 @@ class ZendeskHook(BaseHook):
:param zendesk_conn_id: The Airflow connection used for Zendesk credentials.
"""

def __init__(self, zendesk_conn_id: str) -> None:
conn_name_attr = 'zendesk_conn_id'
default_conn_name = 'zendesk_default'
conn_type = 'zendesk'
hook_name = 'Zendesk'

def __init__(self, zendesk_conn_id: str = default_conn_name) -> None:
super().__init__()
self.__zendesk_conn_id = zendesk_conn_id
self.__url = None
self.zendesk_conn_id = zendesk_conn_id
self.base_api: Optional[BaseApi] = None
zenpy_client, url = self._init_conn()
self.zenpy_client = zenpy_client
self.__url = url
self.get = self.zenpy_client.users._get

def _init_conn(self) -> Tuple[Zenpy, str]:
"""
Create the Zenpy Client for our Zendesk connection.

:return: zenpy.Zenpy client and the url for the API.
"""
conn = self.get_connection(self.zendesk_conn_id)
url = "https://" + conn.host
domain = conn.host
subdomain: Optional[str] = None
if conn.host.count(".") >= 2:
dot_splitted_string = conn.host.rsplit(".", 2)
subdomain = dot_splitted_string[0]
domain = ".".join(dot_splitted_string[1:])
return Zenpy(domain=domain, subdomain=subdomain, email=conn.login, password=conn.password), url

def get_conn(self) -> Zenpy:
"""
Get the underlying Zenpy client.

:return: zenpy.Zenpy client.
"""
return self.zenpy_client

def get_ticket(self, ticket_id: int) -> Ticket:
"""
Retrieve ticket.

def get_conn(self) -> Zendesk:
conn = self.get_connection(self.__zendesk_conn_id)
self.__url = "https://" + conn.host
return Zendesk(
zdesk_url=self.__url, zdesk_email=conn.login, zdesk_password=conn.password, zdesk_token=True
)
:return: Ticket object retrieved.
"""
return self.zenpy_client.tickets(id=ticket_id)

def search_tickets(self, **kwargs) -> SearchResultGenerator:
"""
Search tickets.

:param kwargs: (optional) Search fields given to the zenpy search method.
:return: SearchResultGenerator of Ticket objects.
"""
return self.zenpy_client.search(type='ticket', **kwargs)

def create_tickets(self, tickets: Union[Ticket, List[Ticket]], **kwargs) -> Union[TicketAudit, JobStatus]:
"""
Create tickets.

:param tickets: Ticket or List of Ticket to create.
:param kwargs: (optional) Additional fields given to the zenpy create method.
:return: A TicketAudit object containing information about the Ticket created.
When sending bulk request, returns a JobStatus object.
"""
return self.zenpy_client.tickets.create(tickets, **kwargs)

def __handle_rate_limit_exception(self, rate_limit_exception: ZendeskError) -> None:
def update_tickets(self, tickets: Union[Ticket, List[Ticket]], **kwargs) -> Union[TicketAudit, JobStatus]:
"""
Sleep for the time specified in the exception. If not specified, wait
for 60 seconds.
Update tickets.

:param tickets: Updated Ticket or List of Ticket object to update.
:param kwargs: (optional) Additional fields given to the zenpy update method.
:return: A TicketAudit object containing information about the Ticket updated.
When sending bulk request, returns a JobStatus object.
"""
retry_after = int(rate_limit_exception.response.headers.get('Retry-After', 60))
self.log.info("Hit Zendesk API rate limit. Pausing for %s seconds", retry_after)
time.sleep(retry_after)

def call(
self,
path: str,
query: Optional[dict] = None,
get_all_pages: bool = True,
side_loading: bool = False,
) -> dict:
return self.zenpy_client.tickets.update(tickets, **kwargs)

def delete_tickets(self, tickets: Union[Ticket, List[Ticket]], **kwargs) -> None:
"""
Call Zendesk API and return results

:param path: The Zendesk API to call
:param query: Query parameters
:param get_all_pages: Accumulate results over all pages before
returning. Due to strict rate limiting, this can often timeout.
Waits for recommended period between tries after a timeout.
:param side_loading: Retrieve related records as part of a single
request. In order to enable side-loading, add an 'include'
query parameter containing a comma-separated list of resources
to load. For more information on side-loading see
https://developer.zendesk.com/rest_api/docs/core/side_loading
Delete tickets, returns nothing on success and raises APIException on failure.

:param tickets: Ticket or List of Ticket to delete.
:param kwargs: (optional) Additional fields given to the zenpy delete method.
:return:
"""
query_params = query or {}
zendesk = self.get_conn()
first_request_successful = False

while not first_request_successful:
try:
results = zendesk.call(path, query_params)
first_request_successful = True
except RateLimitError as rle:
self.__handle_rate_limit_exception(rle)

# Find the key with the results
keys = [path.split("/")[-1].split(".json")[0]]
next_page = results['next_page']
if side_loading:
keys += query_params['include'].split(',')
results = {key: results[key] for key in keys}

if get_all_pages:
while next_page is not None:
try:
# Need to split because the next page URL has
# `github.zendesk...`
# in it, but the call function needs it removed.
next_url = next_page.split(self.__url)[1]
self.log.info("Calling %s", next_url)
more_res = zendesk.call(next_url)
for key in results:
results[key].extend(more_res[key])
if next_page == more_res['next_page']:
# Unfortunately zdesk doesn't always throw ZendeskError
# when we are done getting all the data. Sometimes the
# next just refers to the current set of results.
# Hence, need to deal with this special case
break
next_page = more_res['next_page']
except RateLimitError as rle:
self.__handle_rate_limit_exception(rle)
except ZendeskError as zde:
if b"Use a start_time older than 5 minutes" in zde.msg:
# We have pretty up to date data
break
raise zde

return results
return self.zenpy_client.tickets.delete(tickets, **kwargs)
1 change: 1 addition & 0 deletions docs/apache-airflow-providers-zendesk/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Content
:caption: References

Python API <_api/airflow/providers/zendesk/index>
Example DAGs <https://github.com/apache/airflow/tree/main/airflow/providers/zendesk/example_dags>
PyPI Repository <https://pypi.org/project/apache-airflow-providers-zendesk/>
Installing from sources <installing-providers-from-sources>

Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,7 @@ def _get_params(root_schema: dict, prefix: str = "", default_section: str = "")
'tenacity',
'vertica_python',
'winrm',
'zdesk',
'zenpy',
]

# The default options for autodoc directives. They are applied to all autodoc directives automatically.
Expand Down
6 changes: 6 additions & 0 deletions docs/spelling_wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ BackfillJobTest
Backfills
Banco
BaseClient
BaseObject
BaseOperator
BaseView
BaseXCom
Expand Down Expand Up @@ -202,6 +203,7 @@ Jira
JobComplete
JobExists
JobRunning
JobStatus
JobTrigger
Json
Jupyter
Expand Down Expand Up @@ -328,6 +330,7 @@ SSHTunnelForwarder
SaaS
Sagemaker
Sasl
SearchResultGenerator
SecretManagerClient
Seedlist
Sendgrid
Expand Down Expand Up @@ -382,6 +385,7 @@ Terraform
TextToSpeechClient
Tez
Thinknear
TicketAudit
ToC
Tooltip
Tunables
Expand Down Expand Up @@ -420,6 +424,7 @@ Yandex
Yieldr
Zego
Zendesk
Zenpy
Zsh
Zymergen
abc
Expand Down Expand Up @@ -1502,5 +1507,6 @@ youtrack
youtube
zA
zendesk
zenpy
zope
zsh
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,7 @@ def write_version(filename: str = os.path.join(*[my_dir, "airflow", "git_version
'yandexcloud>=0.122.0',
]
zendesk = [
'zdesk',
'zenpy>=2.0.24',
]
# End dependencies group

Expand Down
Loading