[Python] Zendesk APIで HelpCenterの記事を検索するサンプル

はじめに
======

Zendesk APIを使って、HelpCenterの記事を検索して取得するサンプルです。今回は認証にはAPI Tokenを使います。
あまりWebに情報が無いので、参考になればと思い書き残しておきます。
API利用開始までの手順は、本当はもっとあるのかもしれないですが、私の把握している範囲で書いています。

サンプルの環境
-----------

- Python >= 3.6
- requests

周辺知識
--------
- Zendesk APIは、一般的なREST & JSON というAPI。
- 認証には 「メールアドレス」「API Token」が必要です。他にも「パスワード」「OAuth」による認証方式があります。
- 「subdomain」を把握しておく必要があります。
    - 独自ドメインの場合、パット見のURLのHostとは違うので注意しましょう(私はこれが間違っててずっと 404エラーが出ていた...)。
- APIは support API と help center API で何か違うらしい(検索される対象が違うようだ)
    - 認証方法などは共通のようだ

参考URL
---------

- [APIの利用開始とAPI Tokenの発行](https://support.zendesk.com/hc/ja/articles/115002555167-API%E3%83%80%E3%83%83%E3%82%B7%E3%83%A5%E3%83%9C%E3%83%BC%E3%83%89%E3%81%AE%E4%BD%BF%E7%94%A8)
- [API Tokenによる認証方法](https://developer.zendesk.com/rest_api/docs/support/introduction#api-token)
- [Help Center API](https://developer.zendesk.com/rest_api/docs/help_center/introduction)

準備
====

API Tokenを使うために、**APIの利用開始とAPI Tokenの発行** を行います(参考URL参照)。

準備はこれくらいです。

コード
=====
こんな感じで使えました。

```python
from typing import List
import base64
from dataclasses import dataclass, field, fields

import requests


class ZendeskAPI:
    def __init__(self, sub_domain, email, password=None, api_token=None):
        assert password is not None or api_token is not None, 'please specify password or api_token'
        self.sub_domain = sub_domain
        self.email = email
        self.password = password
        self.api_token = api_token

    @property
    def api_host(self):
        return f'{self.sub_domain}.zendesk.com'

    @property
    def authorization_header_value(self):
        if self.password:
            content = f'{self.email}:{self.password}'
        else:
            content = f'{self.email}/token:{self.api_token}'
        return base64.b64encode(content.encode()).decode()

    def request(self, path, method="GET", params=None, data=None, json=None, retry=3):
        url = f'https://{self.api_host}{path}'
        headers = {
            'Authorization': f'Basic {self.authorization_header_value}',
        }

        response = None
        for _ in range(retry):
            response = requests.request(method, url, params=params, data=data, json=json, headers=headers)
            if response.status_code == 200:
                return response
            elif response.status_code < 500:
                break
        if response is not None:
            response.raise_for_status()

    def search_help_articles(self, keywords: List[str]):
        path = '/api/v2/help_center/articles/search.json'
        params = {'query': ",".join(keywords)}
        response = self.request(path, params=params).json()
        article_list = []
        for result in response.get('results') or []:
            article_list.append(populate(ZendeskArticle, result))
        return article_list


def populate(cls: dataclass, data, **kw):
    kwargs = {}
    dd = {}
    dd.update(data)
    dd.update(kw)
    for k in fields(cls):
        key = k.name
        kwargs[key] = dd.get(key)
    return cls(**kwargs)


@dataclass
class ZendeskArticle:
    id: str
    name: str
    title: str
    body: str = field(repr=False)
    label_names: List[str]
    url: str
    html_url: str
    author_id: str
    vote_sum: int = 0


api = ZendeskAPI(ZENDESK_SUB_DOMAIN, ZENDESK_EMAIL, api_token=ZENDESK_API_TOKEN)
articles = api.search_help_articles(['事例'])

for a in articles:
    print(a)
```

↓

```
ZendeskArticle(id=8888888, name='提案時のアイデア集', title='提案時のアイデア集', label_names=['事例', 'アイデア', '提案'], url='https://...json', html_url='https://...%E9%9B%86', author_id=99999999, vote_sum=1)
ZendeskArticle(id=888888, ...)
...
```

あとは煮るなり焼くなり、、、ですね。

さいごに
======
小ネタでした。