ebird-api-requests 4.2.0__tar.gz → 4.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. {ebird_api_requests-4.2.0/src/ebird_api_requests.egg-info → ebird_api_requests-4.3.0}/PKG-INFO +25 -1
  2. {ebird_api_requests-4.2.0 → ebird_api_requests-4.3.0}/README.md +24 -0
  3. {ebird_api_requests-4.2.0 → ebird_api_requests-4.3.0}/pyproject.toml +2 -2
  4. {ebird_api_requests-4.2.0 → ebird_api_requests-4.3.0}/src/ebird/api/requests/__init__.py +6 -1
  5. {ebird_api_requests-4.2.0 → ebird_api_requests-4.3.0}/src/ebird/api/requests/utils.py +123 -4
  6. {ebird_api_requests-4.2.0 → ebird_api_requests-4.3.0/src/ebird_api_requests.egg-info}/PKG-INFO +25 -1
  7. {ebird_api_requests-4.2.0 → ebird_api_requests-4.3.0}/LICENSE.txt +0 -0
  8. {ebird_api_requests-4.2.0 → ebird_api_requests-4.3.0}/setup.cfg +0 -0
  9. {ebird_api_requests-4.2.0 → ebird_api_requests-4.3.0}/setup.py +0 -0
  10. {ebird_api_requests-4.2.0 → ebird_api_requests-4.3.0}/src/ebird/api/requests/checklists.py +0 -0
  11. {ebird_api_requests-4.2.0 → ebird_api_requests-4.3.0}/src/ebird/api/requests/client.py +0 -0
  12. {ebird_api_requests-4.2.0 → ebird_api_requests-4.3.0}/src/ebird/api/requests/constants.py +0 -0
  13. {ebird_api_requests-4.2.0 → ebird_api_requests-4.3.0}/src/ebird/api/requests/hotspots.py +0 -0
  14. {ebird_api_requests-4.2.0 → ebird_api_requests-4.3.0}/src/ebird/api/requests/observations.py +0 -0
  15. {ebird_api_requests-4.2.0 → ebird_api_requests-4.3.0}/src/ebird/api/requests/regions.py +0 -0
  16. {ebird_api_requests-4.2.0 → ebird_api_requests-4.3.0}/src/ebird/api/requests/species.py +0 -0
  17. {ebird_api_requests-4.2.0 → ebird_api_requests-4.3.0}/src/ebird/api/requests/statistics.py +0 -0
  18. {ebird_api_requests-4.2.0 → ebird_api_requests-4.3.0}/src/ebird/api/requests/taxonomy.py +0 -0
  19. {ebird_api_requests-4.2.0 → ebird_api_requests-4.3.0}/src/ebird/api/requests/validation.py +0 -0
  20. {ebird_api_requests-4.2.0 → ebird_api_requests-4.3.0}/src/ebird_api_requests.egg-info/SOURCES.txt +0 -0
  21. {ebird_api_requests-4.2.0 → ebird_api_requests-4.3.0}/src/ebird_api_requests.egg-info/dependency_links.txt +0 -0
  22. {ebird_api_requests-4.2.0 → ebird_api_requests-4.3.0}/src/ebird_api_requests.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ebird-api-requests
3
- Version: 4.2.0
3
+ Version: 4.3.0
4
4
  Summary: A python library for accessing the eBird API, version 2.0
5
5
  Author-email: Stuart MacKay <smackay@flagstonesoftware.com>
6
6
  License: MIT License
@@ -346,6 +346,30 @@ client.get_observations('MX-OAX')
346
346
 
347
347
  The client supports all the API functions.
348
348
 
