ebird-api-requests 4.0.0__py3-none-any.whl
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.
- ebird/api/requests/__init__.py +39 -0
- ebird/api/requests/checklists.py +101 -0
- ebird/api/requests/client.py +410 -0
- ebird/api/requests/constants.py +108 -0
- ebird/api/requests/hotspots.py +239 -0
- ebird/api/requests/observations.py +707 -0
- ebird/api/requests/regions.py +110 -0
- ebird/api/requests/species.py +37 -0
- ebird/api/requests/statistics.py +90 -0
- ebird/api/requests/taxonomy.py +213 -0
- ebird/api/requests/utils.py +164 -0
- ebird/api/requests/validation.py +348 -0
- ebird_api_requests-4.0.0.dist-info/METADATA +389 -0
- ebird_api_requests-4.0.0.dist-info/RECORD +17 -0
- ebird_api_requests-4.0.0.dist-info/WHEEL +5 -0
- ebird_api_requests-4.0.0.dist-info/licenses/LICENSE.txt +21 -0
- ebird_api_requests-4.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
# pylint: disable=C0111
|
|
2
|
+
|
|
3
|
+
"""Functions for validation."""
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from datetime import date, datetime
|
|
7
|
+
from enum import Enum
|
|
8
|
+
|
|
9
|
+
from ebird.api.requests import constants
|
|
10
|
+
|
|
11
|
+
_locales = constants.LOCALES.values()
|
|
12
|
+
_region_types = ", ".join(constants.REGION_TYPES)
|
|
13
|
+
_sort_list = ", ".join(constants.SPECIES_SORT)
|
|
14
|
+
_species_categories = ", ".join(constants.SPECIES_CATEGORIES)
|
|
15
|
+
_species_ordering = ", ".join(constants.SPECIES_ORDERING)
|
|
16
|
+
_top_100_rank = ", ".join(constants.TOP_100_RANK)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Transform(Enum):
|
|
20
|
+
NONE = 1
|
|
21
|
+
LOWER = 2
|
|
22
|
+
UPPER = 3
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def is_country(value):
|
|
26
|
+
return re.match(r"^[A-Z]{2}$", value)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def is_subnational1(value):
|
|
30
|
+
return re.match(r"^[A-Z]{2}-[A-Z0-9]{2,3}$", value)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def is_subnational2(value):
|
|
34
|
+
return re.match(r"^[A-Z]{2}-[A-Z0-9]{2,3}-[A-Z0-9]{2,3}$", value)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def is_region(value):
|
|
38
|
+
return (
|
|
39
|
+
re.match(r"^[A-Z]{2}$", value)
|
|
40
|
+
or re.match(r"^[A-Z]{2}-[A-Z0-9]{2,3}$", value)
|
|
41
|
+
or re.match(r"^[A-Z]{2}-[A-Z0-9]{2,3}-[A-Z0-9]{2,3}$", value)
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def is_location(value):
|
|
46
|
+
return re.match(r"^L\d+$", value)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_location_type(value):
|
|
50
|
+
if not value or not isinstance(value, str):
|
|
51
|
+
raise ValueError("Location type must be a string.")
|
|
52
|
+
if is_country(value):
|
|
53
|
+
result = "country"
|
|
54
|
+
elif is_subnational1(value):
|
|
55
|
+
result = "subnational1"
|
|
56
|
+
elif is_subnational2(value):
|
|
57
|
+
result = "subnational2"
|
|
58
|
+
elif is_location(value):
|
|
59
|
+
result = "location"
|
|
60
|
+
else:
|
|
61
|
+
result = None
|
|
62
|
+
return result
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_location_types(items):
|
|
66
|
+
results = set()
|
|
67
|
+
for item in items:
|
|
68
|
+
results.add(get_location_type(item))
|
|
69
|
+
return list(results)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def clean_code(value, transform=Transform.NONE):
|
|
73
|
+
if not value or not isinstance(value, str):
|
|
74
|
+
raise ValueError("Code must be a string.")
|
|
75
|
+
if transform == Transform.LOWER:
|
|
76
|
+
value = value.lower()
|
|
77
|
+
elif transform == Transform.UPPER:
|
|
78
|
+
value = value.upper()
|
|
79
|
+
return value.strip()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def clean_codes(value, transform=Transform.NONE):
|
|
83
|
+
if isinstance(value, str):
|
|
84
|
+
items = value.split(",")
|
|
85
|
+
elif isinstance(value, list):
|
|
86
|
+
items = value
|
|
87
|
+
else:
|
|
88
|
+
raise ValueError("Must be a comma-separated string or list of names")
|
|
89
|
+
|
|
90
|
+
return [clean_code(code, transform=transform) for code in items]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def clean_lat(value):
|
|
94
|
+
try:
|
|
95
|
+
cleaned = float(value)
|
|
96
|
+
if not -90 <= cleaned <= 90:
|
|
97
|
+
raise ValueError()
|
|
98
|
+
cleaned = "%.2f" % round(cleaned, 2)
|
|
99
|
+
except ValueError as err:
|
|
100
|
+
err.message = (
|
|
101
|
+
"Value for 'lat', %s, must be a decimal number"
|
|
102
|
+
" in the range -90.00 to 90.00" % value
|
|
103
|
+
)
|
|
104
|
+
raise
|
|
105
|
+
|
|
106
|
+
return cleaned
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def clean_lng(value):
|
|
110
|
+
try:
|
|
111
|
+
cleaned = float(value)
|
|
112
|
+
if not -180 <= cleaned <= 180:
|
|
113
|
+
raise ValueError()
|
|
114
|
+
cleaned = "%.2f" % round(cleaned, 2)
|
|
115
|
+
except ValueError as err:
|
|
116
|
+
err.message = (
|
|
117
|
+
"Value for 'lon', %s, must be a decimal number"
|
|
118
|
+
" in the range -180.00 to 180.00" % value
|
|
119
|
+
)
|
|
120
|
+
raise
|
|
121
|
+
|
|
122
|
+
return cleaned
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def clean_dist(value):
|
|
126
|
+
try:
|
|
127
|
+
cleaned = int(value)
|
|
128
|
+
if not 0 <= cleaned <= 50:
|
|
129
|
+
raise ValueError()
|
|
130
|
+
except ValueError as err:
|
|
131
|
+
err.message = (
|
|
132
|
+
"Value for 'dist', %s is not an integer in the" " range 0..50" % value
|
|
133
|
+
)
|
|
134
|
+
raise
|
|
135
|
+
|
|
136
|
+
return cleaned
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def clean_back(value):
|
|
140
|
+
try:
|
|
141
|
+
cleaned = int(value)
|
|
142
|
+
if not 1 <= cleaned <= 30:
|
|
143
|
+
raise ValueError()
|
|
144
|
+
except ValueError as err:
|
|
145
|
+
err.message = (
|
|
146
|
+
"Value for 'back', %s, is not an integer in the" " range 1..30" % value
|
|
147
|
+
)
|
|
148
|
+
raise
|
|
149
|
+
|
|
150
|
+
return cleaned
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def clean_max_results(value, limit):
|
|
154
|
+
try:
|
|
155
|
+
cleaned = None if value is None else int(value)
|
|
156
|
+
if cleaned is not None:
|
|
157
|
+
if not 1 <= cleaned <= limit:
|
|
158
|
+
raise ValueError()
|
|
159
|
+
except ValueError as err:
|
|
160
|
+
err.message = (
|
|
161
|
+
"Value for 'max_results', %s, is not None or an"
|
|
162
|
+
" integer in the range 1..%d" % (value, limit)
|
|
163
|
+
)
|
|
164
|
+
raise
|
|
165
|
+
|
|
166
|
+
return cleaned
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def clean_max_observations(value):
|
|
170
|
+
return clean_max_results(value, 10000)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def clean_max_observers(value):
|
|
174
|
+
return clean_max_results(value, 100)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def clean_max_checklists(value):
|
|
178
|
+
return clean_max_results(value, constants.API_MAX_RESULTS)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def clean_locale(value):
|
|
182
|
+
cleaned = str(value).strip()
|
|
183
|
+
if re.match(r"^[a-zA-Z]{2}$", cleaned):
|
|
184
|
+
cleaned = cleaned.lower()
|
|
185
|
+
elif re.match(r"^[a-zA-Z]{2}_[a-zA-Z]{2,3}$", cleaned):
|
|
186
|
+
cleaned = cleaned[:2].lower() + "_" + cleaned[3:].upper()
|
|
187
|
+
|
|
188
|
+
if cleaned not in _locales:
|
|
189
|
+
raise ValueError("eBird does not support this locale: %s" % cleaned)
|
|
190
|
+
|
|
191
|
+
return cleaned
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def clean_detail(value):
|
|
195
|
+
cleaned = clean_code(value, transform=Transform.LOWER)
|
|
196
|
+
if cleaned not in ("simple", "full"):
|
|
197
|
+
raise ValueError(
|
|
198
|
+
"Value for 'detail', %s, must be either 'simple' or 'full'" % value
|
|
199
|
+
)
|
|
200
|
+
return cleaned
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def clean_provisional(value):
|
|
204
|
+
return "true" if bool(value) else "false"
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def clean_hotspot(value):
|
|
208
|
+
return "true" if bool(value) else "false"
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def clean_location(value):
|
|
212
|
+
cleaned = clean_code(value, transform=Transform.UPPER)
|
|
213
|
+
if not is_location(cleaned):
|
|
214
|
+
raise ValueError("Invalid location identifier: %s" % cleaned)
|
|
215
|
+
return cleaned
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def clean_locations(values):
|
|
219
|
+
cleaned = clean_codes(values, transform=Transform.UPPER)
|
|
220
|
+
|
|
221
|
+
if len(cleaned) > 10:
|
|
222
|
+
raise ValueError("List of locations cannot be longer than 10")
|
|
223
|
+
|
|
224
|
+
for code in cleaned:
|
|
225
|
+
if not is_location(code):
|
|
226
|
+
raise ValueError("%s is not a location code" % code)
|
|
227
|
+
|
|
228
|
+
return cleaned
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def clean_region(value):
|
|
232
|
+
cleaned = clean_code(value)
|
|
233
|
+
if cleaned == "WORLD":
|
|
234
|
+
cleaned = cleaned.lower()
|
|
235
|
+
elif cleaned != "world":
|
|
236
|
+
cleaned = cleaned.upper()
|
|
237
|
+
if not is_region(cleaned):
|
|
238
|
+
raise ValueError(
|
|
239
|
+
"Value for 'region', %s, must be a country, e.g. 'US',"
|
|
240
|
+
"subnational1, e.g. 'US-NV' or subnational2, e.g. 'US-NV-211'"
|
|
241
|
+
)
|
|
242
|
+
return cleaned
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def clean_region_type(value):
|
|
246
|
+
cleaned = clean_code(value, transform=Transform.LOWER)
|
|
247
|
+
if cleaned not in constants.REGION_TYPES:
|
|
248
|
+
raise ValueError(
|
|
249
|
+
"Region type, %s, must be one or more of : %s" % (value, _region_types)
|
|
250
|
+
)
|
|
251
|
+
return cleaned
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def clean_area(value):
|
|
255
|
+
cleaned = clean_code(value, transform=Transform.UPPER)
|
|
256
|
+
area_type = get_location_type(cleaned)
|
|
257
|
+
|
|
258
|
+
if area_type not in ["country", "subnational1", "subnational2", "location"]:
|
|
259
|
+
raise ValueError("Unknown type of area: %s" % area_type)
|
|
260
|
+
|
|
261
|
+
return cleaned
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def clean_areas(values):
|
|
265
|
+
cleaned = clean_codes(values, transform=Transform.UPPER)
|
|
266
|
+
types = get_location_types(cleaned)
|
|
267
|
+
|
|
268
|
+
if len(types) > 1:
|
|
269
|
+
raise ValueError("You cannot mix different types of area together")
|
|
270
|
+
else:
|
|
271
|
+
if types[0] not in ["country", "subnational1", "subnational2", "location"]:
|
|
272
|
+
raise ValueError("Unknown type of area")
|
|
273
|
+
|
|
274
|
+
if len(cleaned) > 10:
|
|
275
|
+
raise ValueError("List of areas cannot be longer than 10")
|
|
276
|
+
|
|
277
|
+
return cleaned
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def clean_categories(value):
|
|
281
|
+
cleaned = clean_codes(value, transform=Transform.LOWER)
|
|
282
|
+
for entry in cleaned:
|
|
283
|
+
if entry not in constants.SPECIES_CATEGORIES:
|
|
284
|
+
raise ValueError(
|
|
285
|
+
"Species category, %s, must be one or more of : %s"
|
|
286
|
+
% (entry, _species_categories)
|
|
287
|
+
)
|
|
288
|
+
return cleaned
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def clean_ordering(value):
|
|
292
|
+
cleaned = clean_code(value, transform=Transform.LOWER)
|
|
293
|
+
if cleaned not in constants.SPECIES_ORDERING:
|
|
294
|
+
raise ValueError(
|
|
295
|
+
"Species ordering, %s, must be one or more of : %s"
|
|
296
|
+
% (value, _species_ordering)
|
|
297
|
+
)
|
|
298
|
+
return cleaned
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def clean_sort(value):
|
|
302
|
+
cleaned = clean_code(value, transform=Transform.LOWER)
|
|
303
|
+
if cleaned not in constants.SPECIES_SORT:
|
|
304
|
+
raise ValueError(
|
|
305
|
+
"Species sort, %s, must be one or more of : %s" % (value, _sort_list)
|
|
306
|
+
)
|
|
307
|
+
return cleaned
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def clean_species_code(value):
|
|
311
|
+
cleaned = clean_code(value, transform=Transform.LOWER)
|
|
312
|
+
if re.match(r"^\w{6}$", cleaned):
|
|
313
|
+
return cleaned
|
|
314
|
+
|
|
315
|
+
raise ValueError(
|
|
316
|
+
"Value for 'species code', %s, must be 6 letters, e.g. 'cangoo'" % value
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def clean_date(value):
|
|
321
|
+
if isinstance(value, str):
|
|
322
|
+
cleaned = datetime.strptime(value, "%Y-%m-%d").date()
|
|
323
|
+
elif isinstance(value, datetime):
|
|
324
|
+
cleaned = value.date()
|
|
325
|
+
elif isinstance(value, date):
|
|
326
|
+
cleaned = value
|
|
327
|
+
else:
|
|
328
|
+
raise ValueError(
|
|
329
|
+
"Date must be a string ('YYYY-mm-dd'),"
|
|
330
|
+
" a date or a datetime: %s" % str(value)
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
if cleaned.year < 1800:
|
|
334
|
+
raise ValueError("Dates cannot be earlier than Jan 1st 1800")
|
|
335
|
+
|
|
336
|
+
if cleaned > date.today():
|
|
337
|
+
raise ValueError("Date is in the future: %s" % cleaned.strftime("%Y-%m-%d"))
|
|
338
|
+
|
|
339
|
+
return cleaned.strftime("%Y/%m/%d")
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def clean_rank(value):
|
|
343
|
+
cleaned = clean_code(value, transform=Transform.LOWER)
|
|
344
|
+
if cleaned not in constants.TOP_100_RANK:
|
|
345
|
+
raise ValueError(
|
|
346
|
+
"Top 100 rank, %s, must be one or more of : %s" % (value, _top_100_rank)
|
|
347
|
+
)
|
|
348
|
+
return cleaned
|