Skip to content

Commit 07b2849

Browse files
committed
Add typehints for most functions, require Python 3.11+
Fix wrong documentation for types/names Remove python setup.py install instructions and add virtualenv req to pip Fixes santiher#6
1 parent d1cfba1 commit 07b2849

File tree

6 files changed

+81
-59
lines changed

6 files changed

+81
-59
lines changed

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1-
# Change Log
1+
# Changelog
22

33
All notable changes to this project will be documented here.
44

5+
## [3.0.0] - 2024-05-14
6+
### Added
7+
- Type hints for most functions
8+
### Changed
9+
- Now requires Python 3.11+
10+
511
## [2.0.0] - 2019-10-14
612
### Changed
713
- When requesting a single resource using the dictionary way, only a single

README.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,29 @@ In order to handle pagination calls to API are done inside a generator.
1414
As a consequence, even post and deletes have to be "nexted" if using the *hit_*
1515
method.
1616

17+
## Requirements
18+
19+
Pythom 3.11+, `2.0.0` was the last version to support Python 2.7-3.10
20+
1721
## Installation
1822

19-
The package can be installed cloning the repository and doing
20-
`python setup.py install` or `pip install .`.
23+
Using PyPI into a [venv](https://docs.python.org/3/library/venv.html):
24+
```bash
25+
pip install python-helpscout-v2 --require-virtualenv`
26+
```
27+
28+
Manually by cloning the repository and executing pip into a [venv](https://docs.python.org/3/library/venv.html):
29+
```bash
30+
pip install . --require-virtualenv`
31+
```
2132

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

2434
## Authentication
2535

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

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

3141
## General use
3242

helpscout/client.py

Lines changed: 38 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import logging
22
import time
3+
import requests
4+
import typing
5+
if typing.TYPE_CHECKING:
6+
from helpscout import HelpScout
7+
from collections.abc import Generator
8+
from typing import Self
39

410
from functools import partial
5-
try: # Python 3
6-
from urllib.parse import urljoin
7-
except ImportError: # Python 2
8-
from urlparse import urljoin
9-
10-
import requests
11+
from urllib.parse import urljoin
1112

1213
from helpscout.exceptions import (HelpScoutException,
1314
HelpScoutAuthenticationException,
@@ -22,10 +23,10 @@
2223

2324
class HelpScout:
2425

25-
def __init__(self, app_id, app_secret,
26-
base_url='https://api.helpscout.net/v2/',
27-
sleep_on_rate_limit_exceeded=True,
28-
rate_limit_sleep=10):
26+
def __init__(self, app_id:str, app_secret:str,
27+
base_url:str='https://api.helpscout.net/v2/',
28+
sleep_on_rate_limit_exceeded:bool=True,
29+
rate_limit_sleep:int=10):
2930
"""Help Scout API v2 client wrapper.
3031
3132
The app credentials are created on the My App section in your profile.
@@ -40,7 +41,7 @@ def __init__(self, app_id, app_secret,
4041
The application secret.
4142
base_url: str
4243
The API's base url.
43-
sleep_on_rate_limit_exceeded: Boolean
44+
sleep_on_rate_limit_exceeded: bool
4445
True to sleep and retry on rate limits exceeded.
4546
Otherwise raises an HelpScoutRateLimitExceededException exception.
4647
rate_limit_sleep: int
@@ -54,7 +55,7 @@ def __init__(self, app_id, app_secret,
5455
self.rate_limit_sleep = rate_limit_sleep
5556
self.access_token = None
5657

57-
def __getattr__(self, endpoint):
58+
def __getattr__(self, endpoint:str) -> 'HelpScoutEndpointRequester':
5859
"""Returns a request to hit the API in a nicer way. E.g.:
5960
> client = HelpScout(app_id='asdasd', app_secret='1021')
6061
> client.conversations.get()
@@ -76,19 +77,18 @@ def __getattr__(self, endpoint):
7677
"""
7778
return HelpScoutEndpointRequester(self, endpoint, False)
7879

79-
def get_objects(self, endpoint, resource_id=None, params=None,
80-
specific_resource=False):
80+
def get_objects(self, endpoint:str, resource_id:int|str|None=None, params:dict|str|None=None, specific_resource:bool=False) -> HelpScoutObject|list[HelpScoutObject]:
8181
"""Returns the objects from the endpoint filtering by the parameters.
8282
8383
Parameters
8484
----------
8585
endpoint: str
8686
One of the endpoints in the API. E.g.: conversations, mailboxes.
87-
resource_id: int or str or None
87+
resource_id: int | str | None
8888
The id of the resource in the endpoint to query.
8989
E.g.: in "GET /v2/conversations/123 HTTP/1.1" the id would be 123.
9090
If None is provided, nothing will be done
91-
params: dict or str or None
91+
params: dict | str | None
9292
Dictionary with the parameters to send to the url.
9393
Or the parameters already un url format.
9494
specific_resource: bool
@@ -98,17 +98,17 @@ def get_objects(self, endpoint, resource_id=None, params=None,
9898
9999
Returns
100100
-------
101-
[HelpScoutObject]
101+
HelpScoutObject | list[HelpScoutObject]
102102
A list of objects returned by the api.
103103
"""
104104
cls = HelpScoutObject.cls(endpoint, endpoint)
105-
results = cls.from_results(
105+
results:list[HelpScoutObject] = cls.from_results(
106106
self.hit_(endpoint, 'get', resource_id, params=params))
107107
if resource_id is not None or specific_resource:
108108
return results[0]
109109
return results
110110

111-
def hit(self, endpoint, method, resource_id=None, data=None, params=None):
111+
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]:
112112
"""Hits the api and returns all the data.
113113
If several calls are needed due to pagination, control won't be
114114
returned to the caller until all is retrieved.
@@ -120,26 +120,26 @@ def hit(self, endpoint, method, resource_id=None, data=None, params=None):
120120
method: str
121121
The http method to hit the endpoint with.
122122
One of {'get', 'post', 'put', 'patch', 'delete', 'head', 'options'}
123-
resource_id: int or str or None
123+
resource_id: int | str | None
124124
The id of the resource in the endpoint to query.
125125
E.g.: in "GET /v2/conversations/123 HTTP/1.1" the id would be 123.
126126
If None is provided, nothing will be done
127-
data: dict or None
127+
dict: dict | None
128128
A dictionary with the data to send to the API as json.
129-
params: dict or str or None
129+
params: dict | str | None
130130
Dictionary with the parameters to send to the url.
131131
Or the parameters already un url format.
132132
133133
Returns
134134
-------
135-
[dict] or [None]
136-
dict: when several objects are received from the API, a list of
135+
list[dict] | list[None]
136+
list: when several objects are received from the API, a list of
137137
dictionaries with HelpScout's _embedded data will be returned
138138
None if http 201 created or 204 no content are received.
139139
"""
140140
return list(self.hit_(endpoint, method, resource_id, data, params))
141141

142-
def hit_(self, endpoint, method, resource_id=None, data=None, params=None):
142+
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]:
143143
"""Hits the api and yields the data.
144144
145145
Parameters
@@ -149,19 +149,19 @@ def hit_(self, endpoint, method, resource_id=None, data=None, params=None):
149149
method: str
150150
The http method to hit the endpoint with.
151151
One of {'get', 'post', 'put', 'patch', 'delete', 'head', 'options'}
152-
resource_id: int or str or None
152+
resource_id: int | str | None
153153
The id of the resource in the endpoint to query.
154154
E.g.: in "GET /v2/conversations/123 HTTP/1.1" the id would be 123.
155155
If None is provided, nothing will be done
156-
data: dict or None
156+
data: dict | None
157157
A dictionary with the data to send to the API as json.
158-
params: dict or str or None
158+
params: dict | str | None
159159
Dictionary with the parameters to send to the url.
160160
Or the parameters already un url format.
161161
162162
Yields
163163
------
164-
dict or None
164+
dict | None
165165
Dictionary with HelpScout's _embedded data.
166166
None if http 201 created or 204 no content are received.
167167
"""
@@ -197,7 +197,7 @@ def hit_(self, endpoint, method, resource_id=None, data=None, params=None):
197197
else:
198198
raise HelpScoutException(r.text)
199199

200-
def _results_with_pagination(self, response, method):
200+
def _results_with_pagination(self, response:dict, method:str) -> Generator[dict, None, None]:
201201
"""Requests and yields pagination results.
202202
203203
Parameters
@@ -243,8 +243,7 @@ def _results_with_pagination(self, response, method):
243243
raise HelpScoutException(r.text)
244244

245245
def _authenticate(self):
246-
"""Authenticates with the API and gets a token for subsequent requests.
247-
"""
246+
"""Authenticates with the API and gets a token for subsequent requests."""
248247
url = urljoin(self.base_url, 'oauth2/token')
249248
data = {
250249
'grant_type': 'client_credentials',
@@ -287,7 +286,7 @@ def __eq__(self, other):
287286
self.sleep_on_rate_limit_exceeded ==
288287
other.sleep_on_rate_limit_exceeded)
289288

290-
def __repr__(self):
289+
def __repr__(self) -> str:
291290
"""Returns the object as a string."""
292291
name = self.__class__.__name__
293292
attrs = (
@@ -307,12 +306,12 @@ def __repr__(self):
307306

308307
class HelpScoutEndpointRequester:
309308

310-
def __init__(self, client, endpoint, specific_resource):
309+
def __init__(self, client:HelpScout, endpoint:str, specific_resource:bool):
311310
"""Client wrapper to perform requester.get/post/put/patch/delete.
312311
313312
Parameters
314313
----------
315-
client: HelpScoutClient
314+
client: HelpScout
316315
A help scout client instance to query the API.
317316
endpoint: str
318317
One of the endpoints in the API. E.g.: conversations, mailboxes.
@@ -324,7 +323,7 @@ def __init__(self, client, endpoint, specific_resource):
324323
self.endpoint = endpoint
325324
self.specific_resource = specific_resource
326325

327-
def __getattr__(self, method):
326+
def __getattr__(self, method:str) -> 'partial | HelpScoutEndpointRequester':
328327
"""Catches http methods like get, post, patch, put and delete.
329328
Returns a subrequester when methods not named after http methods are
330329
requested, as this are considered attributes of the main object, like
@@ -359,7 +358,7 @@ def __getattr__(self, method):
359358
False,
360359
)
361360

362-
def __getitem__(self, resource_id):
361+
def __getitem__(self, resource_id:int|str) -> 'HelpScoutEndpointRequester':
363362
"""Returns a second endpoint requester extending the endpoint to a
364363
specific resource_id or resource_name.
365364
@@ -368,7 +367,7 @@ def __getitem__(self, resource_id):
368367
369368
Parameters
370369
----------
371-
resource_id: int or str
370+
resource_id: int | str
372371
The resource id or attribute available in the API through a
373372
specific call.
374373
@@ -408,7 +407,7 @@ def __eq__(self, other):
408407
self.endpoint == other.endpoint and
409408
self.client == other.client)
410409

411-
def __repr__(self):
410+
def __repr__(self) -> str:
412411
"""Returns the object as a string."""
413412
name = self.__class__.__name__
414413
return '%s(app_id="%s", endpoint="%s")' % (

helpscout/model.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1+
import typing
2+
if typing.TYPE_CHECKING:
3+
from typing import Self
4+
15
class HelpScoutObject(object):
26

37
key = ''
48

5-
def __init__(self, api_object):
6-
"""Object build from an API dictionary.
9+
def __init__(self, api_object:dict):
10+
"""Object built from an API dictionary.
711
Variable assignments to initialized objects is not expected to be done.
812
913
Parameters
@@ -24,18 +28,18 @@ def __init__(self, api_object):
2428
setattr(self, key, value)
2529

2630
@classmethod
27-
def from_results(cls, api_results):
31+
def from_results(cls, api_results) -> list[Self]:
2832
"""Generates HelpScout objects from API results.
2933
3034
Parameters
3135
----------
3236
api_results: generator({cls.key: [dict]}) or generator(dict)
33-
A generator returning API responses that cointain a list of
37+
A generator returning API responses that contain a list of
3438
objects each under the class key.
3539
3640
Returns
3741
-------
38-
[HelpScoutObject]
42+
list[HelpScoutObject]
3943
"""
4044
results = []
4145
for api_result in api_results:
@@ -45,7 +49,7 @@ def from_results(cls, api_results):
4549
return results
4650

4751
@classmethod
48-
def cls(cls, entity_name, key):
52+
def cls(cls, entity_name:str, key:str):
4953
"""Returns the object class based on the entity_name.
5054
5155
Parameters
@@ -77,7 +81,7 @@ def cls(cls, entity_name, key):
7781
globals()[class_name] = cls = type(class_name, (cls,), {'key': key})
7882
return cls
7983

80-
def __setattr__(self, attr, value):
84+
def __setattr__(self, attr:str, value:object):
8185
"""Sets an attribute to an object and adds it to the attributes list.
8286
8387
Parameters
@@ -108,7 +112,7 @@ def __setstate__(self, state):
108112
for attr, value in zip(self._attrs, state[1]):
109113
setattr(self, attr, value)
110114

111-
def __eq__(self, other):
115+
def __eq__(self, other) -> bool:
112116
"""Equality comparison."""
113117
if self.__class__ is not other.__class__:
114118
return False
@@ -130,7 +134,7 @@ def flatten(obj):
130134
values = tuple(getattr(self, attr) for attr in self._attrs)
131135
return hash(self._attrs + flatten(values))
132136

133-
def __repr__(self):
137+
def __repr__(self) -> str:
134138
"""Returns the object as a string."""
135139
name = self.__class__.__name__
136140
attrs = self._attrs
@@ -145,13 +149,15 @@ def __repr__(self):
145149
__str__ = __repr__
146150

147151

148-
def get_subclass_instance(class_name, key):
152+
def get_subclass_instance(class_name: str, key):
149153
"""Gets a dynamic class from a class name for unpickling.
150154
151155
Parameters
152156
----------
153-
name: str
157+
class_name: str
154158
A class name, expected to start with Upper case.
159+
key: ???
160+
TODO Missing desc
155161
156162
Returns
157163
-------

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
requests==2.21.0
1+
requests~=2.31.0

setup.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ def readme():
2929
classifiers=[
3030
'License :: OSI Approved :: MIT License',
3131
'Programming Language :: Python',
32-
'Programming Language :: Python :: 2',
33-
'Programming Language :: Python :: 3'
34-
]
32+
'Programming Language :: Python :: 3',
33+
'Programming Language :: Python :: 3.11',
34+
'Programming Language :: Python :: 3.12'
35+
]
3536
)

0 commit comments

Comments
 (0)