349
+ ### Rate Limiting
350
+
351
+ The eBird API limits how quickly requests can be made. If you exceed the
352
+ limit the API responds with HTTP 429 Too Many Requests. If you are making
353
+ lots of calls, for example downloading all the checklists for a region,
354
+ then set a minimum interval, in seconds, between requests:
355
+
356
+ ```python
357
+ from ebird.api.requests import set_rate_limit
358
+
359
+ set_rate_limit(1.0)
360
+ ```
361
+
362
+ The limit applies to all the requests made by the process. It is disabled
363
+ by default. If the API still responds with HTTP 429, the interval is doubled,
364
+ up to a maximum of 60 seconds, so the requests slow down to a rate the API
365
+ accepts. Use get_rate_limit() to see the current interval.
366
+
367
+ When an HTTP 429 response is returned, the request is retried after waiting
368
+ for the time given in the Retry-After header. If the request still fails
369
+ after all the retries are used, a RateLimitError is raised. It is a subclass
370
+ of HTTPError, and the retry_after attribute contains the number of seconds
371
+ the API asked you to wait, or None if no Retry-After header was returned.
372
+
349
373
  ## Formats
350
374
 
351
375
  Most of the eBird API calls return JSON. Some of the calls such as getting
@@ -316,6 +316,30 @@ client.get_observations('MX-OAX')
316
316
 
317
317
  The client supports all the API functions.
318
318
 
319
+ ### Rate Limiting
320
+
321
+ The eBird API limits how quickly requests can be made. If you exceed the
322
+ limit the API responds with HTTP 429 Too Many Requests. If you are making
323
+ lots of calls, for example downloading all the checklists for a region,
324
+ then set a minimum interval, in seconds, between requests:
325
+
326
+ ```python
327
+ from ebird.api.requests import set_rate_limit
328
+
329
+ set_rate_limit(1.0)
330
+ ```
331
+
332
+ The limit applies to all the requests made by the process. It is disabled
333
+ by default. If the API still responds with HTTP 429, the interval is doubled,
334
+ up to a maximum of 60 seconds, so the requests slow down to a rate the API
335
+ accepts. Use get_rate_limit() to see the current interval.
336
+
337
+ When an HTTP 429 response is returned, the request is retried after waiting
338
+ for the time given in the Retry-After header. If the request still fails
339
+ after all the retries are used, a RateLimitError is raised. It is a subclass
340
+ of HTTPError, and the retry_after attribute contains the number of seconds
341
+ the API asked you to wait, or None if no Retry-After header was returned.
342
+
319
343
  ## Formats
320
344
 
321
345
  Most of the eBird API calls return JSON. Some of the calls such as getting
@@ -29,7 +29,7 @@ license = {text = "MIT License"}
29
29
  name = "ebird-api-requests"
30
30
  readme = "README.md"
31
31
  requires-python = ">= 3.10"
32
- version = "4.2.0"
32
+ version = "4.3.0"
33
33
 
34
34
  [project.urls]
35
35
  Repository = "https://github.com/StuartMacKay/ebird-api-requests.git"
@@ -37,7 +37,7 @@ Issues = "https://github.com/StuartMacKay/ebird-api-requests/issues"
37
37
  Changelog = "https://github.com/StuartMacKay/ebird-api-requests/blob/master/CHANGELOG.md"
38
38
 
39
39
  [tool.bumpversion]
40
- current_version = "4.2.0"
40
+ current_version = "4.3.0"
41
41
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
42
42
  serialize = ["{major}.{minor}.{patch}"]
43
43
  ignore_missing_version = false
@@ -2,7 +2,7 @@
2
2
 
3
3
  """A set of wrapper functions for accessing the eBird API."""
4
4
 
5
- __version__ = "4.2.0"
5
+ __version__ = "4.3.0"
6
6
 
7
7
  from ebird.api.requests.checklists import get_checklist, get_visits
8
8
  from ebird.api.requests.client import Client
@@ -37,3 +37,8 @@ from ebird.api.requests.taxonomy import (
37
37
  get_taxonomy_locales,
38
38
  get_taxonomy_versions,
39
39
  )
40
+ from ebird.api.requests.utils import (
41
+ RateLimitError,
42
+ get_rate_limit,
43
+ set_rate_limit,
44
+ )
@@ -1,13 +1,122 @@
1
1
  """Various functions used in the API."""
