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.
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