Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
# Change Log
# Changelog

All notable changes to this project will be documented here.

## [3.0.0] - 2024-05-14
### Added
- Type hints for most functions
### Changed
- Now requires Python 3.11+

## [2.0.0] - 2019-10-14
### Changed
- When requesting a single resource using the dictionary way, only a single
Expand Down
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,29 @@ In order to handle pagination calls to API are done inside a generator.
As a consequence, even post and deletes have to be "nexted" if using the *hit_*
method.

## Requirements

Pythom 3.11+, `2.0.0` was the last version to support Python 2.7-3.10

## Installation

The package can be installed cloning the repository and doing
`python setup.py install` or `pip install .`.
Using PyPI into a [venv](https://docs.python.org/3/library/venv.html):
```bash
pip install python-helpscout-v2 --require-virtualenv`
```

Manually by cloning the repository and executing pip into a [venv](https://docs.python.org/3/library/venv.html):
```bash
pip install . --require-virtualenv`
```

It can also be install from pypi.org doing `pip install python-helpscout-v2`.

## Authentication

In order to use the API you need an app id and app secret.

More about credentials can be found in
[helpscout's documentation](https://developer.helpscout.com/mailbox-api/overview/authentication/).
[Help Scout's documentation](https://developer.helpscout.com/mailbox-api/overview/authentication/).

## General use

Expand Down
98 changes: 55 additions & 43 deletions helpscout/client.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
# 3.13 defaults to this, TODO remove
from __future__ import annotations

import logging
import time
import requests
import typing
from typing import overload

from functools import partial
try: # Python 3
from urllib.parse import urljoin
except ImportError: # Python 2
from urlparse import urljoin
if typing.TYPE_CHECKING:
from typing import Callable, Literal
from collections.abc import Generator

import requests
from functools import partial
from urllib.parse import urljoin

from helpscout.exceptions import (HelpScoutException,
HelpScoutAuthenticationException,
Expand All @@ -22,10 +27,10 @@

class HelpScout:

def __init__(self, app_id, app_secret,
base_url='https://api.helpscout.net/v2/',
sleep_on_rate_limit_exceeded=True,
rate_limit_sleep=10):
def __init__(self, app_id:str, app_secret:str,
base_url:str='https://api.helpscout.net/v2/',
sleep_on_rate_limit_exceeded:bool=True,
rate_limit_sleep:int=10):
"""Help Scout API v2 client wrapper.

The app credentials are created on the My App section in your profile.
Expand All @@ -40,7 +45,7 @@ def __init__(self, app_id, app_secret,
The application secret.
base_url: str
The API's base url.
sleep_on_rate_limit_exceeded: Boolean
sleep_on_rate_limit_exceeded: bool
True to sleep and retry on rate limits exceeded.
Otherwise raises an HelpScoutRateLimitExceededException exception.
rate_limit_sleep: int
Expand All @@ -54,7 +59,7 @@ def __init__(self, app_id, app_secret,
self.rate_limit_sleep = rate_limit_sleep
self.access_token = None

def __getattr__(self, endpoint):
def __getattr__(self, endpoint:str) -> HelpScoutEndpointRequester:
"""Returns a request to hit the API in a nicer way. E.g.:
> client = HelpScout(app_id='asdasd', app_secret='1021')
> client.conversations.get()
Expand All @@ -76,19 +81,18 @@ def __getattr__(self, endpoint):
"""
return HelpScoutEndpointRequester(self, endpoint, False)

def get_objects(self, endpoint, resource_id=None, params=None,
specific_resource=False):
def get_objects(self, endpoint:str, resource_id:int|str|None=None, params:dict|str|None=None, specific_resource:bool=False) -> HelpScoutObject|list[HelpScoutObject]:
"""Returns the objects from the endpoint filtering by the parameters.

Parameters
----------
endpoint: str
One of the endpoints in the API. E.g.: conversations, mailboxes.
resource_id: int or str or None
resource_id: int | str | None
The id of the resource in the endpoint to query.
E.g.: in "GET /v2/conversations/123 HTTP/1.1" the id would be 123.
If None is provided, nothing will be done
params: dict or str or None
params: dict | str | None
Dictionary with the parameters to send to the url.
Or the parameters already un url format.
specific_resource: bool
Expand All @@ -98,17 +102,16 @@ def get_objects(self, endpoint, resource_id=None, params=None,

Returns
-------
[HelpScoutObject]
HelpScoutObject | list[HelpScoutObject]
A list of objects returned by the api.
"""
cls = HelpScoutObject.cls(endpoint, endpoint)
results = cls.from_results(
self.hit_(endpoint, 'get', resource_id, params=params))
results:list[HelpScoutObject] = cls.from_results( self.hit_(endpoint, 'get', resource_id, params=params) )
if resource_id is not None or specific_resource:
return results[0]
return results

def hit(self, endpoint, method, resource_id=None, data=None, params=None):
def hit(self, endpoint:str, method:str, resource_id:int|str|None=None, data:dict|None=None, params:dict|str|None=None) -> list[dict|None]:
"""Hits the api and returns all the data.
If several calls are needed due to pagination, control won't be
returned to the caller until all is retrieved.
Expand All @@ -120,26 +123,26 @@ def hit(self, endpoint, method, resource_id=None, data=None, params=None):
method: str
The http method to hit the endpoint with.
One of {'get', 'post', 'put', 'patch', 'delete', 'head', 'options'}
resource_id: int or str or None
resource_id: int | str | None
The id of the resource in the endpoint to query.
E.g.: in "GET /v2/conversations/123 HTTP/1.1" the id would be 123.
If None is provided, nothing will be done
data: dict or None
dict: dict | None
A dictionary with the data to send to the API as json.
params: dict or str or None
params: dict | str | None
Dictionary with the parameters to send to the url.
Or the parameters already un url format.

Returns
-------
[dict] or [None]
dict: when several objects are received from the API, a list of
list[dict] | list[None]
list: when several objects are received from the API, a list of
dictionaries with HelpScout's _embedded data will be returned
None if http 201 created or 204 no content are received.
"""
return list(self.hit_(endpoint, method, resource_id, data, params))

def hit_(self, endpoint, method, resource_id=None, data=None, params=None):
def hit_(self, endpoint:str, method:str, resource_id:int|str|None=None, data:dict|None=None, params:dict|str|None=None) -> Generator[dict|None, None, None]:
"""Hits the api and yields the data.

Parameters
Expand All @@ -149,19 +152,19 @@ def hit_(self, endpoint, method, resource_id=None, data=None, params=None):
method: str
The http method to hit the endpoint with.
One of {'get', 'post', 'put', 'patch', 'delete', 'head', 'options'}
resource_id: int or str or None
resource_id: int | str | None
The id of the resource in the endpoint to query.
E.g.: in "GET /v2/conversations/123 HTTP/1.1" the id would be 123.
If None is provided, nothing will be done
data: dict or None
data: dict | None
A dictionary with the data to send to the API as json.
params: dict or str or None
params: dict | str | None
Dictionary with the parameters to send to the url.
Or the parameters already un url format.

Yields
------
dict or None
dict | None
Dictionary with HelpScout's _embedded data.
None if http 201 created or 204 no content are received.
"""
Expand Down Expand Up @@ -197,7 +200,7 @@ def hit_(self, endpoint, method, resource_id=None, data=None, params=None):
else:
raise HelpScoutException(r.text)

def _results_with_pagination(self, response, method):
def _results_with_pagination(self, response:dict, method:str) -> Generator[dict, None, None]:
"""Requests and yields pagination results.

Parameters
Expand Down Expand Up @@ -243,8 +246,7 @@ def _results_with_pagination(self, response, method):
raise HelpScoutException(r.text)

def _authenticate(self):
"""Authenticates with the API and gets a token for subsequent requests.
"""
"""Authenticates with the API and gets a token for subsequent requests."""
url = urljoin(self.base_url, 'oauth2/token')
data = {
'grant_type': 'client_credentials',
Expand All @@ -261,6 +263,8 @@ def _authenticate(self):

def _authentication_headers(self):
"""Returns authentication headers."""
if self.access_token is None:
raise HelpScoutAuthenticationException('Tried to get access_token without prior authentication')
return {
'Authorization': 'Bearer ' + self.access_token,
'content-type': 'application/json',
Expand All @@ -287,7 +291,7 @@ def __eq__(self, other):
self.sleep_on_rate_limit_exceeded ==
other.sleep_on_rate_limit_exceeded)

def __repr__(self):
def __repr__(self) -> str:
"""Returns the object as a string."""
name = self.__class__.__name__
attrs = (
Expand All @@ -307,12 +311,12 @@ def __repr__(self):

class HelpScoutEndpointRequester:

def __init__(self, client, endpoint, specific_resource):
def __init__(self, client:HelpScout, endpoint:str, specific_resource:bool):
"""Client wrapper to perform requester.get/post/put/patch/delete.

Parameters
----------
client: HelpScoutClient
client: HelpScout
A help scout client instance to query the API.
endpoint: str
One of the endpoints in the API. E.g.: conversations, mailboxes.
Expand All @@ -324,7 +328,13 @@ def __init__(self, client, endpoint, specific_resource):
self.endpoint = endpoint
self.specific_resource = specific_resource

def __getattr__(self, method):
@overload
def __getattr__(self, method: Literal['get']) -> Callable: ...

@overload
def __getattr__(self, method:str) -> Callable | partial | HelpScoutEndpointRequester: ...

def __getattr__(self, method:str) -> Callable | partial | HelpScoutEndpointRequester:
"""Catches http methods like get, post, patch, put and delete.
Returns a subrequester when methods not named after http methods are
requested, as this are considered attributes of the main object, like
Expand All @@ -337,8 +347,8 @@ def __getattr__(self, method):

Returns
-------
client.get_objects return value for the *get* method.
client.hit return value for other http named methods.
Callable - client.get_objects return value for the *get* method.
partial - client.hit return value for other http named methods.
HelpScoutEndpointRequester when other attributes are accessed, this is
expected to be used mainly for subattributes of an endpoint or
subendpoints of specific resources, like tags from a conversation.
Expand All @@ -359,7 +369,7 @@ def __getattr__(self, method):
False,
)

def __getitem__(self, resource_id):
def __getitem__(self, resource_id:int|str) -> HelpScoutEndpointRequester:
"""Returns a second endpoint requester extending the endpoint to a
specific resource_id or resource_name.

Expand All @@ -368,7 +378,7 @@ def __getitem__(self, resource_id):

Parameters
----------
resource_id: int or str
resource_id: int | str
The resource id or attribute available in the API through a
specific call.

Expand All @@ -384,7 +394,7 @@ def __getitem__(self, resource_id):
True,
)

def _yielded_function(self, method, *args, **kwargs):
def _yielded_function(self, method:str, *args, **kwargs):
"""Calls a generator function and calls next.
It is intended to be used with post, put, patch and delete which do not
return objects, but as hit is a generator, still have to be nexted.
Expand All @@ -395,6 +405,8 @@ def _yielded_function(self, method, *args, **kwargs):
Positional arguments after *method* to forward to client.hit .
*kwargs: keyword arguments
Keyword arguments after *method* to forward to client.hit.
method: str
Inherited

Returns
-------
Expand All @@ -408,7 +420,7 @@ def __eq__(self, other):
self.endpoint == other.endpoint and
self.client == other.client)

def __repr__(self):
def __repr__(self) -> str:
"""Returns the object as a string."""
name = self.__class__.__name__
return '%s(app_id="%s", endpoint="%s")' % (
Expand Down
Loading