2
2
 
3
+ import datetime as dt
3
4
  import json
5
+ import threading
4
6
  import time
7
+ from email.utils import parsedate_to_datetime
5
8
  from urllib.error import HTTPError
6
9
  from urllib.parse import urlencode
7
10
  from urllib.request import Request, urlopen
8
11
 
9
12
  from ebird.api.requests import constants
10
13
 
14
+ # The maximum interval, in seconds, the rate limiter will slow down to
15
+ # after the eBird API responds with HTTP 429 Too Many Requests.
16
+ MAX_RATE_LIMIT_INTERVAL = 60.0
17
+
18
+
19
+ class RateLimitError(HTTPError):
20
+ """Raised when the eBird API still responds with HTTP 429 Too Many
21
+ Requests after all the retries have been used.
22
+
23
+ It is a subclass of HTTPError so existing error handling still works.
24
+ The retry_after attribute contains the number of seconds the API asked
25
+ the caller to wait, or None if no Retry-After header was returned.
26
+
27
+ """
28
+
29
+ def __init__(self, exc, retry_after=None):
30
+ super().__init__(exc.url, exc.code, exc.msg, exc.headers, exc.fp)
31
+ self.retry_after = retry_after
32
+
33
+
34
+ class RateLimiter:
35
+ """Enforce a minimum interval between the start of successive requests.
36
+
37
+ An interval of zero (the default) disables the limiter.
38
+
39
+ """
40
+
41
+ def __init__(self, interval=0.0):
42
+ self.interval = float(interval)
43
+ self.last_request = None
44
+ self.lock = threading.Lock()
45
+
46
+ def wait(self):
47
+ with self.lock:
48
+ if self.interval > 0 and self.last_request is not None:
49
+ delay = self.interval - (time.monotonic() - self.last_request)
50
+ if delay > 0:
51
+ time.sleep(delay)
52
+ self.last_request = time.monotonic()
53
+
54
+ def slow_down(self):
55
+ with self.lock:
56
+ if self.interval > 0:
57
+ self.interval = min(self.interval * 2, MAX_RATE_LIMIT_INTERVAL)
58
+
59
+
60
+ _rate_limiter = RateLimiter()
61
+
62
+
63
+ def set_rate_limit(interval):
64
+ """Set the minimum interval, in seconds, between requests to the eBird API.
65
+
66
+ The limit applies to all requests made by the process. Setting it to
67
+ zero, the default, disables rate limiting. If the API responds with
68
+ HTTP 429 Too Many Requests, the interval is doubled, up to a maximum
69
+ of MAX_RATE_LIMIT_INTERVAL, for the rest of the process, or until the
70
+ rate limit is set again.
71
+
72
+ :param interval: the number of seconds between requests.
73
+ :type interval: float
74
+
75
+ """
76
+ if interval < 0:
77
+ raise ValueError("The rate limit interval cannot be negative")
78
+ with _rate_limiter.lock:
79
+ _rate_limiter.interval = float(interval)
80
+
81
+
82
+ def get_rate_limit():
83
+ """Get the current minimum interval, in seconds, between requests.
84
+
85
+ :return: the interval, which may have increased since it was set, if the
86
+ eBird API responded with HTTP 429 Too Many Requests.
87
+ :rtype: float
88
+
89
+ """
90
+ return _rate_limiter.interval
91
+
92
+
93
+ def get_retry_after(exc):
94
+ """Get the number of seconds to wait from a Retry-After header.
95
+
96
+ :param exc: the error raised for the HTTP 429 response.
97
+ :type exc: HTTPError
98
+
99
+ :return: the number of seconds to wait, or None if the header was not
100
+ present or could not be parsed.
101
+ :rtype: float | None
102
+
103
+ """
104
+ value = exc.headers.get("Retry-After") if exc.headers else None
105
+ if not value:
106
+ return None
107
+ try:
108
+ return max(float(value), 0.0)
109
+ except ValueError:
110
+ pass
111
+ try:
112
+ when = parsedate_to_datetime(value)
113
+ except (TypeError, ValueError):
114
+ return None
115
+ if when.tzinfo is None:
116
+ when = when.replace(tzinfo=dt.timezone.utc)
117
+ return max((when - dt.datetime.now(dt.timezone.utc)).total_seconds(), 0.0)
118
+
119
+
11
120
  _parameter_defaults = {
12
121
  "back": constants.DEFAULT_BACK,
13
122
  "cat": constants.DEFAULT_SPECIES_CATEGORY,
@@ -101,6 +210,9 @@ def get_response(url, params=None, headers=None, max_retries=3, backoff_factor=1
101
210
  :raises URLError if there is an error with the connection to the
102
211
  eBird site.
103
212
 
213
+ :raises RateLimitError if the eBird API still returns HTTP 429 Too Many
214
+ Requests after all the retries have been used.
215
+
104
216
  :raises HTTPError if the eBird API returns an error.
105
217
 
106
218
  """
@@ -114,14 +226,21 @@ def get_response(url, params=None, headers=None, max_retries=3, backoff_factor=1
114
226
  request.add_header(name, value)
115
227
 
116
228
  for attempt in range(max_retries + 1):
229
+ _rate_limiter.wait()
117
230
  try:
118
231
  return urlopen(request).read()
119
232
  except HTTPError as exc:
120
- if attempt == max_retries:
233
+ retry_after = None
234
+ if exc.code == 429:
235
+ retry_after = get_retry_after(exc)
236
+ _rate_limiter.slow_down()
237
+ if attempt == max_retries:
238
+ raise RateLimitError(exc, retry_after) from exc
239
+ elif attempt == max_retries:
121
240
  raise
122
- retry_after = exc.headers.get("Retry-After") if exc.code == 429 else None
123
- delay = float(retry_after) if retry_after else backoff_factor * (2**attempt)
124
- time.sleep(delay)
241
+ if retry_after is None:
242
+ retry_after = backoff_factor * (2**attempt)
243
+ time.sleep(retry_after)
125
244
 
126
245
 
127
246
  def get_json(content):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ebird-api-requests
3
- Version: 4.2.0
3
+ Version: 4.3.0
4
4
  Summary: A python library for accessing the eBird API, version 2.0
5
5
  Author-email: Stuart MacKay <smackay@flagstonesoftware.com>
6
6
  License: MIT License
@@ -346,6 +346,30 @@ client.get_observations('MX-OAX')
346
346
 
347
347
  The client supports all the API functions.
348
348
 
349
+ ### Rate Limiting
350
+
351
+ The eBird API limits how quickly requests can be made. If you exceed the
352
+ limit the API responds with HTTP 429 Too Many Requests. If you are making
353
+ lots of calls, for example downloading all the checklists for a region,
354
+ then set a minimum interval, in seconds, between requests:
355
+
356
+ ```python
357
+ from ebird.api.requests import set_rate_limit
358
+
359
+ set_rate_limit(1.0)
360
+ ```
361
+
362
+ The limit applies to all the requests made by the process. It is disabled
363
+ by default. If the API still responds with HTTP 429, the interval is doubled,
364
+ up to a maximum of 60 seconds, so the requests slow down to a rate the API
365
+ accepts. Use get_rate_limit() to see the current interval.
366
+
367
+ When an HTTP 429 response is returned, the request is retried after waiting
368
+ for the time given in the Retry-After header. If the request still fails
369
+ after all the retries are used, a RateLimitError is raised. It is a subclass
370
+ of HTTPError, and the retry_after attribute contains the number of seconds
371
+ the API asked you to wait, or None if no Retry-After header was returned.
372
+
349
373
  ## Formats
350
374
 
351
375
  Most of the eBird API calls return JSON. Some of the calls such as getting