Skip to content

Commit 134a30f

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 134a30f

File tree

6 files changed

+77
-60
lines changed

6 files changed

+77
-60
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 & 40 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
35

4-
from functools import partial
5-
try: # Python 3
6-
from urllib.parse import urljoin
7-
except ImportError: # Python 2
8-
from urlparse import urljoin
6+
#import helpscout
7+
if typing.TYPE_CHECKING:
8+
from collections.abc import Generator
99

10-
import requests
10+
from functools import partial
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,16 @@ 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(
106-
self.hit_(endpoint, 'get', resource_id, params=params))
105+
results:list[HelpScoutObject] = cls.from_results( self.hit_(endpoint, 'get', resource_id, params=params) )
107106
if resource_id is not None or specific_resource:
108107
return results[0]
109108
return results
110109

111-
def hit(self, endpoint, method, resource_id=None, data=None, params=None):
110+
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]:
112111
"""Hits the api and returns all the data.
113112
If several calls are needed due to pagination, control won't be
114113
returned to the caller until all is retrieved.
@@ -120,26 +119,26 @@ def hit(self, endpoint, method, resource_id=None, data=None, params=None):
120119
method: str
121120
The http method to hit the endpoint with.
122121
One of {'get', 'post', 'put', 'patch', 'delete', 'head', 'options'}
123-
resource_id: int or str or None
122+
resource_id: int | str | None
124123
The id of the resource in the endpoint to query.
125124
E.g.: in "GET /v2/conversations/123 HTTP/1.1" the id would be 123.
126125
If None is provided, nothing will be done
127-
data: dict or None
126+
dict: dict | None
128127
A dictionary with the data to send to the API as json.
129-
params: dict or str or None
128+
params: dict | str | None
130129
Dictionary with the parameters to send to the url.
131130
Or the parameters already un url format.
132131
133132
Returns
134133
-------
135-
[dict] or [None]
136-
dict: when several objects are received from the API, a list of
134+
list[dict] | list[None]
135+
list: when several objects are received from the API, a list of
137136
dictionaries with HelpScout's _embedded data will be returned
138137
None if http 201 created or 204 no content are received.
139138
"""
140139
return list(self.hit_(endpoint, method, resource_id, data, params))
141140

142-
def hit_(self, endpoint, method, resource_id=None, data=None, params=None):
141+
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]:
143142
"""Hits the api and yields the data.
144143
145144
Parameters
@@ -149,19 +148,19 @@ def hit_(self, endpoint, method, resource_id=None, data=None, params=None):
149148
method: str
150149
The http method to hit the endpoint with.
151150
One of {'get', 'post', 'put', 'patch', 'delete', 'head', 'options'}
152-
resource_id: int or str or None
151+
resource_id: int | str | None
153152
The id of the resource in the endpoint to query.
154153
E.g.: in "GET /v2/conversations/123 HTTP/1.1" the id would be 123.
155154
If None is provided, nothing will be done
156-
data: dict or None
155+
data: dict | None
157156
A dictionary with the data to send to the API as json.
158-
params: dict or str or None
157+
params: dict | str | None
159158
Dictionary with the parameters to send to the url.
160159
Or the parameters already un url format.
161160
162161
Yields
163162
------
164-
dict or None
163+
dict | None
165164
Dictionary with HelpScout's _embedded data.
166165
None if http 201 created or 204 no content are received.
167166
"""
@@ -197,7 +196,7 @@ def hit_(self, endpoint, method, resource_id=None, data=None, params=None):
197196
else:
198197
raise HelpScoutException(r.text)
199198

200-
def _results_with_pagination(self, response, method):
199+
def _results_with_pagination(self, response:dict, method:str) -> Generator[dict, None, None]:
201200
"""Requests and yields pagination results.
202201
203202
Parameters
@@ -243,8 +242,7 @@ def _results_with_pagination(self, response, method):
243242
raise HelpScoutException(r.text)
244243

245244
def _authenticate(self):
246-
"""Authenticates with the API and gets a token for subsequent requests.
247-
"""
245+
"""Authenticates with the API and gets a token for subsequent requests."""
248246
url = urljoin(self.base_url, 'oauth2/token')
249247
data = {
250248
'grant_type': 'client_credentials',
@@ -287,7 +285,7 @@ def __eq__(self, other):
287285
self.sleep_on_rate_limit_exceeded ==
288286
other.sleep_on_rate_limit_exceeded)
289287

290-
def __repr__(self):
288+
def __repr__(self) -> str:
291289
"""Returns the object as a string."""
292290
name = self.__class__.__name__
293291
attrs = (
@@ -307,12 +305,12 @@ def __repr__(self):
307305

308306
class HelpScoutEndpointRequester:
309307

310-
def __init__(self, client, endpoint, specific_resource):
308+
def __init__(self, client:HelpScout, endpoint:str, specific_resource:bool):
311309
"""Client wrapper to perform requester.get/post/put/patch/delete.
312310
313311
Parameters
314312
----------
315-
client: HelpScoutClient
313+
client: HelpScout
316314
A help scout client instance to query the API.
317315
endpoint: str
318316
One of the endpoints in the API. E.g.: conversations, mailboxes.
@@ -324,7 +322,7 @@ def __init__(self, client, endpoint, specific_resource):
324322
self.endpoint = endpoint
325323
self.specific_resource = specific_resource
326324

