Skip to content

Commit c8a580f

Browse files
authored
Pagination and recursive calls (#20)
* Add ApiResourceSet class * Add 'get_recursive' * Add pycodestyle * Fix code style * Rename tests * Update tests * Update docs * Remove deprecated methods * Update changelog * Update changelog * Update CHANGELOG.md
1 parent d451295 commit c8a580f

20 files changed

+351
-76
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ dist/*
99
.coverage.*
1010
.pytest_cache/*
1111
/htmlcov/
12+
/dev

CHANGELOG.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,20 @@ All notable changes to `exonet-api-python` will be documented in this file.
55
Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) principles.
66

77
## [Unreleased]
8-
[Compare 2.0.0 - Unreleased](https://github.com/exonet/exonet-api-python/compare/2.0.0...master)
8+
[Compare 3.0.0 - Unreleased](https://github.com/exonet/exonet-api-python/compare/3.0.0...master)
9+
10+
## [3.0.0](https://github.com/exonet/exonet-api-python/releases/tag/3.0.0) - 2020-09-11
11+
[Compare 2.1.0 - 3.0.0](https://github.com/exonet/exonet-api-python/compare/2.1.0...3.0.0)
12+
### Breaking
13+
- When multiple resources are returned from the API, an instance of `ApiResourceSet` is returned instead of a list. This class is traversable so unless the code does specific `list` things or type checks, no changes are necessary.
14+
15+
### Added
16+
- Add the `total()` method to resource sets to get the total number of resources (and not only the number of resources in the current resource set).
17+
- Add `next_page`, `previous_page`, `first_page` and `last_page` methods to the `ApiResourceSet` for easy loading of paginated resource sets.
18+
- Add a `get_recursive` method to the `RequestBuilder` to get the resource set including recursively the resource sets from the following pages.
19+
20+
### Removed
21+
- The `store` method for creating `POST` requests. (Deprecated since 2.0.0)
922

1023
## [2.1.0](https://github.com/exonet/exonet-api-python/releases/tag/2.1.0) - 2019-11-19
1124
[Compare 2.0.0 - 2.1.0](https://github.com/exonet/exonet-api-python/compare/2.0.0...2.1.0)
@@ -18,7 +31,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip
1831
- The `Api` prefix has been added from the following classes for consistency:
1932
- `Resource` --> `ApiResource`
2033
- `ResourceIdentifier` --> `ApiResourceIdentifier`
21-
34+
2235
### Added
2336
- Support for `PATCH` and `DELETE` requests.
2437

docs/calls.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,19 @@ After setting the options you can call the `get()` method to retrieve the resour
2727
certificates = certificates_request.get()
2828
```
2929

30+
It is also possible to get all resource sets recursively. The package will check the URL defined in `links.next` and as
31+
long as the value is not `null` it will make an additional request and merge the results:
32+
33+
```python
34+
certificates = certificates_request.get_recursive()
35+
```
36+
37+
Please note that the `get_recursive` method respects pagination and filters. So the following example will get all
38+
non-expired certificates, starting from page two in batches of ten:
39+
```python
40+
certificates = certificates_request.filter('expired', False).page(2).size.get_recursive()
41+
```
42+
3043
## Getting a single resource by ID
3144
If you want to get a specific resource by its ID, you can pass it as an argument to the `get` method:
3245
```python

docs/responses.md

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,37 @@
11
# Working with API Responses
22
There are two types of API responses upon a successful request. If a single resource is requested then a [`ApiResource`](resources.md) instance is
3-
returned, if multiple resources are requested then an `list` of `ApiResource`'s is returned.
3+
returned, if multiple resources are requested then an `ApiResourceSet` is returned.
4+
5+
## The `ApiResourceSet` class
6+
When the API returns multiple resources, for example when getting an overview page, an instance of the `ApiResourceSet` class
7+
is returned. The instance of this class contains the requested resources. Traverse each individual resource by using a
8+
`for`-loop on the instance:
9+
```python
10+
certificates = client.resource('certificates').get()
11+
for certificate in certificates:
12+
# Each item is an instance of an ApiResource.
13+
print(certificate.id())
14+
```
15+
16+
The get the number of items in a resource set, you can use one of the following methods:
17+
```php
18+
len(certificates); // Returns the number of resources in the current resource set.
19+
certificates.total() // Returns the total number of resources in the resource set, ignoring pagination.
20+
```
21+
22+
If `len != total` you can get the next/previous/first/last page by calling one of the pagination methods:
23+
```python
24+
# Get the next resource set:
25+
certificates.next_page()
26+
# Get the previous resource set:
27+
certificates.previous_page()
28+
# Get the first resource set:
29+
certificates.first_page()
30+
# Get the last resource set:
31+
certificates.last_page()
32+
```
33+
34+
Each of this methods will return `None` if not available.
435

536
## The [`ApiResource`](resources.md) class
637
Each resource returned by the API is transformed to an [`ApiResource`](resources.md) instance. This makes it possible to have easy access
@@ -21,24 +52,6 @@ print(
2152
)
2253
```
2354

24-
## Multiple resources
25-
When the API returns multiple resources, for example when getting an overview page, a list is returned.
26-
This list contains the requested resources. Iterate over the list to handle the resources:
27-
28-
```python
29-
30-
# Get all certificates
31-
certificates = client.resource('certificates').size(10).get()
32-
33-
for certificate in certificates:
34-
print(
35-
'- {domain} Expires at {expire_date}'.format(
36-
domain=certificate.attribute('domain'),
37-
expire_date=certificate.attribute('expires_at')
38-
)
39-
)
40-
```
41-
4255
---
4356

4457
[Back to the index](index.md)

examples/dns_zone_details.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@
99
client.authenticator.set_token(sys.argv[1])
1010

1111
'''
12-
Get a single dns_zone resource. Because depending on who is authorized, the dns_zone IDs change, all dns_zones are
13-
retrieved with a limit of 1. From this result, the first DNS zone is used. In a real world scenario you would call
14-
something like `zone = client.resource('dns_zones').get('VX09kwR3KxNo')` to get a single DNS zone by it's ID.
12+
Get a single dns_zone resource. Because depending on who is authorized, the dns_zone IDs change, all
13+
dns_zones are retrieved with a limit of 1. From this result, the first DNS zone is used. In a real
14+
world scenario you would call something like
15+
`zone = client.resource('dns_zones').get('VX09kwR3KxNo')` to get a single DNS zone by it's ID.
1516
'''
1617
zones = client.resource('dns_zones').size(1).get()
1718

examples/ticket_details.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@
99
client.authenticator.set_token(sys.argv[1])
1010

1111
'''
12-
Get a single ticket resource. Because depending on who is authorized, the ticket IDs change, all tickets are
13-
retrieved with a limit of 1. From this result, the first ticket is used. In a real world scenario you would call
14-
something like `ticket = client.resource('tickets').get('VX09kwR3KxNo')` to get a single ticket by it's ID.
12+
Get a single ticket resource. Because depending on who is authorized, the ticket IDs change, all
13+
tickets are retrieved with a limit of 1. From this result, the first ticket is used. In a real world
14+
scenario you would call something like `ticket = client.resource('tickets').get('VX09kwR3KxNo')` to
15+
get a single ticket by it's ID.
1516
'''
1617
tickets = client.resource('tickets').size(1).get()
1718

exonetapi/RequestBuilder.py

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
"""
22
Build requests to send to the API.
33
"""
4+
import json
45
import warnings
56

67
import requests
78

8-
from .result import Parser
9+
from exonetapi.structures import ApiResourceSet
910
from exonetapi.exceptions.ValidationException import ValidationException
11+
from .result import Parser
1012

1113

1214
class RequestBuilder(object):
1315
"""Create and make requests to the API.
1416
"""
1517

16-
def __init__(self, resource, client=None):
17-
if not resource.startswith('/'):
18+
def __init__(self, resource=None, client=None):
19+
if resource is not None and not resource.startswith('/'):
1820
resource = '/' + resource
1921

2022
self.__resource = resource
@@ -98,9 +100,8 @@ def get(self, identifier=None):
98100

99101
return Parser(response.content).parse()
100102

101-
def store(self, resource):
102-
warnings.warn("store() is deprecated; use post().", DeprecationWarning)
103-
self.post(resource)
103+
def get_recursive(self):
104+
return self.__get_recursive()
104105

105106
def post(self, resource):
106107
"""Make a POST request to the API with the provided resource as data.
@@ -176,7 +177,10 @@ def __build_url(self, identifier=None):
176177
177178
:return: A URL.
178179
"""
179-
url = self.__client.get_host() + self.__resource
180+
url = self.__client.get_host()
181+
182+
if self.__resource is not None:
183+
url += self.__resource
180184

181185
if identifier:
182186
url += '/' + identifier
@@ -211,3 +215,32 @@ def __make_call(self, method, url, json_data=None, params=None):
211215
response.raise_for_status()
212216

213217
return response
218+
219+
def __get_recursive(self, data=None, url=None):
220+
"""
221+
Get the URL and call this method recursivly as long as there is an URL in the 'next' field
222+
of the 'links' data.
223+
224+
:param data: The ApiResourceSet to append the resources to.
225+
:param url: The URL to call.
226+
:return: The ApiResourceSet containing all requested resources.
227+
"""
228+
response = self.__make_call(
229+
'GET',
230+
url or self.__build_url(),
231+
params=self.__query_params if not url else None
232+
)
233+
234+
content = Parser(response.content).parse()
235+
236+
if data is None:
237+
data = ApiResourceSet()
238+
data.set_meta(content.meta().copy())
239+
240+
data.add_resource(content.resources())
241+
242+
next_link = content.links().get('next')
243+
if next_link is not None:
244+
return self.__get_recursive(data, next_link)
245+
246+
return data

exonetapi/result/Parser.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"""
44
import json
55
from exonetapi.create_resource import create_resource
6-
from exonetapi.structures import ApiResourceIdentifier
6+
from exonetapi.structures import ApiResourceIdentifier, ApiResourceSet
77
from exonetapi.structures.Relationship import Relationship
88

99

@@ -14,17 +14,23 @@ class Parser:
1414

1515
def __init__(self, data):
1616
self.__data = data
17-
self.__json_data = json.loads(self.__data).get('data')
17+
self.__json = json.loads(self.__data)
18+
self.__json_data = self.__json.get('data')
1819

1920
def parse(self):
2021
"""Parse JSON string into a ApiResource or a list of Resources.
2122
22-
:return list|ApiResource: List with ApiResources or a single ApiResource.
23+
:return ApiResourceSet|ApiResource: An ApiResourceSet or a single ApiResource.
2324
"""
2425
if type(self.__json_data) is list:
25-
resources = []
26+
resources = ApiResourceSet()
27+
resources \
28+
.set_meta(self.__json.get('meta')) \
29+
.set_links(self.__json.get('links'))
30+
2631
for resource_data in self.__json_data:
27-
resources.append(self.make_resource(resource_data))
32+
resource = self.make_resource(resource_data)
33+
resources.add_resource(resource)
2834

2935
return resources
3036
else:

exonetapi/structures/ApiResource.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,6 @@ def to_json_resource_identifier(self):
104104
def patch(self):
105105
return exonetapi.RequestBuilder(self.type()).patch(self)
106106

107-
def store(self):
108-
warnings.warn("store() is deprecated; use post().", DeprecationWarning)
109-
return self.post()
110-
111107
def post(self):
112108
return exonetapi.RequestBuilder(self.type()).post(self)
113109

exonetapi/structures/ApiResourceIdentifier.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
class ApiResourceIdentifier(object):
1010
"""Basic ApiResource identifier.
1111
"""
12+
1213
def __init__(self, type, id=None):
1314
"""Initialize the resource.
1415
:param type: The type of the resource.
@@ -71,7 +72,7 @@ def get_relationship(self, name):
7172
:param name: The name of the relation to get.
7273
:return: The defined relation or None
7374
"""
74-
if not name in self.__relationships.keys():
75+
if name not in self.__relationships.keys():
7576
self.__relationships[name] = Relationship(name, self.type(), self.id())
7677

7778
return self.__relationships[name]

0 commit comments

Comments
 (0)