ebird-api-data 0.4.0__tar.gz → 0.5.1__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.
- {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/PKG-INFO +15 -2
- {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/README.md +13 -0
- {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/pyproject.toml +3 -3
- ebird_api_data-0.5.1/src/ebird/api/data/__init__.py +1 -0
- {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/admin.py +2 -15
- {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/loaders.py +159 -88
- ebird_api_data-0.5.1/src/ebird/api/data/migrations/0003_remove_checklist_published_and_more.py +24 -0
- ebird_api_data-0.5.1/src/ebird/api/data/migrations/0004_remove_observer_multiple.py +17 -0
- {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/models/__init__.py +0 -2
- {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/models/checklist.py +0 -5
- {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/models/observation.py +0 -5
- {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/models/observer.py +0 -6
- ebird_api_data-0.4.0/src/ebird/api/data/__init__.py +0 -1
- ebird_api_data-0.4.0/src/ebird/api/data/factories.py +0 -432
- ebird_api_data-0.4.0/src/ebird/api/data/faker_providers.py +0 -345
- ebird_api_data-0.4.0/src/ebird/api/data/fixtures/README.md +0 -355
- ebird_api_data-0.4.0/src/ebird/api/data/fixtures/generate_portugal_data.py +0 -530
- ebird_api_data-0.4.0/src/ebird/api/data/fixtures/portugal_observations.json +0 -144813
- ebird_api_data-0.4.0/src/ebird/api/data/models/filter.py +0 -116
- ebird_api_data-0.4.0/src/ebird/api/data/models/models.md +0 -242
- {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/.gitignore +0 -0
- {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/LICENSE +0 -0
- {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/apps.py +0 -0
- {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/fields.py +0 -0
- {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/locale/en/LC_MESSAGES/django.mo +0 -0
- {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/locale/en/LC_MESSAGES/django.po +0 -0
- {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/management/__init__.py +0 -0
- {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/management/commands/__init__.py +0 -0
- {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/management/commands/add_checklists.py +0 -0
- {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/migrations/0001_initial.py +0 -0
- {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/migrations/0002_expand_filter.py +0 -0
- {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/migrations/__init__.py +0 -0
- {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/models/country.py +0 -0
- {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/models/county.py +0 -0
- {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/models/location.py +0 -0
- {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/models/species.py +0 -0
- {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/models/state.py +0 -0
- {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/templates/admin/data/species/change_list_object_tools.html +0 -0
- {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/templates/admin/data/species/fetch_species.html +0 -0
- {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/templates/data/widgets/translation_field.html +0 -0
- {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/widgets.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ebird-api-data
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.1
|
|
4
4
|
Summary: eBird API Data is a reusable Django app for loading data from eBird into a database.
|
|
5
5
|
Author-email: Stuart MacKay <smackay@flagstonesoftware.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -23,7 +23,7 @@ Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
|
23
23
|
Requires-Python: >=3.10
|
|
24
24
|
Requires-Dist: beautifulsoup4>=4.13.3
|
|
25
25
|
Requires-Dist: django>=4.2
|
|
26
|
-
Requires-Dist: ebird-api-requests
|
|
26
|
+
Requires-Dist: ebird-api-requests>=4.1.0
|
|
27
27
|
Requires-Dist: lxml>=6.0.0
|
|
28
28
|
Description-Content-Type: text/markdown
|
|
29
29
|
|
|
@@ -158,6 +158,19 @@ python manage.py runserver
|
|
|
158
158
|
Now, either visit the site, http:localhost:8000/, or log into the Django Admin,
|
|
159
159
|
http:localhost:8000/admin to browse the tables.
|
|
160
160
|
|
|
161
|
+
IMPORTANT: There is a serious flaw in the eBird API - observers are identified
|
|
162
|
+
by name. That means if more than one observer shares the same name then only
|
|
163
|
+
one Observer record will be added to the database, and all the checklists will
|
|
164
|
+
be assigned to them. In practice, most people ensure their name is unique by
|
|
165
|
+
adding middle initial's etc. However it is a problem with "Anonymous eBirder".
|
|
166
|
+
The solution is to add two Observer records with the name "Anonymous eBirder",
|
|
167
|
+
and a fake identifier, e.g. ANON1, ANON2, etc. That way the loader will first
|
|
168
|
+
scrape the web page for the checklist where the identifier, e.g. USER2743448,
|
|
169
|
+
can be found, and so the checklist will be assigned to the correct Observer.
|
|
170
|
+
Use this workaround for any other observers you encounter that share the same
|
|
171
|
+
name. This won't work retroactively, but then you can delete the checklists,
|
|
172
|
+
and manually reload them.
|
|
173
|
+
|
|
161
174
|
## Project Information
|
|
162
175
|
|
|
163
176
|
* Documentation: https://ebird-api-data.readthedocs.io/en/latest/
|
|
@@ -129,6 +129,19 @@ python manage.py runserver
|
|
|
129
129
|
Now, either visit the site, http:localhost:8000/, or log into the Django Admin,
|
|
130
130
|
http:localhost:8000/admin to browse the tables.
|
|
131
131
|
|
|
132
|
+
IMPORTANT: There is a serious flaw in the eBird API - observers are identified
|
|
133
|
+
by name. That means if more than one observer shares the same name then only
|
|
134
|
+
one Observer record will be added to the database, and all the checklists will
|
|
135
|
+
be assigned to them. In practice, most people ensure their name is unique by
|
|
136
|
+
adding middle initial's etc. However it is a problem with "Anonymous eBirder".
|
|
137
|
+
The solution is to add two Observer records with the name "Anonymous eBirder",
|
|
138
|
+
and a fake identifier, e.g. ANON1, ANON2, etc. That way the loader will first
|
|
139
|
+
scrape the web page for the checklist where the identifier, e.g. USER2743448,
|
|
140
|
+
can be found, and so the checklist will be assigned to the correct Observer.
|
|
141
|
+
Use this workaround for any other observers you encounter that share the same
|
|
142
|
+
name. This won't work retroactively, but then you can delete the checklists,
|
|
143
|
+
and manually reload them.
|
|
144
|
+
|
|
132
145
|
## Project Information
|
|
133
146
|
|
|
134
147
|
* Documentation: https://ebird-api-data.readthedocs.io/en/latest/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ebird-api-data"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.5.1"
|
|
4
4
|
description = "eBird API Data is a reusable Django app for loading data from eBird into a database."
|
|
5
5
|
keywords = ["checklists", "observations", "species", "Django", "eBird", "models"]
|
|
6
6
|
license = "MIT"
|
|
@@ -13,7 +13,7 @@ requires-python = ">=3.10"
|
|
|
13
13
|
dependencies = [
|
|
14
14
|
"beautifulsoup4>=4.13.3",
|
|
15
15
|
"django>=4.2",
|
|
16
|
-
"ebird-api-requests
|
|
16
|
+
"ebird-api-requests>=4.1.0",
|
|
17
17
|
"lxml>=6.0.0",
|
|
18
18
|
]
|
|
19
19
|
classifiers = [
|
|
@@ -64,7 +64,7 @@ dev-dependencies = [
|
|
|
64
64
|
]
|
|
65
65
|
|
|
66
66
|
[tool.bumpversion]
|
|
67
|
-
current_version = "0.
|
|
67
|
+
current_version = "0.5.1"
|
|
68
68
|
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
|
|
69
69
|
serialize = ["{major}.{minor}.{patch}"]
|
|
70
70
|
ignore_missing_version = false
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.5.1"
|
|
@@ -266,10 +266,10 @@ class ObservationAdmin(admin.ModelAdmin):
|
|
|
266
266
|
|
|
267
267
|
@admin.register(models.Observer)
|
|
268
268
|
class ObserverAdmin(admin.ModelAdmin):
|
|
269
|
-
list_display = ("name", "identifier", "
|
|
269
|
+
list_display = ("name", "identifier", "enabled")
|
|
270
270
|
ordering = ("name",)
|
|
271
271
|
search_fields = ("name", "identifier")
|
|
272
|
-
list_filter = ("
|
|
272
|
+
list_filter = ("enabled",)
|
|
273
273
|
form = ObservationForm
|
|
274
274
|
|
|
275
275
|
def formfield_for_dbfield(self, db_field, request, **kwargs):
|
|
@@ -370,16 +370,3 @@ class SpeciesAdmin(admin.ModelAdmin):
|
|
|
370
370
|
extra_context = extra_context or {}
|
|
371
371
|
extra_context["fetch_species_url"] = reverse("admin:data_species_fetch")
|
|
372
372
|
return super().changelist_view(request, extra_context=extra_context)
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
@admin.register(models.Filter)
|
|
376
|
-
class FilterAdmin(admin.ModelAdmin):
|
|
377
|
-
list_display = ("name", "enabled")
|
|
378
|
-
search_fields = ("name", "species__common_name")
|
|
379
|
-
list_filter = ("enabled",)
|
|
380
|
-
autocomplete_fields = ("species", "location", "update_species")
|
|
381
|
-
formfield_overrides = {
|
|
382
|
-
TextField: {
|
|
383
|
-
"widget": TextInput(attrs={"class": "vTextField"}),
|
|
384
|
-
}
|
|
385
|
-
}
|
|
@@ -25,7 +25,6 @@ from .models import (
|
|
|
25
25
|
Checklist,
|
|
26
26
|
Country,
|
|
27
27
|
County,
|
|
28
|
-
Filter,
|
|
29
28
|
Location,
|
|
30
29
|
Observation,
|
|
31
30
|
Observer,
|
|
@@ -144,17 +143,16 @@ class APILoader:
|
|
|
144
143
|
return self.call(func, self.api_key, *args, **kwargs)
|
|
145
144
|
|
|
146
145
|
@staticmethod
|
|
147
|
-
def update(obj, values: dict) ->
|
|
148
|
-
|
|
149
|
-
updated = False
|
|
146
|
+
def update(obj, values: dict) -> dict[str, tuple]:
|
|
147
|
+
changed: dict[str, tuple] = {}
|
|
150
148
|
for key, value in values.items():
|
|
151
149
|
current = getattr(obj, key)
|
|
152
150
|
if current != value:
|
|
153
151
|
setattr(obj, key, value)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
return
|
|
152
|
+
changed[key] = (current, value)
|
|
153
|
+
if changed:
|
|
154
|
+
obj.save()
|
|
155
|
+
return changed
|
|
158
156
|
|
|
159
157
|
def get_country(self, data: dict) -> Country:
|
|
160
158
|
code: str = data["countryCode"]
|
|
@@ -164,10 +162,11 @@ class APILoader:
|
|
|
164
162
|
}
|
|
165
163
|
country, created = Country.objects.get_or_create(code=code, defaults=values)
|
|
166
164
|
if created:
|
|
167
|
-
logger.info("
|
|
168
|
-
elif self.update(country, values):
|
|
169
|
-
|
|
170
|
-
|
|
165
|
+
logger.info("Country %s: added", code)
|
|
166
|
+
elif changed := self.update(country, values):
|
|
167
|
+
if "name" in changed:
|
|
168
|
+
old, new = changed["name"]
|
|
169
|
+
logger.info("Country %s: renamed, %s -> %s", code, old, new)
|
|
171
170
|
return country
|
|
172
171
|
|
|
173
172
|
def get_state(self, data: dict) -> State:
|
|
@@ -178,10 +177,11 @@ class APILoader:
|
|
|
178
177
|
}
|
|
179
178
|
state, created = State.objects.get_or_create(code=code, defaults=values)
|
|
180
179
|
if created:
|
|
181
|
-
logger.info("
|
|
182
|
-
elif self.update(state, values):
|
|
183
|
-
|
|
184
|
-
|
|
180
|
+
logger.info("State: %s: added", code)
|
|
181
|
+
elif changed := self.update(state, values):
|
|
182
|
+
if "name" in changed:
|
|
183
|
+
old, new = changed["name"]
|
|
184
|
+
logger.info("State %s: renamed, %s -> %s", code, old, new)
|
|
185
185
|
return state
|
|
186
186
|
|
|
187
187
|
def get_county(self, data) -> County:
|
|
@@ -193,10 +193,11 @@ class APILoader:
|
|
|
193
193
|
}
|
|
194
194
|
county, created = County.objects.get_or_create(code=code, defaults=values)
|
|
195
195
|
if created:
|
|
196
|
-
logger.info("
|
|
197
|
-
elif self.update(county, values):
|
|
198
|
-
|
|
199
|
-
|
|
196
|
+
logger.info("County %s: added", code)
|
|
197
|
+
elif changed := self.update(county, values):
|
|
198
|
+
if "name" in changed:
|
|
199
|
+
old, new = changed["name"]
|
|
200
|
+
logger.info("County %s: renamed, %s -> %s", code, old, new)
|
|
200
201
|
return county
|
|
201
202
|
|
|
202
203
|
def add_location(self, data: dict) -> Location:
|
|
@@ -223,13 +224,17 @@ class APILoader:
|
|
|
223
224
|
)
|
|
224
225
|
|
|
225
226
|
if not created:
|
|
226
|
-
values.pop("name")
|
|
227
|
+
values.pop("name") # Don't overwrite the name
|
|
227
228
|
|
|
228
229
|
if created:
|
|
229
|
-
logger.info("
|
|
230
|
-
elif self.update(location, values):
|
|
231
|
-
|
|
232
|
-
|
|
230
|
+
logger.info("Location %s: added %s", identifier, values["name"])
|
|
231
|
+
elif changed := self.update(location, values):
|
|
232
|
+
included = ["name", "hotspot", "county", "state", "country"]
|
|
233
|
+
filtered = {key: value for key, value in changed.items() if key in included}
|
|
234
|
+
for key, (old, new) in filtered.items():
|
|
235
|
+
logger.info(
|
|
236
|
+
"Location %s: changed %s, %s -> %s", identifier, key, old, new
|
|
237
|
+
)
|
|
233
238
|
return location
|
|
234
239
|
|
|
235
240
|
def add_species(self, code: str) -> Species:
|
|
@@ -260,7 +265,7 @@ class APILoader:
|
|
|
260
265
|
values["family_common_name"] = json.dumps(values["family_common_name"])
|
|
261
266
|
|
|
262
267
|
species = Species.objects.create(species_code=code, **values)
|
|
263
|
-
logger.info("
|
|
268
|
+
logger.info("Species added: %s, %s", code, species.get_common_name())
|
|
264
269
|
|
|
265
270
|
return species
|
|
266
271
|
|
|
@@ -282,7 +287,6 @@ class APILoader:
|
|
|
282
287
|
|
|
283
288
|
values: dict = {
|
|
284
289
|
"edited": checklist.edited,
|
|
285
|
-
"published": False,
|
|
286
290
|
"checklist": checklist,
|
|
287
291
|
"country": checklist.country,
|
|
288
292
|
"state": checklist.state,
|
|
@@ -315,49 +319,93 @@ class APILoader:
|
|
|
315
319
|
observation, created = Observation.objects.get_or_create(
|
|
316
320
|
identifier=identifier, defaults=values
|
|
317
321
|
)
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
322
|
+
|
|
323
|
+
# Log observations that were added after the checklist was first loaded.
|
|
324
|
+
if created and checklist.created < checklist.edited:
|
|
325
|
+
logger.info(
|
|
326
|
+
"Checklist %s: added %s (%d)",
|
|
327
|
+
checklist.identifier,
|
|
328
|
+
species,
|
|
329
|
+
values["count"],
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
if changed := self.update(observation, values):
|
|
333
|
+
for field in ["audio", "photo", "video"]:
|
|
334
|
+
if field in changed:
|
|
335
|
+
old, new = changed[field]
|
|
336
|
+
logger.info(
|
|
337
|
+
"Checklist %s: %s, %s %s",
|
|
338
|
+
checklist.identifier,
|
|
339
|
+
species,
|
|
340
|
+
"added" if new else "deleted",
|
|
341
|
+
field,
|
|
342
|
+
)
|
|
343
|
+
if "count" in changed:
|
|
344
|
+
old, new = changed["count"]
|
|
345
|
+
logger.info(
|
|
346
|
+
"Checklist %s: %s, changed count, %s -> %s",
|
|
347
|
+
checklist.identifier,
|
|
348
|
+
species,
|
|
349
|
+
old,
|
|
350
|
+
new,
|
|
351
|
+
)
|
|
352
|
+
if "species" in changed:
|
|
353
|
+
old, new = changed["species"]
|
|
354
|
+
logger.info(
|
|
355
|
+
"Checklist %s: changed species, %s -> %s",
|
|
356
|
+
checklist.identifier,
|
|
357
|
+
old,
|
|
358
|
+
new,
|
|
359
|
+
)
|
|
324
360
|
return observation
|
|
325
361
|
|
|
326
362
|
def get_observer_identifier(self, data) -> str:
|
|
327
|
-
|
|
328
|
-
logger.info("Scraping checklist: %s",
|
|
363
|
+
checklist_identifier: str = data["subId"]
|
|
364
|
+
logger.info("Scraping checklist: %s", checklist_identifier)
|
|
329
365
|
response = self.call(
|
|
330
|
-
requests.get, "https://ebird.org/checklist/%s" %
|
|
366
|
+
requests.get, "https://ebird.org/checklist/%s" % checklist_identifier
|
|
331
367
|
)
|
|
332
368
|
content = response.content
|
|
333
369
|
soup = BeautifulSoup(content, "lxml")
|
|
334
370
|
attribute = "data-participant-userid"
|
|
335
371
|
node = soup.find("span", attrs={attribute: True})
|
|
336
|
-
|
|
372
|
+
identifier = node[attribute] if node else ""
|
|
373
|
+
if not identifier:
|
|
374
|
+
logger.error("Observer Identifier: not found")
|
|
375
|
+
return identifier
|
|
337
376
|
|
|
338
377
|
def get_observer(self, data: dict) -> Observer:
|
|
378
|
+
# In theory, there is a serious problem here, as observers can have
|
|
379
|
+
# the same name. There is no way to resolve this except to prime the
|
|
380
|
+
# database with multiple Observers sharing the same name. That will
|
|
381
|
+
# force the identifier to be scraped from the checklist web page and
|
|
382
|
+
# so the correct "John Smith" will be used. In practice, apart from
|
|
383
|
+
# Anonymous eBirder, this is not a problem. Most observers want their
|
|
384
|
+
# name to be unique (so they show up in the leader boards), and so they
|
|
385
|
+
# will add middle initials, etc. so the names are generally unique.
|
|
386
|
+
|
|
339
387
|
name: str = data.get("userDisplayName", "Anonymous eBirder")
|
|
340
|
-
|
|
341
|
-
|
|
388
|
+
number_of_observers = Observer.objects.filter(original=name).count()
|
|
389
|
+
|
|
390
|
+
if number_of_observers == 0:
|
|
391
|
+
identifier = self.get_observer_identifier(data)
|
|
392
|
+
# The observer might have changed their name.
|
|
393
|
+
observer, created = Observer.objects.update_or_create(
|
|
394
|
+
identifier=identifier, defaults={"name": name, "original": name}
|
|
395
|
+
)
|
|
396
|
+
elif number_of_observers == 1:
|
|
397
|
+
observer = Observer.objects.get(original=name)
|
|
398
|
+
identifier = observer.identifier
|
|
399
|
+
created = False
|
|
400
|
+
else:
|
|
342
401
|
identifier = self.get_observer_identifier(data)
|
|
343
402
|
observer, created = Observer.objects.get_or_create(
|
|
344
403
|
identifier=identifier,
|
|
345
|
-
defaults={"name": name, "original":
|
|
404
|
+
defaults={"name": name, "original": name},
|
|
346
405
|
)
|
|
347
|
-
else:
|
|
348
|
-
if observer := Observer.objects.filter(original=name).first():
|
|
349
|
-
created = False
|
|
350
|
-
else:
|
|
351
|
-
identifier = self.get_observer_identifier(data)
|
|
352
|
-
observer, created = Observer.objects.get_or_create(
|
|
353
|
-
identifier=identifier,
|
|
354
|
-
defaults={"name": name, "original": name},
|
|
355
|
-
)
|
|
356
406
|
|
|
357
407
|
if created:
|
|
358
|
-
logger.info("
|
|
359
|
-
if Observer.objects.filter(original=name).count() > 1:
|
|
360
|
-
logger.error("Multiple observers exist with same name: %s", name)
|
|
408
|
+
logger.info("Observer added: %s (%s)", name, identifier)
|
|
361
409
|
|
|
362
410
|
return observer
|
|
363
411
|
|
|
@@ -392,7 +440,6 @@ class APILoader:
|
|
|
392
440
|
values: dict = {
|
|
393
441
|
"added": added,
|
|
394
442
|
"edited": edited,
|
|
395
|
-
"published": False,
|
|
396
443
|
"country": location.country,
|
|
397
444
|
"state": location.state,
|
|
398
445
|
"county": location.county,
|
|
@@ -413,40 +460,78 @@ class APILoader:
|
|
|
413
460
|
|
|
414
461
|
if data["obsTimeValid"]:
|
|
415
462
|
values["time"] = started.time()
|
|
463
|
+
else:
|
|
464
|
+
values["time"] = None
|
|
416
465
|
|
|
417
466
|
if "numObservers" in data:
|
|
418
467
|
values["observer_count"] = int(data["numObservers"])
|
|
468
|
+
else:
|
|
469
|
+
values["observer_count"] = None
|
|
419
470
|
|
|
420
471
|
if duration := data.get("durationHrs"):
|
|
421
472
|
values["duration"] = int(duration * 60.0)
|
|
473
|
+
else:
|
|
474
|
+
values["duration"] = None
|
|
422
475
|
|
|
423
476
|
if dist := data.get("effortDistanceKm"):
|
|
424
477
|
values["distance"] = round(Decimal(dist), 3)
|
|
478
|
+
else:
|
|
479
|
+
values["distance"] = None
|
|
425
480
|
|
|
426
481
|
if area := data.get("effortAreaHa"):
|
|
427
482
|
values["area"] = round(Decimal(area), 3)
|
|
483
|
+
else:
|
|
484
|
+
values["area"] = None
|
|
428
485
|
|
|
429
486
|
if "comments" in data:
|
|
430
487
|
values["comments"] = data["comments"]
|
|
488
|
+
else:
|
|
489
|
+
values["comments"] = ""
|
|
431
490
|
|
|
432
491
|
checklist, created = Checklist.objects.get_or_create(
|
|
433
492
|
identifier=identifier, defaults=values
|
|
434
493
|
)
|
|
435
|
-
|
|
436
|
-
values.pop("published")
|
|
494
|
+
|
|
437
495
|
if created:
|
|
438
|
-
logger.info("
|
|
439
|
-
elif self.update(checklist, values):
|
|
440
|
-
|
|
441
|
-
|
|
496
|
+
logger.info("Checklist %s: added", identifier)
|
|
497
|
+
elif changed := self.update(checklist, values):
|
|
498
|
+
ignored = ["added", "edited", "started", "url", "comments"]
|
|
499
|
+
filtered = {
|
|
500
|
+
key: value for key, value in changed.items() if key not in ignored
|
|
501
|
+
}
|
|
502
|
+
if "observer" in filtered:
|
|
503
|
+
old, new = filtered.pop("observer")
|
|
504
|
+
logger.info(
|
|
505
|
+
"Checklist %s: changed observer, %s (%s) -> %s (%s)",
|
|
506
|
+
identifier,
|
|
507
|
+
old.name,
|
|
508
|
+
old.identifier,
|
|
509
|
+
new.name,
|
|
510
|
+
new.identifier
|
|
511
|
+
)
|
|
512
|
+
for key, (old, new) in filtered.items():
|
|
513
|
+
logger.info(
|
|
514
|
+
"Checklist %s: changed %s, %s -> %s",
|
|
515
|
+
identifier,
|
|
516
|
+
key.replace("_", " "),
|
|
517
|
+
old,
|
|
518
|
+
new,
|
|
519
|
+
)
|
|
520
|
+
else:
|
|
521
|
+
logger.debug("Checklist %s: unchanged", identifier)
|
|
442
522
|
|
|
443
523
|
for observation_data in observations:
|
|
444
524
|
self.get_observation(observation_data, checklist)
|
|
445
525
|
|
|
446
|
-
for
|
|
447
|
-
if
|
|
448
|
-
|
|
449
|
-
|
|
526
|
+
for obs in checklist.observations.all():
|
|
527
|
+
if obs.edited != checklist.edited:
|
|
528
|
+
obs.delete()
|
|
529
|
+
logger.info(
|
|
530
|
+
"Checklist %s: deleted, %s (%s)",
|
|
531
|
+
identifier,
|
|
532
|
+
obs.species,
|
|
533
|
+
obs.count,
|
|
534
|
+
)
|
|
450
535
|
|
|
451
536
|
return checklist
|
|
452
537
|
|
|
@@ -472,15 +557,19 @@ class APILoader:
|
|
|
472
557
|
)
|
|
473
558
|
|
|
474
559
|
if len(results) == API_MAX_RESULTS:
|
|
475
|
-
logger.info("API limit reached - fetching
|
|
560
|
+
logger.info("Region %s: API limit reached - fetching subregions", region)
|
|
476
561
|
if sub_regions := self.fetch_subregions(region):
|
|
477
562
|
for sub_region in sub_regions:
|
|
478
|
-
|
|
563
|
+
sub_region_visits = self.fetch_visits(sub_region, date)
|
|
564
|
+
visits.extend(sub_region_visits)
|
|
565
|
+
logger.info(
|
|
566
|
+
"Region %s: %d visits", sub_region, len(sub_region_visits)
|
|
567
|
+
)
|
|
479
568
|
else:
|
|
480
569
|
# No more sub-regions, issue a warning and return the results
|
|
481
570
|
visits.extend(results)
|
|
482
571
|
logger.warning(
|
|
483
|
-
"API limit reached -
|
|
572
|
+
"Region %s: API limit reached - no subregions available", region
|
|
484
573
|
)
|
|
485
574
|
else:
|
|
486
575
|
visits.extend(results)
|
|
@@ -502,11 +591,11 @@ class APILoader:
|
|
|
502
591
|
|
|
503
592
|
"""
|
|
504
593
|
|
|
505
|
-
logger.info("Adding checklists: %s, %s", region, date)
|
|
594
|
+
logger.info("Adding checklists: %s, %s", region, date.strftime("%Y-%m-%d"))
|
|
506
595
|
|
|
507
596
|
visits: list[dict] = self.fetch_visits(region, date)
|
|
508
597
|
|
|
509
|
-
logger.info("
|
|
598
|
+
logger.info("Region %s: %s visits", region, len(visits))
|
|
510
599
|
|
|
511
600
|
for visit in visits:
|
|
512
601
|
data = visit["loc"]
|
|
@@ -514,26 +603,8 @@ class APILoader:
|
|
|
514
603
|
|
|
515
604
|
for visit in visits:
|
|
516
605
|
identifier = visit["subId"]
|
|
517
|
-
if (
|
|
518
|
-
not update
|
|
519
|
-
and Checklist.objects.filter(identifier=identifier).exists()
|
|
520
|
-
):
|
|
606
|
+
if not update and Checklist.objects.filter(identifier=identifier).exists():
|
|
521
607
|
continue
|
|
522
608
|
self.add_checklist(identifier)
|
|
523
609
|
|
|
524
|
-
|
|
525
|
-
self.publish()
|
|
526
|
-
|
|
527
|
-
logger.info("Adding checklists completed")
|
|
528
|
-
|
|
529
|
-
@staticmethod
|
|
530
|
-
def run_filters():
|
|
531
|
-
for filter in Filter.objects.filter(enabled=True):
|
|
532
|
-
logger.info("Applying filter: %s", filter.name)
|
|
533
|
-
count = filter.apply()
|
|
534
|
-
logger.info("Observations updated: %d", count)
|
|
535
|
-
|
|
536
|
-
@staticmethod
|
|
537
|
-
def publish():
|
|
538
|
-
Checklist.objects.filter(published=False).update(published=True)
|
|
539
|
-
Observation.objects.filter(published=False).update(published=True)
|
|
610
|
+
logger.info("Added checklists: %s, %s", region, date.strftime("%Y-%m-%d"))
|
ebird_api_data-0.5.1/src/ebird/api/data/migrations/0003_remove_checklist_published_and_more.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Generated by Django 5.2.7 on 2025-11-05 08:59
|
|
2
|
+
|
|
3
|
+
from django.db import migrations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('data', '0002_expand_filter'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.RemoveField(
|
|
14
|
+
model_name='checklist',
|
|
15
|
+
name='published',
|
|
16
|
+
),
|
|
17
|
+
migrations.RemoveField(
|
|
18
|
+
model_name='observation',
|
|
19
|
+
name='published',
|
|
20
|
+
),
|
|
21
|
+
migrations.DeleteModel(
|
|
22
|
+
name='Filter',
|
|
23
|
+
),
|
|
24
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Generated by Django 5.2.7 on 2025-11-05 09:15
|
|
2
|
+
|
|
3
|
+
from django.db import migrations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('data', '0003_remove_checklist_published_and_more'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.RemoveField(
|
|
14
|
+
model_name='observer',
|
|
15
|
+
name='multiple',
|
|
16
|
+
),
|
|
17
|
+
]
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from .checklist import Checklist
|
|
2
2
|
from .country import Country
|
|
3
3
|
from .county import County
|
|
4
|
-
from .filter import Filter
|
|
5
4
|
from .location import Location
|
|
6
5
|
from .observation import Observation
|
|
7
6
|
from .observer import Observer
|
|
@@ -12,7 +11,6 @@ __all__ = (
|
|
|
12
11
|
"Checklist",
|
|
13
12
|
"Country",
|
|
14
13
|
"County",
|
|
15
|
-
"Filter",
|
|
16
14
|
"Location",
|
|
17
15
|
"Observation",
|
|
18
16
|
"Observer",
|
|
@@ -33,11 +33,6 @@ class Checklist(models.Model):
|
|
|
33
33
|
verbose_name=_("edited"),
|
|
34
34
|
)
|
|
35
35
|
|
|
36
|
-
published = models.BooleanField(
|
|
37
|
-
help_text=_("Is the checklist published?"),
|
|
38
|
-
verbose_name=_("published"),
|
|
39
|
-
)
|
|
40
|
-
|
|
41
36
|
identifier = models.CharField(
|
|
42
37
|
max_length=15,
|
|
43
38
|
primary_key=True,
|
|
@@ -23,11 +23,6 @@ class Observation(models.Model):
|
|
|
23
23
|
verbose_name=_("edited"),
|
|
24
24
|
)
|
|
25
25
|
|
|
26
|
-
published = models.BooleanField(
|
|
27
|
-
help_text=_("Is the observation published?"),
|
|
28
|
-
verbose_name=_("published"),
|
|
29
|
-
)
|
|
30
|
-
|
|
31
26
|
identifier = models.CharField(
|
|
32
27
|
max_length=15,
|
|
33
28
|
primary_key=True,
|
|
@@ -24,12 +24,6 @@ class Observer(models.Model):
|
|
|
24
24
|
help_text=_("The original name of the observer from eBird."),
|
|
25
25
|
)
|
|
26
26
|
|
|
27
|
-
multiple = models.BooleanField(
|
|
28
|
-
default=False,
|
|
29
|
-
verbose_name=_("Multiple accounts"),
|
|
30
|
-
help_text=_("Is the name used by more than one eBird account."),
|
|
31
|
-
)
|
|
32
|
-
|
|
33
27
|
enabled = models.BooleanField(
|
|
34
28
|
default=True,
|
|
35
29
|
verbose_name=_("Enabled"),
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.4.0"
|