blob: 83078e7f758457bdd3f0b9f2b0b90bb47782e1e1 [file]
"""Slack source helpers."""
from typing import Any, Generator, Iterable, List, Optional
from urllib.parse import urljoin
import pendulum
from dlt.common.time import ensure_pendulum_datetime
from dlt.common.typing import Dict, TAnyDateTime, TDataItem
from dlt.sources.helpers import requests
from jsonpath_ng.ext import parse # type: ignore
from .settings import MAX_PAGE_SIZE, SLACK_API_URL
class SlackApiException(Exception):
"""Slack api exception."""
class PaidOnlyException(SlackApiException):
"""Slack api exception."""
def extract_jsonpath(
expression: str,
json_data: TDataItem,
) -> Generator[Any, None, None]:
"""Extract records from an input based on a JSONPath expression."""
if not expression:
yield json_data
return
jsonpath = parse(expression)
for match in jsonpath.find(json_data):
yield match.value
def update_jsonpath(expression: str, json_data: TDataItem, value: Any) -> Any:
"""Update a record in an input based on a JSONPath expression."""
jsonpath = parse(expression)
return jsonpath.update_or_create(json_data, value)
def ensure_dt_type(dt: TAnyDateTime, to_ts: bool = False) -> Any:
"""Converts a datetime to a pendulum datetime or timestamp.
Args:
dt: The datetime to convert.
to_ts: Whether to convert to a timestamp or not.
Returns:
Any: The converted datetime or timestamp.
"""
if dt is None:
return None
out_dt = ensure_pendulum_datetime(dt)
if to_ts:
return out_dt.timestamp()
return out_dt
class SlackAPI:
"""
A Slack API client that can be used to get pages of data from Slack.
"""
def __init__(
self,
access_token: str,
page_size: int = MAX_PAGE_SIZE,
) -> None:
"""
Args:
access_token: The private app password to the app on your shop.
page_size: The max number of items to fetch per page. Defaults to 1000.
"""
self.access_token = access_token
self.page_size = page_size
@property
def headers(self) -> Dict[str, str]:
"""Generate the headers to use for the request."""
return {"Authorization": f"Bearer {self.access_token}"}
def parameters(
self, params: Optional[Dict[str, Any]] = None, next_cursor: str = None
) -> Dict[str, str]:
"""
Generate the query parameters to use for the request.
Args:
params: The query parameters to include in the request.
next_cursor: The cursor to use to get the next page of results.
Returns:
The query parameters to use for the request.
"""
params = params or {}
params["limit"] = self.page_size
if next_cursor:
params["cursor"] = next_cursor
return params
def url(self, resource: str) -> str:
"""
Generate the URL to use for the request.
Args:
resource: The resource to get pages for (e.g. conversations.list).
"""
return urljoin(SLACK_API_URL, resource)
def _get_next_cursor(self, response: Dict[str, Any]) -> Any:
"""
Get the next cursor from the response.
Args:
response: The response from the Slack API.
"""
cursor_jsonpath = "$.response_metadata.next_cursor"
return next(extract_jsonpath(cursor_jsonpath, response), None)
def _convert_datetime_fields(
self, item: Dict[str, Any], datetime_fields: List[str]
) -> Dict[str, Any]:
"""Convert timestamp fields in the item to pendulum datetime objects.
The item is modified in place.
Args:
item: The item to convert
datetime_fields: List of fields to convert to pendulum datetime objects.
Returns:
The same data item (for convenience)
"""
if not datetime_fields:
return item
for field in datetime_fields:
if timestamp := next(extract_jsonpath(field, item), None):
if isinstance(timestamp, str):
timestamp = float(timestamp)
if timestamp > 1e10:
timestamp = timestamp / 1000
pendulum_dt = pendulum.from_timestamp(timestamp)
item = update_jsonpath(field, item, pendulum_dt)
return item
def get_pages(
self,
resource: str,
response_path: str = None,
params: Dict[str, Any] = None,
datetime_fields: List[str] = None,
context: Dict[str, Any] = None,
) -> Iterable[TDataItem]:
"""Get all pages from slack using requests.
Iterates through all pages and yield each page items.\
Args:
resource: The resource to get pages for (e.g. conversations.list).
response_path: The path to the list of items in the response JSON.
params: Query params to include in the request.
datetime_fields: List of fields to convert to pendulum datetime objects.
context: Additional context to add to each item.
Yields:
List of data items from the page
"""
has_next_page = True
next_cursor = None
# Iterate through all pages
while has_next_page:
# Make the request
response = requests.get(
url=self.url(resource),
headers=self.headers,
params=self.parameters(params or {}, next_cursor),
)
json_response = response.json()
# Stop if there was an error
if not json_response.get("ok"):
has_next_page = False
error = json_response.get("error")
if error == "paid_only":
raise PaidOnlyException("This resource is just available on paid accounts.")
else:
raise SlackApiException(error)
# Yield the page converting datetime fields
output = []
for item in extract_jsonpath(response_path, json_response):
item = self._convert_datetime_fields(item, datetime_fields)
item.update(context or {})
output.append(item)
yield output
# Get the next cursor
next_cursor = self._get_next_cursor(json_response)
if not next_cursor:
has_next_page = False