327-
def __getattr__(self, method):
325+
def __getattr__(self, method:str) -> 'partial | HelpScoutEndpointRequester':
328326
"""Catches http methods like get, post, patch, put and delete.
329327
Returns a subrequester when methods not named after http methods are
330328
requested, as this are considered attributes of the main object, like
@@ -359,7 +357,7 @@ def __getattr__(self, method):
359357
False,
360358
)
361359

362-
def __getitem__(self, resource_id):
360+
def __getitem__(self, resource_id:int|str) -> 'HelpScoutEndpointRequester':
363361
"""Returns a second endpoint requester extending the endpoint to a
364362
specific resource_id or resource_name.
365363
@@ -368,7 +366,7 @@ def __getitem__(self, resource_id):
368366
369367
Parameters
370368
----------
371-
resource_id: int or str
369+
resource_id: int | str
372370
The resource id or attribute available in the API through a
373371
specific call.
374372
@@ -408,7 +406,7 @@ def __eq__(self, other):
408406
self.endpoint == other.endpoint and
409407
self.client == other.client)
410408

411-
def __repr__(self):
409+
def __repr__(self) -> str:
412410
"""Returns the object as a string."""
413411
name = self.__class__.__name__
414412
return '%s(app_id="%s", endpoint="%s")' % (

helpscout/model.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ class HelpScoutObject(object):
22

33
key = ''
44

5-
def __init__(self, api_object):
6-
"""Object build from an API dictionary.
5+
def __init__(self, api_object:dict):
6+
"""Object built from an API dictionary.
77
Variable assignments to initialized objects is not expected to be done.
88
99
Parameters
@@ -24,18 +24,18 @@ def __init__(self, api_object):
2424
setattr(self, key, value)
2525

2626
@classmethod
27-
def from_results(cls, api_results):
27+
def from_results(cls, api_results) -> 'list[HelpScoutObject]':
2828
"""Generates HelpScout objects from API results.
2929
3030
Parameters
3131
----------
3232
api_results: generator({cls.key: [dict]}) or generator(dict)
33-
A generator returning API responses that cointain a list of
33+
A generator returning API responses that contain a list of
3434
objects each under the class key.
3535
3636
Returns
3737
-------
38-
[HelpScoutObject]
38+
list[HelpScoutObject]
3939
"""
4040
results = []
4141
for api_result in api_results:
@@ -45,7 +45,7 @@ def from_results(cls, api_results):
4545
return results
4646

4747
@classmethod
48-
def cls(cls, entity_name, key):
48+
def cls(cls, entity_name:str, key:str):
4949
"""Returns the object class based on the entity_name.
5050
5151
Parameters
@@ -77,7 +77,7 @@ def cls(cls, entity_name, key):
7777
globals()[class_name] = cls = type(class_name, (cls,), {'key': key})
7878
return cls
7979

80-
def __setattr__(self, attr, value):
80+
def __setattr__(self, attr:str, value:object):
8181
"""Sets an attribute to an object and adds it to the attributes list.
8282
8383
Parameters
@@ -108,7 +108,7 @@ def __setstate__(self, state):
108108
for attr, value in zip(self._attrs, state[1]):
109109
setattr(self, attr, value)
110110

111-
def __eq__(self, other):
111+
def __eq__(self, other) -> bool:
112112
"""Equality comparison."""
113113
if self.__class__ is not other.__class__:
114114
return False
@@ -130,7 +130,7 @@ def flatten(obj):
130130
values = tuple(getattr(self, attr) for attr in self._attrs)
131131
return hash(self._attrs + flatten(values))
132132

133-
def __repr__(self):
133+
def __repr__(self) -> str:
134134
"""Returns the object as a string."""
135135
name = self.__class__.__name__
136136
attrs = self._attrs
@@ -145,13 +145,15 @@ def __repr__(self):
145145
__str__ = __repr__
146146

147147

148-
def get_subclass_instance(class_name, key):
148+
def get_subclass_instance(class_name: str, key):
149149
"""Gets a dynamic class from a class name for unpickling.
150150
151151
Parameters
152152
----------
153-
name: str
153+
class_name: str
154154
A class name, expected to start with Upper case.
155+
key: ???
156+
TODO Missing desc
155157
156158
Returns
157159
-------

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)