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,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'extra/enum'
4
+
5
+ class MessageLevel < Enum
6
+
7
+ item :TRACE, -1
8
+ item :DEBUG, data: 0
9
+ item :INFO, data: 1
10
+ item :WARNING, data: 2
11
+ item :ERROR, data: 3
12
+ item :FATAL, data: 4
13
+ item :UNKNOWN, data: 5
14
+
15
+ item_alias :WARN => :WARNING
16
+
17
+ def severity
18
+ self.data
19
+ end
20
+
21
+ freeze
22
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShiftAge
4
+
5
+ class << self
6
+
7
+ def parse source
8
+ source = source.to_i if /\d+/ === source
9
+ source = source.to_s.downcase.intern if Symbol === source || String === source
10
+ case source
11
+ when nil
12
+ 0
13
+ when Integer
14
+ source
15
+ when :daily, :weekly, :monthly
16
+ source
17
+ else
18
+ raise TypeError, "Invalid shift age value: #{source}!"
19
+ end
20
+ end
21
+
22
+ end
23
+
24
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'extra/enum'
4
+
5
+ class UpdateMode < Enum
6
+
7
+ items :UPDATE,
8
+ :FORCE,
9
+ :RELOAD,
10
+ :MINIMAL,
11
+ :OFFLINE
12
+
13
+ item_alias :DEFAULT => :UPDATE,
14
+ :FORCE_UPDATE => :FORCE,
15
+ :FORCE_RELOAD => :RELOAD,
16
+ :SKIP_EXISTING => :MINIMAL,
17
+ :NO_UPDATE => :OFFLINE
18
+
19
+ freeze
20
+ end
@@ -0,0 +1,296 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'optparse'
5
+
6
+ require_relative '../utils/deep'
7
+ require_relative '../../extra/period'
8
+ require_relative 'info'
9
+ require_relative 'config/messagelevel'
10
+ require_relative 'config/updatemode'
11
+ require_relative 'config/shiftage'
12
+
13
+ class Application
14
+
15
+ include AppInfo
16
+
17
+ API_DEFAULT = 'https://api.inaturalist.org/v1/'
18
+ DEFAULT_LOG = "./#{ NAME }.log"
19
+
20
+ DEFAULTS = {
21
+ threads: {
22
+ enable: true,
23
+ tasks: 3,
24
+ },
25
+ data: {
26
+ cache: true,
27
+ directory: File.expand_path("~/.cache/#{ NAME }/"),
28
+ update: UpdateMode::UPDATE,
29
+ update_period: Period::DAY,
30
+ outdate_period: Period::WEEK,
31
+ },
32
+ verbose: MessageLevel::ERROR,
33
+ log: {
34
+ enable: false,
35
+ file: DEFAULT_LOG,
36
+ level: MessageLevel::WARNING,
37
+ shift: {
38
+ age: nil,
39
+ size: nil,
40
+ },
41
+ },
42
+ api: {
43
+ root: API_DEFAULT,
44
+ locale: nil,
45
+ preferred_place_id: nil,
46
+ open_timeout: nil,
47
+ read_timeout: nil,
48
+ },
49
+ preamble: [],
50
+ }
51
+
52
+ CONFIG_FILE = File.expand_path "~/.config/#{ NAME }.yml"
53
+
54
+ private def parse_files files
55
+ files.map do |file|
56
+ if file.start_with?('@')
57
+ list = file[1..]
58
+ File.readlines(list, chomp: true).filter { |l| !l.empty? }
59
+ else
60
+ file
61
+ end
62
+ end.flatten
63
+ end
64
+
65
+ private def parse_args!
66
+ options = {}
67
+ opts = OptionParser::new USAGE do |o|
68
+
69
+ o.accept UpdateMode do |mode|
70
+ UpdateMode::parse mode, case_sensitive: false
71
+ end
72
+
73
+ o.accept MessageLevel do |level|
74
+ MessageLevel::parse level, case_sensitive: false
75
+ end
76
+
77
+ o.accept ShiftAge do |shift_age|
78
+ ShiftAge::parse shift_age
79
+ end
80
+
81
+ o.accept Period do |period|
82
+ Period::parse period
83
+ end
84
+
85
+ o.separator ''
86
+ o.separator "\e[1mInformation:\e[0m"
87
+
88
+ o.on '-h', '--help', 'Show help and exit.' do
89
+ puts ABOUT
90
+ puts
91
+ puts opts.help
92
+ exit 0
93
+ end
94
+
95
+ o.on '-?', '--usage', 'Show short help and exit.' do
96
+ puts opts.help
97
+ exit 0
98
+ end
99
+
100
+ o.on '--about', 'Show program info and exit.' do
101
+ puts ABOUT
102
+ exit 0
103
+ end
104
+
105
+ o.on '--version', 'Show version and exit.' do
106
+ puts VERSION
107
+ exit 0
108
+ end
109
+
110
+ o.separator ''
111
+ o.separator "\e[1mConfiguration:\e[0m"
112
+
113
+ o.on '-c', '--config FILE', String, "Configuration file instead standard (~/.config/#{ NAME }.yml)." do |value|
114
+ options[:config_file] = value
115
+ end
116
+
117
+ o.separator ''
118
+ o.separator "\e[1mFlow Control:\e[0m"
119
+
120
+ o.on '-1', '--no-threads', 'Disable threads.' do
121
+ options[:threads] ||= {}
122
+ options[:threads][:enable] = false
123
+ end
124
+
125
+ o.on '-t', '--tasks NUMBER', Integer, 'Limit number of concurrent tasks.' do |value|
126
+ options[:threads] ||= {}
127
+ options[:threads][:tasks] = value
128
+ end
129
+
130
+ o.separator ''
131
+ o.separator "\e[1mCache Control:\e[0m"
132
+
133
+ o.on '-u', '--update MODE', UpdateMode, "Update mode:", " 'default' (or 'update')",
134
+ " 'force-update' (or 'force')",
135
+ " 'force-reload' (or 'reload')",
136
+ " 'skip-existing' (or 'minimal')",
137
+ " 'no-update' (or 'offline')" do |value|
138
+ options[:data] ||= {}
139
+ options[:data][:update] = value
140
+ end
141
+
142
+ o.on '-f', '--force-update', 'Force update.' do
143
+ options[:data] ||= {}
144
+ options[:data][:update] = UpdateMode::FORCE
145
+ end
146
+
147
+ o.on '-F', '--force-reload', '--reload', 'Force reload.' do
148
+ options[:data] ||= {}
149
+ options[:data][:update] = UpdateMode::RELOAD
150
+ end
151
+
152
+ o.on '--skip-existing', 'Skip updating if exist.' do
153
+ options[:data] ||= {}
154
+ options[:data][:update] = UpdateMode::MINIMAL
155
+ end
156
+
157
+ o.on '-n', '--offline', '--no-update', 'No updating.' do
158
+ options[:data] ||= {}
159
+ options[:data][:update] = UpdateMode::OFFLINE
160
+ end
161
+
162
+ o.separator ''
163
+
164
+ o.on '--update-period PERIOD', Period, 'Update period.' do |value|
165
+ options[:data] ||= {}
166
+ options[:data][:update_period] = value
167
+ end
168
+
169
+ o.on '--obsolete-period PERIOD', Period, 'Obsolete period.' do |value|
170
+ options[:data] ||= {}
171
+ options[:data][:obsolete_period] = value
172
+ end
173
+
174
+ o.on '--cache-directory PATH', String, 'Cache directory path.' do |value|
175
+ options[:data] ||= {}
176
+ options[:data][:directory] = value
177
+ end
178
+
179
+ o.separator ''
180
+
181
+ o.on '--clean-requests', 'Clean outdated requests in data cache.' do
182
+ options[:preamble] << :clean_requests
183
+ end
184
+
185
+ o.on '--clean-observations', 'Clean observations without request in data cache.' do
186
+ options[:preamble] << :clean_observations
187
+ end
188
+
189
+ o.on '--clean-orphans', 'Clean orphan objects in data cache.' do
190
+ options[:preamble] << :clean_orphans
191
+ end
192
+
193
+ o.on '--clean-all', 'Clean data cache.' do
194
+ options[:preamble] << :clean_all
195
+ end
196
+
197
+ o.separator ''
198
+ o.separator "\e[1mAPI Parameters:\e[0m"
199
+
200
+ o.on '-A', '--api-root URI', String, 'API root' do |value|
201
+ options[:api] ||= {}
202
+ options[:api][:root] = value
203
+ end
204
+
205
+ o.on '-L', '--locale LOCALE', String, 'Set the locale.' do |value|
206
+ value = nil if value.downcase == 'none'
207
+ options[:api] ||= {}
208
+ options[:api][:locale] = value
209
+ end
210
+
211
+ o.on '-P', '--preferred-place ID', Integer, 'Preferred place for API.' do |value|
212
+ value = nil if value == 0
213
+ options[:api] ||= {}
214
+ options[:api][:preferred_place_id] = value
215
+ end
216
+
217
+ o.separator ''
218
+ o.separator "\e[1mLogging Control:\e[0m"
219
+
220
+ o.on '--verbose-level LEVEL', MessageLevel, 'Set the logging level for $STDERR.', " 'fatal'",
221
+ " 'error'",
222
+ " 'warn' (or 'warning')",
223
+ " 'info'",
224
+ " 'debug'" do |value|
225
+ options[:verbose] = value
226
+ end
227
+
228
+ o.on '-v', '--verbose', 'Set verbose level to INFO.' do
229
+ options[:verbose] = MessageLevel::INFO
230
+ end
231
+
232
+ o.on '--log-level LEVEL', MessageLevel, 'Set the logging level for file log' do |value|
233
+ options[:log] ||= {}
234
+ options[:log][:level] = value
235
+ end
236
+
237
+ o.on '--log-file FILE', String, 'File name for log.' do |value|
238
+ value = DEFAULT_LOG if /^default$/i === value
239
+ options[:log] ||= {}
240
+ options[:log][:file] = value
241
+ end
242
+
243
+ o.on '-l', '--log [FILE]', String, "Enable logging to file. If file is not specified './#{ NAME }.log' used.",
244
+ " 'none', 'no' or 'false' to disabling." do |value|
245
+ options[:log] ||= {}
246
+ case value
247
+ when nil, /^(true|yes|default)$/i
248
+ options[:log][:enable] = true
249
+ options[:log][:file] = DEFAULT_LOG
250
+ when /^(false|no|none)$/i
251
+ options[:log][:enable] = false
252
+ options[:log][:file] = DEFAULT_LOG
253
+ else
254
+ options[:log][:enable] = true
255
+ options[:log][:file] = value.to_s
256
+ end
257
+ end
258
+
259
+ o.on '--log-shift-age AGE', ShiftAge, 'Log file shift age.' do |value|
260
+ value = nil if value == 0
261
+ options[:log] ||= {}
262
+ options[:log][:shift] ||= {}
263
+ options[:log][:shift][:age] = value
264
+ end
265
+
266
+ o.on '--log-shift-size SIZE', Integer, 'Log file shift size.' do |value|
267
+ value = nil if value == 0
268
+ options[:log] ||= {}
269
+ options[:log][:shift] ||= {}
270
+ options[:log][:shift][:size] = value
271
+ end
272
+
273
+ o.separator ''
274
+
275
+ o.separator "\e[1mFile Arguments:\e[0m"
276
+ o.separator "\t‹task[, ...]›\t\t One or more names of task files.\n" +
277
+ "\t\t\t\t If name has not extension try to read '‹task›' than '‹task›.inat' than '‹task›.rb'."
278
+
279
+ end
280
+ files = parse_files(opts.parse(ARGV))
281
+ [ options, files ]
282
+ end
283
+
284
+ private def setup!
285
+ @config = DEFAULTS
286
+ options, @files = parse_args!
287
+ config_file = options[:config_file] || CONFIG_FILE
288
+ @config.deep_merge! YAML.load_file(config_file, symbolize_names: true, freeze: true) if File.exist?(config_file)
289
+ @config.deep_merge! options
290
+ @config[:verbose] = MessageLevel::parse @config[:verbose]
291
+ @config[:log][:level] = MessageLevel::parse @config[:log][:level]
292
+ end
293
+
294
+ attr_reader :config
295
+
296
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Globals
4
+
5
+ class << self
6
+
7
+ def logger
8
+ @@logger
9
+ end
10
+
11
+ def logger= value
12
+ @@logger = value
13
+ end
14
+
15
+ def current_task
16
+ Thread.current.thread_variable_get 'current_task'
17
+ end
18
+
19
+ def current_task= value
20
+ Thread.current.thread_variable_set 'current_task', value
21
+ end
22
+
23
+ def config
24
+ @@config
25
+ end
26
+
27
+ def config= value
28
+ @@config = value
29
+ end
30
+
31
+ def status
32
+ @@status
33
+ end
34
+
35
+ def status= value
36
+ @@status = value
37
+ end
38
+
39
+ end
40
+
41
+ end
42
+
43
+ G = Globals
44
+
45
+ module LogDSL
46
+
47
+ def included mod
48
+ mod.extend LogDSL
49
+ end
50
+
51
+ def log message, level: MessageLevel::INFO
52
+ G.status.wrap do
53
+ G.logger.log(G.current_task&.name || '‹main›', level, message)
54
+ end
55
+ end
56
+
57
+ def error message, exception: nil
58
+ log message, level: MessageLevel::ERROR
59
+ raise exception, message if Class === exception && Exception >= exception
60
+ end
61
+
62
+ def warning message
63
+ log message, level: MessageLevel::WARNING
64
+ end
65
+
66
+ def info message
67
+ log message, level: MessageLevel::INFO
68
+ end
69
+
70
+ def debug message
71
+ log message, level: MessageLevel::DEBUG
72
+ end
73
+
74
+ def echo message, level: MessageLevel::UNKNOWN
75
+ log message, level: level
76
+ end
77
+
78
+ module_function :log, :error, :warning, :info, :debug, :echo
79
+
80
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppInfo
4
+
5
+ AUTHOR = 'Ivan Shikhalev'
6
+ EMAIL = 'shkikhalev@gmail.com'
7
+
8
+ VERSION = '0.8.0.11'
9
+ HOMEPAGE = 'https://github.com/shikhalev/inat-get'
10
+ SOURCE_URL = 'https://github.com/shikhalev/inat-get'
11
+
12
+ EXE = File.basename $0
13
+ NAME = File.basename $0, '.rb'
14
+
15
+ USAGE = "\e[1mUsage $\e[0m #{EXE} [options] ‹task[, ...]›"
16
+ ABOUT = "\e[1mINat::Get v#{VERSION}\e[0m — A toolset for fetching and processing data from \e[4miNaturalist.org\e[0m.\n" +
17
+ "\n" +
18
+ "\e[1mSource:\e[0m \e[4m#{HOMEPAGE}\e[0m\n" +
19
+ "\e[1mAuthor:\e[0m #{AUTHOR} ‹\e[4m#{EMAIL}\e[0m›"
20
+
21
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ class DualLogger
6
+
7
+ def config
8
+ @application.config
9
+ end
10
+
11
+ def initialize application
12
+ @application = application
13
+ v_para = {
14
+ level: config[:verbose].severity
15
+ }
16
+ @verb = Logger::new STDERR, **v_para
17
+ if config[:log][:enable]
18
+ f_para = {
19
+ level: config[:log][:level].severity,
20
+ }
21
+ f_para[:shift_age] = config[:log][:shift][:age] if config[:log][:shift][:age]
22
+ f_para[:shift_level] = config[:log][:shift][:level] if config[:log][:shift][:level]
23
+ @file = Logger::new config[:log][:file], **f_para
24
+ else
25
+ @file = nil
26
+ end
27
+ end
28
+
29
+ def log task, level, message
30
+ task = task.name if task.respond_to?(:name)
31
+ @verb.log(level.severity, message, task)
32
+ @file.log(level.severity, message, task) if @file
33
+ end
34
+
35
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppPreamble
4
+
5
+ private def do_clean_requests
6
+ # NEED: implement
7
+ end
8
+
9
+ private def do_clean_observations
10
+ # NEED: implement
11
+ end
12
+
13
+ private def do_clean_orphans
14
+ # NEED: implement
15
+ end
16
+
17
+ private def do_clean_all
18
+ # NEED: implement
19
+ end
20
+
21
+ private def preamble!
22
+ config[:preamble].each do |name|
23
+ self.send "do_#{name}"
24
+ end
25
+ end
26
+
27
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'globals'
4
+
5
+ # TODO: переделать на отдельный поток и очередь статусов. Ввести отдельно ключ, как идентификатор запроса, например.
6
+
7
+ module Status
8
+
9
+ class << self
10
+
11
+ private def ellipsis src, len
12
+ return '' unless String === src
13
+ if src.length <= len
14
+ src
15
+ else
16
+ src[.. len - 4] + '...'
17
+ end
18
+ end
19
+
20
+ # private def get_num
21
+ # @num ||= 0
22
+ # @num += 1
23
+ # @num
24
+ # end
25
+
26
+ def init
27
+ @lines = {}
28
+ @mutex = Mutex::new
29
+ @shift = 0
30
+ end
31
+
32
+ def up
33
+ $stderr.print "\e[#{ @shift }A\r" if @shift > 0
34
+ end
35
+
36
+ def out
37
+ $stderr.print "\e[0K\n"
38
+ @lines.each do |key, value|
39
+ $stderr.print "#{ value }\e[0K\n"
40
+ end
41
+ $stderr.print "\e[0K\n"
42
+ @shift = @lines.size + 2
43
+ end
44
+
45
+ def wrap
46
+ @mutex.synchronize do
47
+ up
48
+ yield
49
+ out
50
+ end
51
+ end
52
+
53
+ def status key, value
54
+ task = ellipsis G.current_task&.name, 16
55
+ key = task if key == nil
56
+ line = format("%16s | %s", key.to_s, value)
57
+ @mutex.synchronize do
58
+ @lines ||= {}
59
+ @lines[key] = line
60
+ up
61
+ out
62
+ end
63
+ end
64
+
65
+ end
66
+
67
+ private def status str
68
+ self.class.status str
69
+ end
70
+
71
+ end
72
+
73
+ Status::init
74
+ G.status = Status
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'dsl'
4
+ require_relative '../../report/table'
5
+
6
+ class Task::Context
7
+
8
+ include Task::DSL
9
+ include TableDSL
10
+
11
+ attr_accessor :name
12
+
13
+ def config
14
+ @task.config
15
+ end
16
+
17
+ def logger
18
+ @task.logger
19
+ end
20
+
21
+ def done?
22
+ @done
23
+ end
24
+
25
+ def initialize task, name, path
26
+ @task = task
27
+ @name = name
28
+ @done = false
29
+ source_code = File.readlines(path).map { |line| " #{line}" }.join('')
30
+ full_code = "define_singleton_method :execute do\n" +
31
+ " begin\n" +
32
+ " #{source_code}\n" +
33
+ " Status::status nil, 'DONE'\n" +
34
+ " rescue Exception => e\n" +
35
+ " error \"\#{e.inspect}\"\n" +
36
+ " error \"\#{e.backtrace.join(\"\\n\\t\")}\"\n" +
37
+ " Status::status nil, 'ERROR : ' + e.inspect\n" +
38
+ " rescue\n" +
39
+ " error 'Unknown error!'\n" +
40
+ " Status::status nil, 'ERROR'\n" +
41
+ " end\n" +
42
+ " @done = true\n" +
43
+ "end"
44
+ self.instance_eval full_code
45
+ end
46
+
47
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../globals'
4
+ require_relative '../config/messagelevel'
5
+
6
+ require_relative '../../data/entity/observation'
7
+ require_relative '../../data/query'
8
+ require_relative '../../data/sets/dataset'
9
+ require_relative '../../data/sets/list'
10
+
11
+ class Task; end
12
+
13
+ module Task::DSL
14
+
15
+ include LogDSL
16
+
17
+ def select **params
18
+ query = Query::new(**params)
19
+ DataSet::new nil, query.observations
20
+ end
21
+
22
+ module_function :select
23
+
24
+ end