API Pagination Response
Cursor Pagination
The API will automatically generate the URL for the next page of results when applicable. If the full payload returns in the first payload, the "next" response will be null.
The URL for the next page is found under the payload.metadata.next property for a 200 - OK HTTP response like so:
{
"status": 200,
"title": "OK",
"description": "Successful request",
"payload": {
"metadata": {
"next": "https://web3api.io/api/v2/defi/lending/compoundv2/assets/ZRX?cursor=N4IgRgNg9gxg1jAFgQwJYDsCSATEAuEARhgE5SA2EgBnIFYAzGbY%2BkAGhAAcAnKAF1hQIOfCBhQAtpygBXdNgBuAJnYgJybnACmffOhkQIHDDAgzsWgIKGAKt2ToAzshh9UUdAFEFW9H0f49MgQjlocbhJaAGJQ3Oq6BIgy6ugASlrI2MiQWqrYqNxaru7oopYAygDCqo58GnwAIsh8uXiE5OSEACwkXYQA7AAcVCMcvthNLfjtnSTUXSOLHI6oAF6thKMgFj7QnFrcALJQFoHBoRz0sfGiAFaOHqqcyADmGxyxqC8YweV13I1mhs6CQAMyEJSLJYgT7fdDBTzySbA2bzKFUDjIRyhPgiAhUAAeWj62CUXUGJFo-UI2FBSiUhApZPIWSCtC0YAshHoXUoJB5JEGqggWL4NlQkVqyCkohm3V6AyFHBFtQAMlAXph5FoCdNwQBfIA"
},
...
}
}
As an API user, to access the next page of results, you should:
- Retrieve the URL from payload.metadata.next
- Copy your request headers from your initial API call e.g. x-api-key, x-amberdata-blockchain-id etc.
- Make a HTTP GET request with the next page URL and the copied headers
Querying Long Timeframes
API endpoints have a maximum supported range for the query parameters endDate and startDate.
Increasing the range for endDate and startDate
Amberdata reserves the right to increase the supported range for endDate and startDate for any endpoint. An increase in the supported range is fully backwards compatible.
There are ways to call endpoints and get data for timeframes longer than the maximum range of endDate and startDate. Here below is an example of how to get 1 year of data for the DEX - Trades endpoint which has a maximum range of 30 days.
Disclaimer
The code below has been verified and tested for demonstration purposes only.
import os
from datetime import datetime
from dataclasses import dataclass, field
from endpoint_timerange_handler import EndpointTimeRangeHandler, Endpoint
from endpoint_caller_v2 import EndpointCaller
def http_ok_next_page_url_extractor(page):
"""
Function that extracts the next page url from the current page of data
Parameters
----------
page: dict
Returns
-------
next_page_url: str
"""
if 'payload' in page and page['payload'] is not None:
payload = page['payload']
if 'metadata' in payload and payload['metadata'] is not None:
metadata = payload['metadata']
if 'next' in metadata and metadata['next'] is not None:
next_page_url = metadata['next']
return next_page_url
return ""
@dataclass(kw_only=True)
class DEXTradesHistorical(Endpoint):
poolAddress: str
path_template: str = field(default='/market/defi/trades/{}/historical')
max_interval_in_seconds: int = field(default=2592000) #30 days * 24 hrs * 60 min * 60 seconds
def format_path(self) -> str:
return self.path_template.format(self.poolAddress)
"""
You can implement similar dataclasses for other endpoints as needed.
`max_interval_in_seconds` is the maximum supported range for endDate and startDate.
The maximum range for each endpoint can be found in the documentation.
"""
def get_api_responses(start_date: datetime, end_date: datetime, endpoint: DEXTradesHistorical) -> None:
"""
Get all pages of data between `start_date` and `end_date`.
"""
endpoint_caller = EndpointCaller(os.getenv('PRODUCTION_API_KEY'))
endpoint_timerange_handler = EndpointTimeRangeHandler(endpoint_caller)
for page in endpoint_timerange_handler.get_data_for_timerange(
start_date,
end_date,
endpoint,
http_ok_next_page_url_extractor
):
response = page.data
print(f"Timestamp of first entry in the page: {response['payload']['data'][0][1]}")
# print('uncomment here below as needed')
# print(page.data)
# print(page.duration_seconds)
def call_dex_trades_historical() -> None:
"""
Example configuration of an endpoint to be called.
This example demonstrates a way to query a time range larger than the endpoint's max supported range (endDate - startDate).
"""
start_date = "2022-01-01T00:00:00"
end_date = "2023-01-01T00:00:00"
start_date_as_dt = datetime.fromisoformat(start_date).replace(microsecond=0)
end_date_as_dt = datetime.fromisoformat(end_date).replace(microsecond=0)
poolAddress = '0xcbcdf9626bc03e24f779434178a73a0b4bad62ed' # WBTC/ETH 0.3%
exchange = 'uniswapv3'
dex_trades_historical = DEXTradesHistorical(poolAddress=poolAddress)
dex_trades_historical.add_query_parameter('exchange', exchange)
get_api_responses(start_date_as_dt, end_date_as_dt, dex_trades_historical)
if __name__ == "__main__":
call_dex_trades_historical()
from datetime import datetime
from datetime import timedelta
from dataclasses import dataclass, field
from endpoint_caller_v2 import EndpointCaller
@dataclass(kw_only=True)
class Endpoint:
"""
This is a parent class that should be inherited and implemented for specific endpoints.
"""
query: dict = field(default_factory=dict)
headers: dict = field(default_factory=dict)
def add_query_parameter(self, parameter_name: str, parameter_value) -> None:
if parameter_name is not None and len(parameter_name) > 0:
self.query[parameter_name] = parameter_value
def format_path(self) -> str:
"""
Child classes must implement this function.
"""
return ""
def add_header(self, header_name: str, header_value: str) -> None:
if header_name is not None and len(header_name) > 0:
self.headers[header_name] = header_value
class EndpointTimeRangeHandler:
def __init__(self, endpoint_caller: EndpointCaller) -> None:
self.endpoint_caller = endpoint_caller
def get_data(self,
start_date: datetime,
end_date: datetime,
endpoint: Endpoint,
http_ok_next_page_url_extractor):
endpoint.add_query_parameter('startDate', str(start_date).replace(" ", "T"))
endpoint.add_query_parameter('endDate', str(end_date).replace(" ", "T"))
yield from self.endpoint_caller.call_endpoint_and_get_all_pages(
endpoint.format_path(),
endpoint.query,
endpoint.headers,
http_ok_next_page_url_extractor
)
def get_data_for_timerange(self,
start_date: datetime,
end_date: datetime,
endpoint: Endpoint,
http_ok_next_page_url_extractor):
"""
Given an arbitrarily large timerange denoted by `end_date` and `start_date`, this
function will call the endpoint continuously by breaking up the requested time range into chunks.
The chunks do not exceed the single request maximum range (endDate - startDate) for the specific endpoint.
Parameters
----------
start_date: datetime
end_date: datetime
endpoint: Endpoint
An implementation of Endpoint for a specific class
http_ok_next_page_url_extractor: function
The function that extracts the next page url from a given API response
Returns
-------
page: AmberdataResponse
A single page is yielded for each call to this generator function
"""
duration = end_date - start_date
intervals = duration.total_seconds()/endpoint.max_interval_in_seconds
hours = duration.total_seconds()/3600
print(f"Getting {hours} hours of data (# of intervals: {intervals})")
start_date_copy = start_date
timerange_stack = []
while start_date_copy + timedelta(seconds=endpoint.max_interval_in_seconds) <= end_date:
intermediate_end_date = start_date_copy + timedelta(seconds=endpoint.max_interval_in_seconds)
timerange_stack.append((start_date_copy, intermediate_end_date))
start_date_copy = intermediate_end_date
timerange_stack.append((start_date_copy, end_date))
timerange_stack.reverse()
while len(timerange_stack) > 0:
timerange = timerange_stack.pop()
print(f"Retrieving data from {str(timerange[0])} to {str(timerange[1])}")
yield from self.get_data(timerange[0], timerange[1], endpoint, http_ok_next_page_url_extractor)
import time
import requests
import json
PRODUCTION_BASE_URL = "https://web3api.io/api/v2"
PRODUCTION_BASE_URL_FORMATTABLE = "https://web3api.io/api/v2{}"
class AmberdataResponse:
def __init__(self, data, status, duration_seconds, request_url):
self.data = data
self.status = status
self.duration_seconds = duration_seconds
self.request_url = request_url
self.attempts = 0
def increment_attempt(self, by=None):
if by is not None:
self.attempts += by
else:
self.attempts += 1
"""
Type Hints
"""
AmberdataResponseStack = list[AmberdataResponse]
class EndpointCaller:
def __init__(self, amberdata_api_key):
self.x_api_key = amberdata_api_key
def call_endpoint_and_get_data_as_json(self, path: str, query: dict, headers: dict):
headers_with_api_key = {
**headers,
'x-api-key': self.x_api_key
}
if path.startswith(PRODUCTION_BASE_URL):
"""
When the next page URL is pre-formed and can be used as-is.
"""
full_api_url = path
else:
full_api_url = PRODUCTION_BASE_URL_FORMATTABLE.format(path)
start_time = time.time()
request = None
try:
request = requests.get(full_api_url, params=query, headers=headers_with_api_key)
print("Making HTTP call for: {}".format(request.url))
end_time = time.time()
duration = end_time - start_time
if request.status_code == 200:
json_data = request.json()
return AmberdataResponse(json_data, 200, duration, request.url)
else:
return AmberdataResponse(request.text, request.status_code, duration, request.url)
except Exception as exc:
print(exc)
as_5xx_error_json = {
'status': 500, #default to 500 error
'message': 'Failed to complete HTTP request.'
}
end_time = time.time()
duration = end_time - start_time
return AmberdataResponse(json.dumps(as_5xx_error_json), 500, duration, None)
def call_endpoint_and_get_all_pages(self, path: str, query: dict, headers: dict, http_ok_next_page_url_extractor):
"""
Iterative, non-recursive way to get all the pages given an initial URL.
Avoids Python's max recursion depth (~1000).
This is a generator function and should be used accordingly.
Parameters
----------
path: str
The endpoint path with the path parameters inserted i.e if the path is `/market/defi/trades/{pool}/historical/` then provide `/market/defi/trades/0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640/historical`
query: dict
The query parameters in a dict i.e `{'startDate': '2023-05-20', 'endDate': '2023-05-21'}`
headers: dict
The request headers in a dict, you do not have to pass in the API key because the `EndpointCaller` class is initialized with it
http_ok_next_page_url_extractor: function
The function that extracts the next page url from a given API response
Returns
-------
api_response: AmberdataResponse
Individual page from the calling the endpoint
"""
stack: AmberdataResponseStack = []
current_page_response = self.call_endpoint_and_get_data_as_json(path, query, headers)
current_page_response.increment_attempt()
stack.append(current_page_response)
while len(stack) > 0:
response = stack.pop()
if response.status == 200:
next_page_url = http_ok_next_page_url_extractor(response.data)
if len(next_page_url) > 0:
next_page_response = self.call_endpoint_and_get_data_as_json(next_page_url, None, headers)
next_page_response.increment_attempt()
stack.append(next_page_response)
yield response
else:
if response.attempts < 3 and response.request_url is not None:
retried_page_response = self.call_endpoint_and_get_data_as_json(response.request_url, None, headers)
retried_page_response.increment_attempt(by=response.attempts + 1)
stack.append(retried_page_response)
else:
yield response
assert len(stack) == 0, "Stack is not empty, more pages need to be retrieved. # of remaining pages is {}".format(len(stack))
return AmberdataResponse("DONE", -1, 0, "")