inat-get 0.8.0.11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +674 -0
  3. data/README.md +16 -0
  4. data/Rakefile +4 -0
  5. data/bin/inat-get +59 -0
  6. data/docs/logo.png +0 -0
  7. data/inat-get.gemspec +33 -0
  8. data/lib/extra/enum.rb +184 -0
  9. data/lib/extra/period.rb +252 -0
  10. data/lib/extra/uuid.rb +90 -0
  11. data/lib/inat/app/application.rb +50 -0
  12. data/lib/inat/app/config/messagelevel.rb +22 -0
  13. data/lib/inat/app/config/shiftage.rb +24 -0
  14. data/lib/inat/app/config/updatemode.rb +20 -0
  15. data/lib/inat/app/config.rb +296 -0
  16. data/lib/inat/app/globals.rb +80 -0
  17. data/lib/inat/app/info.rb +21 -0
  18. data/lib/inat/app/logging.rb +35 -0
  19. data/lib/inat/app/preamble.rb +27 -0
  20. data/lib/inat/app/status.rb +74 -0
  21. data/lib/inat/app/task/context.rb +47 -0
  22. data/lib/inat/app/task/dsl.rb +24 -0
  23. data/lib/inat/app/task.rb +75 -0
  24. data/lib/inat/data/api.rb +218 -0
  25. data/lib/inat/data/cache.rb +9 -0
  26. data/lib/inat/data/db.rb +87 -0
  27. data/lib/inat/data/ddl.rb +18 -0
  28. data/lib/inat/data/entity/comment.rb +29 -0
  29. data/lib/inat/data/entity/flag.rb +22 -0
  30. data/lib/inat/data/entity/identification.rb +45 -0
  31. data/lib/inat/data/entity/observation.rb +172 -0
  32. data/lib/inat/data/entity/observationphoto.rb +25 -0
  33. data/lib/inat/data/entity/observationsound.rb +26 -0
  34. data/lib/inat/data/entity/photo.rb +31 -0
  35. data/lib/inat/data/entity/place.rb +57 -0
  36. data/lib/inat/data/entity/project.rb +94 -0
  37. data/lib/inat/data/entity/projectadmin.rb +21 -0
  38. data/lib/inat/data/entity/projectobservationrule.rb +50 -0
  39. data/lib/inat/data/entity/request.rb +58 -0
  40. data/lib/inat/data/entity/sound.rb +27 -0
  41. data/lib/inat/data/entity/taxon.rb +94 -0
  42. data/lib/inat/data/entity/user.rb +67 -0
  43. data/lib/inat/data/entity/vote.rb +22 -0
  44. data/lib/inat/data/entity.rb +291 -0
  45. data/lib/inat/data/enums/conservationstatus.rb +30 -0
  46. data/lib/inat/data/enums/geoprivacy.rb +14 -0
  47. data/lib/inat/data/enums/iconictaxa.rb +23 -0
  48. data/lib/inat/data/enums/identificationcategory.rb +13 -0
  49. data/lib/inat/data/enums/licensecode.rb +16 -0
  50. data/lib/inat/data/enums/projectadminrole.rb +11 -0
  51. data/lib/inat/data/enums/projecttype.rb +37 -0
  52. data/lib/inat/data/enums/qualitygrade.rb +12 -0
  53. data/lib/inat/data/enums/rank.rb +60 -0
  54. data/lib/inat/data/model.rb +551 -0
  55. data/lib/inat/data/query.rb +1145 -0
  56. data/lib/inat/data/sets/dataset.rb +104 -0
  57. data/lib/inat/data/sets/list.rb +190 -0
  58. data/lib/inat/data/sets/listers.rb +15 -0
  59. data/lib/inat/data/sets/wrappers.rb +137 -0
  60. data/lib/inat/data/types/extras.rb +88 -0
  61. data/lib/inat/data/types/location.rb +89 -0
  62. data/lib/inat/data/types/std.rb +293 -0
  63. data/lib/inat/report/table.rb +135 -0
  64. data/lib/inat/utils/deep.rb +30 -0
  65. 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,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'extra/enum'
4
+
5
+ class GeoPrivacy < Enum
6
+
7
+ items :open,
8
+ :obscured,
9
+ :obscured_private
10
+
11
+ item_alias :private => :obscured_private
12
+
13
+ freeze
14
+ end
@@ -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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'extra/enum'
4
+
5
+ class IdentificationCategory < Enum
6
+
7
+ items :improving,
8
+ :supporting,
9
+ :leading,
10
+ :maverick
11
+
12
+ freeze
13
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'extra/enum'
4
+
5
+ class LicenseCode < Enum
6
+
7
+ items :'cc0',
8
+ :'cc-by',
9
+ :'cc-by-nc',
10
+ :'cc-by-nd',
11
+ :'cc-by-sa',
12
+ :'cc-by-nc-nd',
13
+ :'cc-by-nc-sa'
14
+
15
+ freeze
16
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'extra/enum'
4
+
5
+ class ProjectAdminRole < Enum
6
+
7
+ items :curator,
8
+ :manager
9
+
10
+ freeze
11
+ 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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'extra/enum'
4
+
5
+ class QualityGrade < Enum
6
+
7
+ items :research,
8
+ :needs_id,
9
+ :casual
10
+
11
+ freeze
12
+ 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