inat-get 0.8.0.11
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.
- checksums.yaml +7 -0
- data/LICENSE +674 -0
- data/README.md +16 -0
- data/Rakefile +4 -0
- data/bin/inat-get +59 -0
- data/docs/logo.png +0 -0
- data/inat-get.gemspec +33 -0
- data/lib/extra/enum.rb +184 -0
- data/lib/extra/period.rb +252 -0
- data/lib/extra/uuid.rb +90 -0
- data/lib/inat/app/application.rb +50 -0
- data/lib/inat/app/config/messagelevel.rb +22 -0
- data/lib/inat/app/config/shiftage.rb +24 -0
- data/lib/inat/app/config/updatemode.rb +20 -0
- data/lib/inat/app/config.rb +296 -0
- data/lib/inat/app/globals.rb +80 -0
- data/lib/inat/app/info.rb +21 -0
- data/lib/inat/app/logging.rb +35 -0
- data/lib/inat/app/preamble.rb +27 -0
- data/lib/inat/app/status.rb +74 -0
- data/lib/inat/app/task/context.rb +47 -0
- data/lib/inat/app/task/dsl.rb +24 -0
- data/lib/inat/app/task.rb +75 -0
- data/lib/inat/data/api.rb +218 -0
- data/lib/inat/data/cache.rb +9 -0
- data/lib/inat/data/db.rb +87 -0
- data/lib/inat/data/ddl.rb +18 -0
- data/lib/inat/data/entity/comment.rb +29 -0
- data/lib/inat/data/entity/flag.rb +22 -0
- data/lib/inat/data/entity/identification.rb +45 -0
- data/lib/inat/data/entity/observation.rb +172 -0
- data/lib/inat/data/entity/observationphoto.rb +25 -0
- data/lib/inat/data/entity/observationsound.rb +26 -0
- data/lib/inat/data/entity/photo.rb +31 -0
- data/lib/inat/data/entity/place.rb +57 -0
- data/lib/inat/data/entity/project.rb +94 -0
- data/lib/inat/data/entity/projectadmin.rb +21 -0
- data/lib/inat/data/entity/projectobservationrule.rb +50 -0
- data/lib/inat/data/entity/request.rb +58 -0
- data/lib/inat/data/entity/sound.rb +27 -0
- data/lib/inat/data/entity/taxon.rb +94 -0
- data/lib/inat/data/entity/user.rb +67 -0
- data/lib/inat/data/entity/vote.rb +22 -0
- data/lib/inat/data/entity.rb +291 -0
- data/lib/inat/data/enums/conservationstatus.rb +30 -0
- data/lib/inat/data/enums/geoprivacy.rb +14 -0
- data/lib/inat/data/enums/iconictaxa.rb +23 -0
- data/lib/inat/data/enums/identificationcategory.rb +13 -0
- data/lib/inat/data/enums/licensecode.rb +16 -0
- data/lib/inat/data/enums/projectadminrole.rb +11 -0
- data/lib/inat/data/enums/projecttype.rb +37 -0
- data/lib/inat/data/enums/qualitygrade.rb +12 -0
- data/lib/inat/data/enums/rank.rb +60 -0
- data/lib/inat/data/model.rb +551 -0
- data/lib/inat/data/query.rb +1145 -0
- data/lib/inat/data/sets/dataset.rb +104 -0
- data/lib/inat/data/sets/list.rb +190 -0
- data/lib/inat/data/sets/listers.rb +15 -0
- data/lib/inat/data/sets/wrappers.rb +137 -0
- data/lib/inat/data/types/extras.rb +88 -0
- data/lib/inat/data/types/location.rb +89 -0
- data/lib/inat/data/types/std.rb +293 -0
- data/lib/inat/report/table.rb +135 -0
- data/lib/inat/utils/deep.rb +30 -0
- metadata +137 -0
@@ -0,0 +1,291 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../app/globals'
|
4
|
+
|
5
|
+
require_relative 'model'
|
6
|
+
require_relative 'ddl'
|
7
|
+
require_relative 'db'
|
8
|
+
require_relative 'api'
|
9
|
+
|
10
|
+
class Entity < Model
|
11
|
+
|
12
|
+
include LogDSL
|
13
|
+
|
14
|
+
class << self
|
15
|
+
|
16
|
+
include LogDSL
|
17
|
+
|
18
|
+
def inherited sub
|
19
|
+
sub.send :init
|
20
|
+
DDL << sub
|
21
|
+
end
|
22
|
+
|
23
|
+
private :new
|
24
|
+
|
25
|
+
private def init
|
26
|
+
@mutex = Mutex::new
|
27
|
+
end
|
28
|
+
|
29
|
+
private def update
|
30
|
+
raise ArgumentError, "Block is required!", caller unless block_given?
|
31
|
+
result = nil
|
32
|
+
exception = nil
|
33
|
+
@mutex.synchronize do
|
34
|
+
begin
|
35
|
+
result = yield
|
36
|
+
rescue Exception => e
|
37
|
+
exception = e
|
38
|
+
end
|
39
|
+
end
|
40
|
+
raise exception.class, exception.message, caller, cause: exception if exception
|
41
|
+
result
|
42
|
+
end
|
43
|
+
|
44
|
+
private def get id
|
45
|
+
return nil if id == nil
|
46
|
+
update do
|
47
|
+
@entities ||= {}
|
48
|
+
@entities[id] ||= new id
|
49
|
+
@entities[id]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def fetch *ids
|
54
|
+
return [] if ids.empty?
|
55
|
+
# sids = if ids.size > 7
|
56
|
+
# ids.take(7).map(&:to_s) + [ 'and more' ]
|
57
|
+
# else
|
58
|
+
# ids.map(&:to_s)
|
59
|
+
# end
|
60
|
+
# Status::status '[fetch]', "#{ self } : " + sids.join(', ') + ' ...'
|
61
|
+
result = ids.map { |id| get id }.filter { |x| x != nil }
|
62
|
+
nc_ids = result.select { |e| !e.complete? && !e.process? }.map(&:id)
|
63
|
+
read(*nc_ids)
|
64
|
+
nc_ids = result.select { |e| !e.complete? && !e.process? }.map(&:id)
|
65
|
+
load(*nc_ids)
|
66
|
+
nc_ids = result.select { |e| !e.complete? && !e.process? }.map(&:id)
|
67
|
+
warning "Some #{ self } IDs were not fetched: #{ nc_ids.join(', ') }!" unless nc_ids.empty?
|
68
|
+
# result = [ nil ] if result == []
|
69
|
+
# Status::status '[fetch]', "#{ self } : " + sids.join(', ') + ' DONE'
|
70
|
+
result
|
71
|
+
end
|
72
|
+
|
73
|
+
def by_id id
|
74
|
+
fetch(id).first
|
75
|
+
end
|
76
|
+
|
77
|
+
# TODO: подумать о переименовании
|
78
|
+
def from_db_rows data
|
79
|
+
result = []
|
80
|
+
data.each do |row|
|
81
|
+
id = row['id'] || row[:id]
|
82
|
+
raise TypeError, "Invalid data row: no 'id' field!" unless id
|
83
|
+
# check.delete id
|
84
|
+
entity = get id
|
85
|
+
entity.update(from_db: true) do
|
86
|
+
fields.each do |_, field|
|
87
|
+
case field.kind
|
88
|
+
when :value
|
89
|
+
name, value = field.from_db row
|
90
|
+
if name != nil && value != nil
|
91
|
+
entity.send "#{ name }=", value
|
92
|
+
end
|
93
|
+
when :links
|
94
|
+
ids = DB.execute("SELECT #{ field.link_field } FROM #{ field.table_name } WHERE #{ field.back_field } = ?", entity.id).map { |x| x[field.link_field.to_s] }
|
95
|
+
entity.send "#{ field.id_field }=", ids
|
96
|
+
when :backs
|
97
|
+
# TODO: подумать над тем, чтобы сразу загрузить: вынести парсинг отдельно...
|
98
|
+
ids = DB.execute("SELECT id FROM #{ field.type.table } WHERE #{ field.back_field } = ?", entity.id).map { |x| x['id'] }
|
99
|
+
entity.send "#{ field.id_field }=", ids
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
result << entity
|
104
|
+
end
|
105
|
+
result
|
106
|
+
end
|
107
|
+
|
108
|
+
def read *ids
|
109
|
+
return [] if ids.empty?
|
110
|
+
# check = ids.dup
|
111
|
+
# fields = self.fields
|
112
|
+
data = DB.execute "SELECT * FROM #{ self.table } WHERE id IN (#{ (['?'] * ids.size).join(',') })", *ids
|
113
|
+
from_db_rows data
|
114
|
+
end
|
115
|
+
|
116
|
+
def load *ids
|
117
|
+
return [] if ids.empty? || @api_path.nil?
|
118
|
+
data = API.get @api_path, @api_part, @api_limit, *ids
|
119
|
+
data.map { |obj| parse obj }
|
120
|
+
end
|
121
|
+
|
122
|
+
def load_file filename
|
123
|
+
data = API.load_file filename
|
124
|
+
data.map { |obj| parse obj }
|
125
|
+
end
|
126
|
+
|
127
|
+
def parse src
|
128
|
+
return nil if src == nil
|
129
|
+
raise TypeError, "Source must be a Hash! (#{ src.inspect })" unless Hash === src
|
130
|
+
# if !(Hash === src)
|
131
|
+
# warning "INVALID SOURCE for #{ self }: #{ src.inspect }"
|
132
|
+
# return nil
|
133
|
+
# end
|
134
|
+
id = src[:id] || src['id']
|
135
|
+
raise ArgumentError, "Source must have an Integer 'id' value!", caller unless Integer === id
|
136
|
+
fields = self.fields
|
137
|
+
entity = self.get id
|
138
|
+
entity.update do
|
139
|
+
src.each do |key, value|
|
140
|
+
key = key.intern if String === key
|
141
|
+
field = fields[key] || fields.values.find { |f| f.id_field == key }
|
142
|
+
raise ArgumentError, "Field not found in #{ self.name }: '#{ key }'!", caller unless field
|
143
|
+
if field.write?
|
144
|
+
unless (field.type === value) || (field.id_field == key && Integer === value)
|
145
|
+
if field.id_field == key
|
146
|
+
# do nothing
|
147
|
+
elsif field.type.respond_to?(:parse)
|
148
|
+
if Array === value
|
149
|
+
if field.kind == :backs
|
150
|
+
value = value.map { |v| field.type.parse(v.merge(field.back_field => entity.id)) }
|
151
|
+
else
|
152
|
+
value = value.map { |v| field.type.parse(v) }
|
153
|
+
end
|
154
|
+
else
|
155
|
+
value = field.type.parse(value)
|
156
|
+
end
|
157
|
+
else
|
158
|
+
raise TypeError, "Invalid '#{ key }' value type: #{ value.inspect }!", caller
|
159
|
+
end
|
160
|
+
end
|
161
|
+
entity.send "#{ key }=", value
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
entity
|
166
|
+
# entity.save
|
167
|
+
end
|
168
|
+
|
169
|
+
def ddl
|
170
|
+
"INTEGER REFERENCES #{ self.table } (id)"
|
171
|
+
end
|
172
|
+
|
173
|
+
end
|
174
|
+
|
175
|
+
field :id, type: Integer, primary_key: true
|
176
|
+
|
177
|
+
def initialize id
|
178
|
+
super()
|
179
|
+
self.id = id
|
180
|
+
# @saved = true
|
181
|
+
@s_count = 0
|
182
|
+
end
|
183
|
+
|
184
|
+
def complete?
|
185
|
+
fields = self.class.fields.values.select { |f| f.required? }
|
186
|
+
# values = fields.map { |f| [ f.name, send(f.name) ] }
|
187
|
+
fields.all? { |f| send(f.name) != nil }
|
188
|
+
end
|
189
|
+
|
190
|
+
def save
|
191
|
+
return self if @saved
|
192
|
+
# debug "Save #{ self.class.name } id = #{ self.id } saved = #{ @saved.inspect }"
|
193
|
+
@s_count += 1
|
194
|
+
debug "Saving count = #{ @s_count } [#{ self.class }: #{ self.id }]" if @s_count > 1
|
195
|
+
names = []
|
196
|
+
values = []
|
197
|
+
links = []
|
198
|
+
backs = []
|
199
|
+
# update do
|
200
|
+
self.class.fields.each do |_, field|
|
201
|
+
case field.kind
|
202
|
+
when :value
|
203
|
+
value = self.send(field.name)
|
204
|
+
if Entity === value && value != self # && !value.process?
|
205
|
+
value.save
|
206
|
+
end
|
207
|
+
name, value = field.to_db value
|
208
|
+
if name != nil && value != nil
|
209
|
+
names << name
|
210
|
+
values << value
|
211
|
+
end
|
212
|
+
when :links
|
213
|
+
links << { field: field, values: self.send(field.name) } if field.owned?
|
214
|
+
when :backs
|
215
|
+
backs << { field: field, values: self.send(field.name) } if field.owned?
|
216
|
+
end
|
217
|
+
end
|
218
|
+
# end
|
219
|
+
names = names.flatten
|
220
|
+
values = values.flatten
|
221
|
+
# DB.transaction do |db|
|
222
|
+
DB.execute "INSERT OR REPLACE INTO #{ self.class.table } (#{ names.join(',') }) VALUES (#{ (['?'] * values.size).join(',') });", *values
|
223
|
+
@saved = true
|
224
|
+
links.each do |link|
|
225
|
+
field = link[:field]
|
226
|
+
values = link[:values]
|
227
|
+
olinks = []
|
228
|
+
values.each do |value|
|
229
|
+
value.save if value != self
|
230
|
+
# DB.execute "INSERT OR REPLACE INTO #{ field.table_name } (#{ field.back_field }, #{ field.link_field }) VALUES (?, ?);", self.id, value.id
|
231
|
+
olinks << "INSERT OR REPLACE INTO #{ field.table_name } (#{ field.back_field }, #{ field.link_field }) VALUES (#{ self.id }, #{ value.id });"
|
232
|
+
end
|
233
|
+
DB.execute_batch olinks.join("\n")
|
234
|
+
news = values.map(&:id)
|
235
|
+
olds = DB.execute("SELECT #{ field.link_field } as id FROM #{ field.table_name } WHERE #{ field.back_field } = ?;", self.id).map { |r| r['id'] }
|
236
|
+
diff = olds.filter { |o| !news.include?(o) }
|
237
|
+
if !diff.empty?
|
238
|
+
DB.execute "DELETE FROM #{ field.table_name } WHERE #{ field.back_field } = ? AND #{ field.link_field } IN (#{ (['?'] * diff.size).join(',') });",
|
239
|
+
self.id, *diff
|
240
|
+
end
|
241
|
+
end
|
242
|
+
backs.each do |back|
|
243
|
+
field = back[:field]
|
244
|
+
values = back[:values]
|
245
|
+
values.each do |value|
|
246
|
+
value.send "#{ field.back_field }=", self.id
|
247
|
+
value.save if value != self
|
248
|
+
end
|
249
|
+
news = values.map(&:id)
|
250
|
+
olds = DB.execute("SELECT id FROM #{ field.type.table } WHERE #{ field.back_field } = ?;", self.id).map { |r| r['id'] }
|
251
|
+
diff = olds.filter { |o| !news.include?(o) }
|
252
|
+
if !diff.empty?
|
253
|
+
DB.execute "DELETE FROM #{ field.type.table } WHERE #{ field.back_field } = ? AND id IN (#{ (['?'] * diff.size).join(',') });",
|
254
|
+
self.id, *diff
|
255
|
+
end
|
256
|
+
end
|
257
|
+
# end
|
258
|
+
# @saved = true
|
259
|
+
# TODO: разобраться и почистить двойное присваивание
|
260
|
+
self
|
261
|
+
end
|
262
|
+
|
263
|
+
def to_db
|
264
|
+
self.id
|
265
|
+
end
|
266
|
+
|
267
|
+
end
|
268
|
+
|
269
|
+
module BySLUG
|
270
|
+
|
271
|
+
def by_slug slug
|
272
|
+
# Status::status '[fetch]', "#{ self } : #{ slug } ..."
|
273
|
+
@entities ||= {}
|
274
|
+
results = @entities.values.select { |e| e.slug == slug.intern }
|
275
|
+
if results.empty?
|
276
|
+
data = DB.execute "SELECT * FROM #{ table } WHERE slug = ?", slug.to_s
|
277
|
+
results = from_db_rows data
|
278
|
+
end
|
279
|
+
if results.empty?
|
280
|
+
data = API.get @api_path, :path, 1, slug
|
281
|
+
results = data.map { |d| parse(d) }
|
282
|
+
end
|
283
|
+
# Status::status '[fetch]', "#{ self } : #{ slug } DONE"
|
284
|
+
if results.empty?
|
285
|
+
nil
|
286
|
+
else
|
287
|
+
results.first
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'extra/enum'
|
4
|
+
|
5
|
+
class CS < Enum
|
6
|
+
|
7
|
+
item :NE, data: 0
|
8
|
+
item :DD, data: 5
|
9
|
+
item :LC, data: 10
|
10
|
+
item :NT, data: 20
|
11
|
+
item :VU, data: 30
|
12
|
+
item :EN, data: 40
|
13
|
+
item :CR, data: 50
|
14
|
+
item :EW, data: 60
|
15
|
+
item :EX, data: 70
|
16
|
+
|
17
|
+
item_alias :not_evaluated => :NE,
|
18
|
+
:data_deficient => :DD,
|
19
|
+
:least_concern => :LC,
|
20
|
+
:near_threatened => :NT,
|
21
|
+
:vulnerable => :VU,
|
22
|
+
:endangered => :EN,
|
23
|
+
:critically_endangered => :CR,
|
24
|
+
:extinct_in_the_wild => :EW,
|
25
|
+
:extinct => :EX
|
26
|
+
|
27
|
+
freeze
|
28
|
+
end
|
29
|
+
|
30
|
+
ConservationStatus = CS
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'extra/enum'
|
4
|
+
|
5
|
+
class IconicTaxa < Enum
|
6
|
+
|
7
|
+
items :Aves,
|
8
|
+
:Amphibia,
|
9
|
+
:Reptilia,
|
10
|
+
:Mammalia,
|
11
|
+
:Actinopterygii,
|
12
|
+
:Mollusca,
|
13
|
+
:Arachnida,
|
14
|
+
:Insecta,
|
15
|
+
:Animalia,
|
16
|
+
:Plantae,
|
17
|
+
:Fungi,
|
18
|
+
:Protozoa,
|
19
|
+
:Chromista,
|
20
|
+
:unknown
|
21
|
+
|
22
|
+
freeze
|
23
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'extra/enum'
|
4
|
+
|
5
|
+
class ProjectType < Enum
|
6
|
+
|
7
|
+
items :collection,
|
8
|
+
:umbrella,
|
9
|
+
:contest,
|
10
|
+
:bioblitz,
|
11
|
+
:assessment,
|
12
|
+
:manual
|
13
|
+
|
14
|
+
# TODO: переделать тип во что-то универсальное. наверное.
|
15
|
+
|
16
|
+
class << self
|
17
|
+
|
18
|
+
def parse src
|
19
|
+
if src == ''
|
20
|
+
return ProjectType::MANUAL
|
21
|
+
else
|
22
|
+
super src
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_s
|
29
|
+
if self == ProjectType::MANUAL
|
30
|
+
return ''
|
31
|
+
else
|
32
|
+
super
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
freeze
|
37
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'extra/enum'
|
4
|
+
|
5
|
+
class Rank < Enum
|
6
|
+
|
7
|
+
item :stateofmatter, data: 100
|
8
|
+
item :kingdom, data: 70
|
9
|
+
item :phylum, data: 60
|
10
|
+
item :subphylum, data: 57
|
11
|
+
item :superclass, data: 53
|
12
|
+
item :class, data: 50
|
13
|
+
item :subclass, data: 47
|
14
|
+
item :infraclass, data: 45
|
15
|
+
item :subterclass, data: 44
|
16
|
+
item :superorder, data: 43
|
17
|
+
item :order, data: 40
|
18
|
+
item :suborder, data: 37
|
19
|
+
item :infraorder, data: 35
|
20
|
+
item :parvorder, data: 34.5
|
21
|
+
item :zoosection, data: 34
|
22
|
+
item :zoosubsection, data: 33.5
|
23
|
+
item :superfamily, data: 33
|
24
|
+
item :epifamily, data: 32
|
25
|
+
item :family, data: 30
|
26
|
+
item :subfamily, data: 27
|
27
|
+
item :supertribe, data: 26
|
28
|
+
item :tribe, data: 25
|
29
|
+
item :subtribe, data: 24
|
30
|
+
item :genus, data: 20
|
31
|
+
item :genushybrid, data: 20
|
32
|
+
item :subgenus, data: 15
|
33
|
+
item :section, data: 13
|
34
|
+
item :subsection, data: 12
|
35
|
+
item :complex, data: 11
|
36
|
+
item :species, data: 10
|
37
|
+
item :hybrid, data: 10
|
38
|
+
item :subspecies, data: 5
|
39
|
+
item :variety, data: 5
|
40
|
+
item :form, data: 5
|
41
|
+
item :infrahybrid, data: 5
|
42
|
+
|
43
|
+
item_alias :division => :phylum,
|
44
|
+
:'sub-class' => :subclass,
|
45
|
+
:'super-order' => :superorder,
|
46
|
+
:'sub-order' => :suborder,
|
47
|
+
:'super-family' => :superfamily,
|
48
|
+
:'sub-family' => :subfamily,
|
49
|
+
:gen => :genus,
|
50
|
+
:sp => :species,
|
51
|
+
:spp => :species,
|
52
|
+
:infraspecies => :subspecies,
|
53
|
+
:ssp => :subspecies,
|
54
|
+
:'sub-species' => :subspecies,
|
55
|
+
:subsp => :subspecies,
|
56
|
+
:trinomial => :subspecies,
|
57
|
+
:var => :variety
|
58
|
+
|
59
|
+
freeze
|
60
|
+
end
|