ebird-api-data 0.1.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.
File without changes
@@ -0,0 +1,21 @@
1
+ from .checklist import Checklist
2
+ from .country import Country
3
+ from .county import County
4
+ from .filter import Filter
5
+ from .location import Location
6
+ from .observation import Observation
7
+ from .observer import Observer
8
+ from .species import Species
9
+ from .state import State
10
+
11
+ __all__ = (
12
+ "Checklist",
13
+ "Country",
14
+ "County",
15
+ "Filter",
16
+ "Location",
17
+ "Observation",
18
+ "Observer",
19
+ "Species",
20
+ "State",
21
+ )
@@ -0,0 +1,178 @@
1
+ from django.db import models
2
+ from django.utils.translation import gettext_lazy as _
3
+
4
+
5
+ class Checklist(models.Model):
6
+ class Meta:
7
+ verbose_name = _("checklist")
8
+ verbose_name_plural = _("checklists")
9
+
10
+ created = models.DateTimeField(
11
+ blank=True,
12
+ null=True,
13
+ help_text=_("The date and time the checklist was added to eBird."),
14
+ verbose_name=_("created"),
15
+ )
16
+
17
+ edited = models.DateTimeField(
18
+ blank=True,
19
+ null=True,
20
+ help_text=_("The date and time the eBird checklist was last edited."),
21
+ verbose_name=_("edited"),
22
+ )
23
+
24
+ published = models.BooleanField(
25
+ help_text=_("Is the checklist published?"),
26
+ verbose_name=_("published"),
27
+ )
28
+
29
+ identifier = models.TextField(
30
+ unique=True,
31
+ verbose_name=_("identifier"),
32
+ help_text=_("The unique identifier for the checklist."),
33
+ )
34
+
35
+ country = models.ForeignKey(
36
+ "data.Country",
37
+ related_name="checklists",
38
+ on_delete=models.PROTECT,
39
+ verbose_name=_("country"),
40
+ help_text=_("The country where the checklist was made."),
41
+ )
42
+
43
+ state = models.ForeignKey(
44
+ "data.State",
45
+ related_name="checklists",
46
+ on_delete=models.PROTECT,
47
+ verbose_name=_("state"),
48
+ help_text=_("The state where the checklist was made."),
49
+ )
50
+
51
+ county = models.ForeignKey(
52
+ "data.County",
53
+ blank=True,
54
+ null=True,
55
+ related_name="checklists",
56
+ on_delete=models.PROTECT,
57
+ verbose_name=_("county"),
58
+ help_text=_("The county where the checklist was made."),
59
+ )
60
+
61
+ location = models.ForeignKey(
62
+ "data.Location",
63
+ related_name="checklists",
64
+ on_delete=models.PROTECT,
65
+ verbose_name=_("location"),
66
+ help_text=_("The location where checklist was made."),
67
+ )
68
+
69
+ observer = models.ForeignKey(
70
+ "data.Observer",
71
+ related_name="checklists",
72
+ on_delete=models.PROTECT,
73
+ verbose_name=_("observer"),
74
+ help_text=_("The person who submitted the checklist."),
75
+ )
76
+
77
+ observer_count = models.IntegerField(
78
+ blank=True,
79
+ null=True,
80
+ verbose_name=_("observer count"),
81
+ help_text=_("The total number of observers."),
82
+ )
83
+
84
+ species_count = models.IntegerField(
85
+ blank=True,
86
+ null=True,
87
+ verbose_name=_("species count"),
88
+ help_text=_("The number of species reported."),
89
+ )
90
+
91
+ date = models.DateField(
92
+ db_index=True,
93
+ verbose_name=_("date"),
94
+ help_text=_("The date the checklist was started."),
95
+ )
96
+
97
+ time = models.TimeField(
98
+ blank=True,
99
+ null=True,
100
+ verbose_name=_("time"),
101
+ help_text=_("The time the checklist was started."),
102
+ )
103
+
104
+ started = models.DateTimeField(
105
+ blank=True,
106
+ db_index=True,
107
+ null=True,
108
+ verbose_name=_("date & time"),
109
+ help_text=_("The date and time the checklist was started."),
110
+ )
111
+
112
+ protocol_code = models.TextField(
113
+ blank=True,
114
+ verbose_name=_("protocol code"),
115
+ help_text=_("The code used to identify the protocol."),
116
+ )
117
+
118
+ project_code = models.TextField(
119
+ blank=True,
120
+ verbose_name=_("project code"),
121
+ help_text=_("The code used to identify the project (portal)."),
122
+ )
123
+
124
+ duration = models.IntegerField(
125
+ blank=True,
126
+ null=True,
127
+ verbose_name=_("duration"),
128
+ help_text=_("The number of minutes spent counting."),
129
+ )
130
+
131
+ distance = models.DecimalField(
132
+ blank=True,
133
+ null=True,
134
+ decimal_places=3,
135
+ max_digits=6,
136
+ verbose_name=_("distance"),
137
+ help_text=_("The distance, in metres, covered while travelling."),
138
+ )
139
+
140
+ area = models.DecimalField(
141
+ blank=True,
142
+ null=True,
143
+ decimal_places=3,
144
+ max_digits=6,
145
+ verbose_name=_("area"),
146
+ help_text=_("The area covered, in hectares."),
147
+ )
148
+
149
+ complete = models.BooleanField(
150
+ default=False,
151
+ verbose_name=_("complete"),
152
+ help_text=_("All species seen are reported."),
153
+ )
154
+
155
+ comments = models.TextField(
156
+ blank=True,
157
+ verbose_name=_("comments"),
158
+ help_text=_("Any comments about the checklist."),
159
+ )
160
+
161
+ url = models.URLField(
162
+ blank=True,
163
+ verbose_name=_("url"),
164
+ help_text=_("URL where the original checklist can be viewed."),
165
+ )
166
+
167
+ data = models.JSONField(
168
+ verbose_name=_("Data"),
169
+ help_text=_("Data describing a Checklist."),
170
+ default=dict,
171
+ blank=True,
172
+ )
173
+
174
+ def __repr__(self) -> str:
175
+ return str(self.identifier)
176
+
177
+ def __str__(self) -> str:
178
+ return str(self.identifier)
@@ -0,0 +1,29 @@
1
+ from django.db import models
2
+ from django.utils.translation import gettext_lazy as _
3
+
4
+
5
+ class Country(models.Model):
6
+ class Meta:
7
+ verbose_name = _("country")
8
+ verbose_name_plural = _("countries")
9
+
10
+ code = models.CharField(
11
+ max_length=2,
12
+ db_index=True,
13
+ verbose_name=_("code"),
14
+ help_text=_("The code used to identify the country."),
15
+ )
16
+
17
+ name = models.TextField(
18
+ verbose_name=_("name"), help_text=_("The name of the country.")
19
+ )
20
+
21
+ place = models.TextField(
22
+ verbose_name=_("place"), help_text=_("The hierarchical name of the country.")
23
+ )
24
+
25
+ def __repr__(self) -> str:
26
+ return str(self.code)
27
+
28
+ def __str__(self) -> str:
29
+ return str(self.name)
@@ -0,0 +1,29 @@
1
+ from django.db import models
2
+ from django.utils.translation import gettext_lazy as _
3
+
4
+
5
+ class County(models.Model):
6
+ class Meta:
7
+ verbose_name = _("county")
8
+ verbose_name_plural = _("counties")
9
+
10
+ code = models.CharField(
11
+ max_length=10,
12
+ db_index=True,
13
+ verbose_name=_("code"),
14
+ help_text=_("The code used to identify the county."),
15
+ )
16
+
17
+ name = models.TextField(
18
+ verbose_name=_("name"), help_text=_("The name of the county.")
19
+ )
20
+
21
+ place = models.TextField(
22
+ verbose_name=_("place"), help_text=_("The hierarchical name of the county.")
23
+ )
24
+
25
+ def __repr__(self) -> str:
26
+ return str(self.code)
27
+
28
+ def __str__(self) -> str:
29
+ return str(self.name)
@@ -0,0 +1,71 @@
1
+ import logging
2
+
3
+ from django.db import models
4
+ from django.utils.translation import gettext_lazy as _
5
+
6
+ from .observation import Observation
7
+
8
+ log = logging.getLogger(__name__)
9
+
10
+
11
+ class Filter(models.Model):
12
+ class Meta:
13
+ verbose_name = _("filter")
14
+ verbose_name_plural = _("filters")
15
+
16
+ enabled = models.BooleanField(
17
+ help_text=_("Is the filter active?"),
18
+ verbose_name=_("enabled"),
19
+ )
20
+
21
+ name = models.TextField(
22
+ verbose_name=_("name"), help_text=_("The name of the filter.")
23
+ )
24
+
25
+ species = models.ForeignKey(
26
+ "data.Species",
27
+ related_name="filtered_on",
28
+ on_delete=models.CASCADE,
29
+ verbose_name=_("species"),
30
+ help_text=_("The species used to find matching Observations."),
31
+ )
32
+
33
+ location = models.ForeignKey(
34
+ "data.Location",
35
+ blank=True,
36
+ null=True,
37
+ related_name="filtered_on",
38
+ on_delete=models.CASCADE,
39
+ verbose_name=_("location"),
40
+ help_text=_("The location used to find matching Observations."),
41
+ )
42
+
43
+ update_species = models.ForeignKey(
44
+ "data.Species",
45
+ related_name="filtered_by",
46
+ on_delete=models.CASCADE,
47
+ verbose_name=_("update species"),
48
+ help_text=_("Matching Observations are updated to this species."),
49
+ )
50
+
51
+ def __repr__(self) -> str:
52
+ return str(self.name)
53
+
54
+ def __str__(self) -> str:
55
+ return str(self.name)
56
+
57
+ def apply(self):
58
+ count = 0
59
+ filters = models.Q()
60
+
61
+ if self.species:
62
+ filters &= models.Q(species=self.species)
63
+ if self.location:
64
+ filters &= models.Q(location=self.location)
65
+
66
+ for observation in Observation.objects.filter(filters):
67
+ observation.species = self.update_species
68
+ observation.save()
69
+ count += 1
70
+
71
+ return count
@@ -0,0 +1,153 @@
1
+ from django.db import models
2
+ from django.utils.translation import gettext_lazy as _
3
+
4
+
5
+ class Location(models.Model):
6
+ """
7
+ Location defines the location where the observations in a Checklist
8
+ were made.
9
+
10
+ Design Notes
11
+ ------------
12
+ Rather than use country, subnational1 and subnational2 from the eBird API,
13
+ the names country, state and county from the eBird Basic Dataset were used.
14
+ Originally the models were designed so data from either source could be
15
+ loaded, but since the schemas did not overlap completely that meant quite
16
+ a few fields were optional. This dual-purpose role was discarded and the
17
+ models were changed to specifically support the eBird API. The names, state
18
+ and county were kept because they were shorter, and it was easier to remember
19
+ the order.
20
+
21
+ The hierarchy: country, state, country describes the administrative, but not
22
+ the geographical/ecological organisation. Being able to identify groups of
23
+ related locations in a national park or Important Bird Areas, or groups of
24
+ counties or states is useful to find out what has been seen in a given area.
25
+ Additional models for Area, Island and Region were considered but ultimately
26
+ rejected for three main reasons:
27
+
28
+ 1. For hotspots it is easy to map locations to an Area or Island, however
29
+ about 40% of all locations (for Portugal) are personal or private, even
30
+ when the hotspot is well known, or should have been well known. Unless
31
+ these were ignored, that would mean an on-going effort to add these
32
+ locations to the relevant groups. For even a small country or area the
33
+ effort is enormous. Autocorrecting location should be possible but that
34
+ would mean defining boundaries for each hotspot. There was talk of eBird
35
+ doing that but again the effort would be enormous.
36
+
37
+ 2. If the hierarchy is extended to add new levels, then the codes used by
38
+ eBird, US, US-NY, US-NY-109 would need to be changed to incorporate
39
+ the new levels, since it makes finding related locations easy. The
40
+ alternative is to create the hierarchy using foreign keys. Again that
41
+ create a considerable administrative burden.
42
+
43
+ 3. Grouping locations, countries, or states is mainly for usability,
44
+ however for small numbers of each, adding form controls that support
45
+ multi-select can be used as a workaround / viable alternative, and
46
+ even provide more flexibility.
47
+
48
+ On the second point, it might be worth adopting an alternative hierarchy
49
+ such as the NUTS3 system used in the European Union, which has an extra
50
+ level. It also uses hierarchical codes, e.g. PT1, PT15, PT15D. Counties
51
+ are the smallest areas, and are shared by both systems, so it is easy to
52
+ map eBird codes to NUTS codes.
53
+
54
+ Optimisations
55
+ -------------
56
+ A Checklist has a Location, but the hierarchy was flattened so the County,
57
+ State and Country are duplicated on the Checklist model to reduce the
58
+ number of joins, and so speed up queries. Similarly, Location, County, State,
59
+ and Country are duplicated on Observation for the same reason.
60
+
61
+ """
62
+
63
+ class Meta:
64
+ verbose_name = _("location")
65
+ verbose_name_plural = _("locations")
66
+
67
+ identifier = models.TextField(
68
+ unique=True,
69
+ verbose_name=_("identifier"),
70
+ help_text=_("The unique identifier for the location."),
71
+ )
72
+
73
+ original = models.TextField(
74
+ verbose_name=_("original name"),
75
+ help_text=_("The original name of the location from eBird."),
76
+ )
77
+
78
+ name = models.TextField(
79
+ verbose_name=_("name"),
80
+ help_text=_("The display name of the location."),
81
+ )
82
+
83
+ country = models.ForeignKey(
84
+ "data.Country",
85
+ related_name="locations",
86
+ on_delete=models.PROTECT,
87
+ verbose_name=_("country"),
88
+ help_text=_("The country for the location."),
89
+ )
90
+
91
+ state = models.ForeignKey(
92
+ "data.State",
93
+ related_name="locations",
94
+ on_delete=models.PROTECT,
95
+ verbose_name=_("state"),
96
+ help_text=_("The state for the location."),
97
+ )
98
+
99
+ county = models.ForeignKey(
100
+ "data.County",
101
+ blank=True,
102
+ null=True,
103
+ related_name="locations",
104
+ on_delete=models.PROTECT,
105
+ verbose_name=_("county"),
106
+ help_text=_("The county for the location."),
107
+ )
108
+
109
+ latitude = models.DecimalField(
110
+ blank=True,
111
+ null=True,
112
+ decimal_places=7,
113
+ max_digits=9,
114
+ verbose_name=_("latitude"),
115
+ help_text=_("The decimal latitude of the location, relative to the equator."),
116
+ )
117
+
118
+ longitude = models.DecimalField(
119
+ blank=True,
120
+ null=True,
121
+ decimal_places=7,
122
+ max_digits=10,
123
+ verbose_name=_("longitude"),
124
+ help_text=_(
125
+ "The decimal longitude of the location, relative to the prime meridian."
126
+ ),
127
+ )
128
+
129
+ url = models.URLField(
130
+ blank=True,
131
+ verbose_name=_("url"),
132
+ help_text=_("URL of the location page on eBird."),
133
+ )
134
+
135
+ hotspot = models.BooleanField(
136
+ blank=True,
137
+ null=True,
138
+ verbose_name=_("is hotspot"),
139
+ help_text=_("Is the location a hotspot."),
140
+ )
141
+
142
+ data = models.JSONField(
143
+ verbose_name=_("Data"),
144
+ help_text=_("Data describing a Location."),
145
+ default=dict,
146
+ blank=True,
147
+ )
148
+
149
+ def __repr__(self) -> str:
150
+ return str(self.identifier)
151
+
152
+ def __str__(self) -> str:
153
+ return self.name
@@ -0,0 +1,189 @@
1
+ import json
2
+ import logging
3
+
4
+ from json import JSONDecodeError
5
+
6
+ from django.core.validators import MinValueValidator
7
+ from django.db import models
8
+ from django.utils.translation import get_language
9
+ from django.utils.translation import gettext_lazy as _
10
+
11
+ log = logging.getLogger(__name__)
12
+
13
+
14
+ class Observation(models.Model):
15
+ class Meta:
16
+ verbose_name = _("observation")
17
+ verbose_name_plural = _("observations")
18
+
19
+ edited = models.DateTimeField(
20
+ blank=True,
21
+ null=True,
22
+ help_text=_("The date and time the eBird checklist was last edited."),
23
+ verbose_name=_("edited"),
24
+ )
25
+
26
+ published = models.BooleanField(
27
+ help_text=_("Is the observation published?"),
28
+ verbose_name=_("published"),
29
+ )
30
+
31
+ identifier = models.TextField(
32
+ unique=True,
33
+ verbose_name=_("identifier"),
34
+ help_text=_("A global unique identifier for the observation."),
35
+ )
36
+
37
+ checklist = models.ForeignKey(
38
+ "data.Checklist",
39
+ related_name="observations",
40
+ on_delete=models.CASCADE,
41
+ verbose_name=_("checklist"),
42
+ help_text=_("The checklist this observation belongs to."),
43
+ )
44
+
45
+ species = models.ForeignKey(
46
+ "data.Species",
47
+ related_name="observations",
48
+ on_delete=models.PROTECT,
49
+ verbose_name=_("species"),
50
+ help_text=_("The identified species."),
51
+ )
52
+
53
+ observer = models.ForeignKey(
54
+ "data.Observer",
55
+ related_name="observations",
56
+ on_delete=models.PROTECT,
57
+ verbose_name=_("observer"),
58
+ help_text=_("The person who made the observation."),
59
+ )
60
+
61
+ country = models.ForeignKey(
62
+ "data.Country",
63
+ related_name="observations",
64
+ on_delete=models.PROTECT,
65
+ verbose_name=_("country"),
66
+ help_text=_("The country where the observation was made."),
67
+ )
68
+
69
+ state = models.ForeignKey(
70
+ "data.State",
71
+ related_name="observations",
72
+ on_delete=models.PROTECT,
73
+ verbose_name=_("state"),
74
+ help_text=_("The state where the observation was made."),
75
+ )
76
+
77
+ county = models.ForeignKey(
78
+ "data.County",
79
+ blank=True,
80
+ null=True,
81
+ related_name="observations",
82
+ on_delete=models.PROTECT,
83
+ verbose_name=_("county"),
84
+ help_text=_("The county where the observation was made."),
85
+ )
86
+
87
+ location = models.ForeignKey(
88
+ "data.Location",
89
+ related_name="observations",
90
+ on_delete=models.PROTECT,
91
+ verbose_name=_("location"),
92
+ help_text=_("The location where the observation was made."),
93
+ )
94
+
95
+ date = models.DateField(
96
+ db_index=True,
97
+ verbose_name=_("date"),
98
+ help_text=_("The date the observation was made."),
99
+ )
100
+
101
+ time = models.TimeField(
102
+ blank=True,
103
+ null=True,
104
+ verbose_name=_("time"),
105
+ help_text=_("The time the observation was made."),
106
+ )
107
+
108
+ started = models.DateTimeField(
109
+ blank=True,
110
+ db_index=True,
111
+ null=True,
112
+ verbose_name=_("date & time"),
113
+ help_text=_("The date and time the observation was made."),
114
+ )
115
+
116
+ count = models.IntegerField(
117
+ validators=[MinValueValidator(0)],
118
+ verbose_name=_("count"),
119
+ help_text=_("The number of birds seen."),
120
+ )
121
+
122
+ audio = models.BooleanField(
123
+ default=False,
124
+ verbose_name=_("has audio"),
125
+ help_text=_("Have audio recordings been uploaded to the Macaulay library."),
126
+ )
127
+
128
+ photo = models.BooleanField(
129
+ default=False,
130
+ verbose_name=_("has photos"),
131
+ help_text=_("Have photos been uploaded to the Macaulay library."),
132
+ )
133
+
134
+ video = models.BooleanField(
135
+ default=False,
136
+ verbose_name=_("has video"),
137
+ help_text=_("Have video recordings been uploaded to the Macaulay library."),
138
+ )
139
+
140
+ approved = models.BooleanField(
141
+ default=True,
142
+ verbose_name=_("Approved"),
143
+ help_text=_("Has the observation been accepted."),
144
+ )
145
+
146
+ reason = models.TextField(
147
+ blank=True,
148
+ verbose_name=_("Reason"),
149
+ help_text=_(
150
+ "The reason given for the observation to be marked as not approved."
151
+ ),
152
+ )
153
+
154
+ comments = models.TextField(
155
+ blank=True,
156
+ verbose_name=_("comments"),
157
+ help_text=_("Any comments about the observation."),
158
+ )
159
+
160
+ urn = models.TextField(
161
+ blank=True,
162
+ verbose_name=_("URN"),
163
+ help_text=_("The globally unique identifier for the observation."),
164
+ )
165
+
166
+ data = models.JSONField(
167
+ verbose_name=_("Data"),
168
+ help_text=_("Data describing an Observation."),
169
+ default=dict,
170
+ blank=True,
171
+ )
172
+
173
+ def __repr__(self) -> str:
174
+ return str(self.identifier)
175
+
176
+ def __str__(self) -> str:
177
+ return str(self.identifier)
178
+
179
+ def get_reason(self) -> str:
180
+ try:
181
+ data = json.loads(self.reason)
182
+ reason = data.get(get_language(), "")
183
+ except JSONDecodeError:
184
+ log.error("Incorrect JSON for Observation reason: %s", self.id)
185
+ reason = ""
186
+ return reason
187
+
188
+ def has_media(self):
189
+ return self.audio or self.photo or self.video