inat-get 0.8.0.11
Sign up to get free protection for your applications and to get access to all the features.
- 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
data/README.md
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
<img src="docs/logo.png" align="right" style="float:right;width:57%;">
|
2
|
+
|
3
|
+
# INat::Get
|
4
|
+
|
5
|
+
A toolset for fetching and processing data from **[iNaturalist.org][inat]**.
|
6
|
+
|
7
|
+
The author of this software is not associated or affiliated with the iNaturalist.
|
8
|
+
Only the [public API][api] is used in accordance with [the conditions][cond].
|
9
|
+
|
10
|
+
[inat]: https://www.inaturalist.org/
|
11
|
+
[api]: https://api.inaturalist.org/v1/docs/
|
12
|
+
[cond]: https://api.inaturalist.org/v1/docs/#terms-of-use
|
13
|
+
|
14
|
+
## Current status
|
15
|
+
|
16
|
+
Early alpha version.
|
data/Rakefile
ADDED
data/bin/inat-get
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
#!/usr/bin/ruby -w
|
2
|
+
|
3
|
+
require 'date'
|
4
|
+
require 'pp'
|
5
|
+
|
6
|
+
# require 'extra/enum'
|
7
|
+
|
8
|
+
require 'inat/app/application'
|
9
|
+
require 'inat/app/globals'
|
10
|
+
require 'inat/data/entity/observation'
|
11
|
+
require 'inat/data/entity/request'
|
12
|
+
require 'inat/data/db'
|
13
|
+
require 'inat/data/query'
|
14
|
+
|
15
|
+
app = Application.new
|
16
|
+
app.run
|
17
|
+
# PP::pp Globals.config, $>, 64
|
18
|
+
|
19
|
+
# puts
|
20
|
+
# puts DDL.DDL
|
21
|
+
|
22
|
+
# PP::pp DB::instance, $>, 64
|
23
|
+
|
24
|
+
# data = API::query(:observations, user_login: 'shikhalev', month: 11)
|
25
|
+
|
26
|
+
# PP::pp data.map { |d| Observation::parse(d) }.size, $>, 64
|
27
|
+
|
28
|
+
# https://www.inaturalist.org/projects/174222
|
29
|
+
# https://www.inaturalist.org/places/193592
|
30
|
+
# https://www.inaturalist.org/places/193881 (Ачитский район)
|
31
|
+
# https://www.inaturalist.org/places/194104 (Богдановичский район)
|
32
|
+
# https://www.inaturalist.org/places/194414 (Качканарский район)
|
33
|
+
# https://www.inaturalist.org/places/194023 (Берёзовский район)
|
34
|
+
# https://api.inaturalist.org/v1/observations?project_id=176067&updated_since=2023-10-31T19:36:29+05:00&per_page=200&order_by=id&order=asc&locale=ru&preferred_place_id=11829
|
35
|
+
|
36
|
+
# W, [2023-11-02T01:54:41.818021 #8118] WARN -- ‹main›: Some Taxon IDs were not fetched: 48460, 1, 47120, 245097, 47119, 47118, 120474, 342614, 319384, 67599, 495875, 153683, 153680, 367182, 48893, 1252003, 60920!
|
37
|
+
# W, [2023-11-02T03:49:58.509031 #8118] WARN -- ‹main›: Some Taxon IDs were not fetched: 1, 47118, 47119, 47120, 48460, 48893, 60920, 67599, 120474, 153680, 153683, 245097, 319384, 342614, 367182, 495875, 1252003!
|
38
|
+
|
39
|
+
# query = Query::new project_id: 180212
|
40
|
+
# PP::pp query.observations.size, $>, 64
|
41
|
+
|
42
|
+
# require 'inat/data/types/apiquery'
|
43
|
+
|
44
|
+
# pp Time::new.to_date.to_s
|
45
|
+
|
46
|
+
# a = ApiQuery::new ololo: 1, lalala: UUID.generate
|
47
|
+
|
48
|
+
# PP::pp a, $>, 64
|
49
|
+
# puts a.to_query
|
50
|
+
|
51
|
+
# Request::create ''
|
52
|
+
|
53
|
+
# pp URI::parse('https://api.inaturalist.org/v1/observations?project_id=176067&updated_since=2023-10-31T19:36:29+05:00&per_page=200&order_by=id&order=asc&locale=ru&preferred_place_id=11829')
|
54
|
+
|
55
|
+
# d = Date::parse '2023-11-01'
|
56
|
+
# t = d.to_time
|
57
|
+
# i = t.to_i
|
58
|
+
|
59
|
+
# pp [ d, t, i ]
|
data/docs/logo.png
ADDED
Binary file
|
data/inat-get.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lib/inat/app/info'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "inat-get"
|
7
|
+
spec.version = AppInfo::VERSION
|
8
|
+
spec.authors = [ AppInfo::AUTHOR ]
|
9
|
+
spec.email = [ AppInfo::EMAIL ]
|
10
|
+
|
11
|
+
spec.summary = "Client for iNaturalist API."
|
12
|
+
# spec.description = "TODO: Write a longer description or delete this line."
|
13
|
+
spec.homepage = AppInfo::HOMEPAGE
|
14
|
+
spec.license = "GPL-3.0"
|
15
|
+
spec.required_ruby_version = ">= 3.1.0"
|
16
|
+
|
17
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
18
|
+
spec.metadata["source_code_uri"] = AppInfo::SOURCE_URL
|
19
|
+
|
20
|
+
spec.files = Dir.chdir(__dir__) do
|
21
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
22
|
+
(File.expand_path(f) == __FILE__) ||
|
23
|
+
f.start_with?(*%w[test/ spec/ features/ samples/ .git .circleci appveyor Gemfile])
|
24
|
+
end
|
25
|
+
end
|
26
|
+
spec.bindir = "bin"
|
27
|
+
spec.executables = [ 'inat-get' ]
|
28
|
+
spec.require_paths = [ "lib" ]
|
29
|
+
|
30
|
+
spec.add_dependency "sqlite3", "~> 1.6.6"
|
31
|
+
spec.add_dependency "tzinfo", "~> 2.0.6"
|
32
|
+
|
33
|
+
end
|
data/lib/extra/enum.rb
ADDED
@@ -0,0 +1,184 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Enum
|
4
|
+
|
5
|
+
class << self
|
6
|
+
|
7
|
+
def inherited sub
|
8
|
+
if self != ::Enum
|
9
|
+
raise TypeError, "Enum classes are closed!", caller
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
private :new
|
14
|
+
|
15
|
+
private def item name, order = nil, description: nil, data: nil
|
16
|
+
raise TypeError, "Name of enum value must be a Symbol!", caller unless Symbol === name
|
17
|
+
raise TypeError, "Order of enum value must be a Integer!", caller unless nil === order || Integer === order
|
18
|
+
raise TypeError, "Description of enum value must be a String!", caller unless nil === description || String === description
|
19
|
+
@values ||= []
|
20
|
+
@by_name ||= {}
|
21
|
+
@by_order ||= {}
|
22
|
+
order ||= (@values.map(&:order).max || 0) + 1
|
23
|
+
value = new name, order, description, data
|
24
|
+
value.freeze
|
25
|
+
@values << value
|
26
|
+
@by_name[name] = value
|
27
|
+
@by_order[order] = value
|
28
|
+
const_name = if name[0] != name[0].upcase
|
29
|
+
name.upcase
|
30
|
+
else
|
31
|
+
name
|
32
|
+
end
|
33
|
+
const_name = const_name.to_s.gsub '-', '_'
|
34
|
+
self.const_set const_name, value
|
35
|
+
return value
|
36
|
+
end
|
37
|
+
|
38
|
+
private def items *names, **names_with_order
|
39
|
+
names.each do |name|
|
40
|
+
item name
|
41
|
+
end
|
42
|
+
names_with_order.each do |name, order|
|
43
|
+
item name, order
|
44
|
+
end
|
45
|
+
return values
|
46
|
+
end
|
47
|
+
|
48
|
+
private def item_alias **params
|
49
|
+
@aliases ||= {}
|
50
|
+
params.each do |name, value|
|
51
|
+
raise TypeError, "Alias name must be a Symbol!", caller unless Symbol === name
|
52
|
+
value = self[value] if Symbol === value
|
53
|
+
raise TypeError, "Alias value must be a Symbol or #{ self.name }!", caller unless self === value
|
54
|
+
@aliases[name] = value
|
55
|
+
@by_name[name] = value
|
56
|
+
const_name = if name[0] != name[0].upcase
|
57
|
+
name.upcase
|
58
|
+
else
|
59
|
+
name
|
60
|
+
end
|
61
|
+
const_name = const_name.to_s.gsub '-', '_'
|
62
|
+
self.const_set const_name, value
|
63
|
+
end
|
64
|
+
return aliases
|
65
|
+
end
|
66
|
+
|
67
|
+
def values
|
68
|
+
@values ||= []
|
69
|
+
@values.sort_by(&:order).dup.freeze
|
70
|
+
end
|
71
|
+
|
72
|
+
def aliases
|
73
|
+
@aliases ||= {}
|
74
|
+
@aliases.dup.freeze
|
75
|
+
end
|
76
|
+
|
77
|
+
def to_a
|
78
|
+
values
|
79
|
+
end
|
80
|
+
|
81
|
+
def to_h
|
82
|
+
@by_name.dup.freeze
|
83
|
+
end
|
84
|
+
|
85
|
+
include Enumerable
|
86
|
+
|
87
|
+
def each
|
88
|
+
if block_given?
|
89
|
+
@values.sort_by(&:order).each do |item|
|
90
|
+
yield item
|
91
|
+
end
|
92
|
+
else
|
93
|
+
to_enum :each
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
private def get name_or_order
|
98
|
+
return nil if name == nil
|
99
|
+
result = @by_name[name_or_order] || @by_order[name_or_order]
|
100
|
+
raise ArgumentError, "Invalid name or order: #{ name_or_order.inspect }!", caller if result == nil
|
101
|
+
return result
|
102
|
+
end
|
103
|
+
|
104
|
+
def [] name_or_order
|
105
|
+
return name_or_order if self === name_or_order
|
106
|
+
case name_or_order
|
107
|
+
when Symbol, Integer
|
108
|
+
@by_name[name_or_order] || @by_order[name_or_order]
|
109
|
+
when Range
|
110
|
+
Range::new get(name_or_order.begin), get(name_or_order.end), name_or_order.exclude_end?
|
111
|
+
else
|
112
|
+
raise TypeError, "Invalid key: #{ name_or_order.inspect }!", caller
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def parse src, case_sensitive: true
|
117
|
+
return src if self === src
|
118
|
+
raise TypeError, "Source must be a String!", caller unless String === src
|
119
|
+
src = src.strip
|
120
|
+
prefix = self.name + '::'
|
121
|
+
src = src[prefix.length ..] if src.start_with?(prefix)
|
122
|
+
return nil if src.empty?
|
123
|
+
key = if case_sensitive
|
124
|
+
@by_name.keys.find { |v| v.to_s == src }
|
125
|
+
else
|
126
|
+
src = src.downcase
|
127
|
+
@by_name.keys.find { |v| v.to_s.downcase == src }
|
128
|
+
end
|
129
|
+
raise NameError, "Invalid name: #{ src.inspect }!", caller if key.nil?
|
130
|
+
@by_name[key]
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
attr_reader :name, :order, :description, :data
|
136
|
+
|
137
|
+
def initialize name, order, description, data
|
138
|
+
@name = name
|
139
|
+
@order = order
|
140
|
+
@description = description
|
141
|
+
@data = data
|
142
|
+
end
|
143
|
+
|
144
|
+
def intern
|
145
|
+
@name
|
146
|
+
end
|
147
|
+
|
148
|
+
def to_s
|
149
|
+
@name.to_s
|
150
|
+
end
|
151
|
+
|
152
|
+
def to_i
|
153
|
+
@order
|
154
|
+
end
|
155
|
+
|
156
|
+
def inspect
|
157
|
+
const_name = @name
|
158
|
+
add = ''
|
159
|
+
if @name[0] != @name[0].upcase
|
160
|
+
const_name = "#{@name.upcase}"
|
161
|
+
add = " = #{ @name.inspect }"
|
162
|
+
end
|
163
|
+
add += " @data=#{ @data.inspect }" if @data
|
164
|
+
add += " @description=#{ @description.inspect }" if @description
|
165
|
+
const_name = const_name.to_s.gsub '-', '_'
|
166
|
+
"\#<#{ self.class.name }::#{ const_name }#{ add }>"
|
167
|
+
end
|
168
|
+
|
169
|
+
include Comparable
|
170
|
+
|
171
|
+
def <=> other
|
172
|
+
return nil if !(self.class === other)
|
173
|
+
@order <=> other.order
|
174
|
+
end
|
175
|
+
|
176
|
+
def succ
|
177
|
+
self.class.values.select { |v| v.order > @order }.first
|
178
|
+
end
|
179
|
+
|
180
|
+
def pred
|
181
|
+
self.class.values.select { |v| v.order < @order }.last
|
182
|
+
end
|
183
|
+
|
184
|
+
end
|
data/lib/extra/period.rb
ADDED
@@ -0,0 +1,252 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'time'
|
4
|
+
require 'date'
|
5
|
+
|
6
|
+
class Period
|
7
|
+
|
8
|
+
attr_reader :value
|
9
|
+
|
10
|
+
def initialize value
|
11
|
+
@value = value
|
12
|
+
end
|
13
|
+
|
14
|
+
class << self
|
15
|
+
|
16
|
+
private :new
|
17
|
+
|
18
|
+
def parse src
|
19
|
+
return nil if src == nil
|
20
|
+
return src if Period === src
|
21
|
+
raise TypeError, "Source (#{ src.inspect }) must be a String!", caller unless String === src
|
22
|
+
src = src.strip
|
23
|
+
value = 0
|
24
|
+
case src
|
25
|
+
when /^\d+$/
|
26
|
+
value = src.to_i
|
27
|
+
when /^(\d+[wW])?(\d+[dD])?(\d+[hH])?(\d+[mM])?(\d+[sS])?$/
|
28
|
+
weeks = /(\d+)[wW]/.match(src)
|
29
|
+
value += weeks[1].to_i * 7 * 24 * 60 * 60 if weeks
|
30
|
+
days = /(\d+)[dD]/.match(src)
|
31
|
+
value += days[1].to_i * 24 * 60 * 60 if days
|
32
|
+
hours = /(\d+)[hH]/.match(src)
|
33
|
+
value += hours[1].to_i * 60 * 60 if hours
|
34
|
+
minutes = /(\d+)[mM]/.match(src)
|
35
|
+
value += minutes[1].to_i * 60 if minutes
|
36
|
+
seconds = /(\d+)[sS]/.match(src)
|
37
|
+
value += seconds[1].to_i if seconds
|
38
|
+
else
|
39
|
+
raise ArgumentError, "Invalid source: #{ src }!", caller
|
40
|
+
end
|
41
|
+
new(value).freeze
|
42
|
+
end
|
43
|
+
|
44
|
+
def make weeks: 0, days: 0, hours: 0, minutes: 0, seconds: 0
|
45
|
+
raise TypeError, "Parameter 'weeks' must be an Integer!", caller unless Integer === weeks
|
46
|
+
raise TypeError, "Parameter 'days' must be an Integer!", caller unless Integer === days
|
47
|
+
raise TypeError, "Parameter 'hours' must be an Integer!", caller unless Integer === hours
|
48
|
+
raise TypeError, "Parameter 'minutes' must be an Integer!", caller unless Integer === minutes
|
49
|
+
raise TypeError, "Parameter 'seconds' must be an Integer!", caller unless Integer === seconds
|
50
|
+
value = seconds
|
51
|
+
value += minutes * 60
|
52
|
+
value += hours * 60 * 60
|
53
|
+
value += days * 24 * 60 * 60
|
54
|
+
value += weeks * 7 * 24 * 60 * 60
|
55
|
+
new(value).freeze
|
56
|
+
end
|
57
|
+
|
58
|
+
def diff first, second
|
59
|
+
first = first.to_time if Date === first
|
60
|
+
second = second.to_time if Date === second
|
61
|
+
raise TypeError, "Parameter 'first' must be a Time or Date!", caller unless Time === first
|
62
|
+
raise TypeError, "Parameter 'second' must be a Time or Date!", caller unless Time === second
|
63
|
+
value = first.to_i - second.to_i
|
64
|
+
new(value).freeze
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
WEEK = make weeks: 1
|
70
|
+
DAY = make days: 1
|
71
|
+
HOUR = make hours: 1
|
72
|
+
MINUTE = make minutes: 1
|
73
|
+
SECOND = make seconds: 1
|
74
|
+
ZERO = make seconds: 0
|
75
|
+
|
76
|
+
def weeks
|
77
|
+
g = sgn
|
78
|
+
w = @value.abs / (7 * 24 * 60 * 60)
|
79
|
+
g * w
|
80
|
+
end
|
81
|
+
|
82
|
+
def days all: true
|
83
|
+
g = sgn
|
84
|
+
d = @value.abs / (24 * 60 * 60)
|
85
|
+
all && g * d || g * (d % 7)
|
86
|
+
end
|
87
|
+
|
88
|
+
def hours all: false
|
89
|
+
g = sgn
|
90
|
+
h = @value.abs / (60 * 60)
|
91
|
+
all && g * h || g * (h % 24)
|
92
|
+
end
|
93
|
+
|
94
|
+
def minutes all: false
|
95
|
+
g = sgn
|
96
|
+
m = @value.abs / 60
|
97
|
+
all && g * m || g * (m % 60)
|
98
|
+
end
|
99
|
+
|
100
|
+
def seconds all: false
|
101
|
+
g = sgn
|
102
|
+
s = @value.abs
|
103
|
+
all && g * s || g * (s % 60)
|
104
|
+
end
|
105
|
+
|
106
|
+
def sgn
|
107
|
+
if @value == 0
|
108
|
+
0
|
109
|
+
elsif @value > 0
|
110
|
+
1
|
111
|
+
else
|
112
|
+
-1
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def abs
|
117
|
+
self.class.make seconds: @value.abs
|
118
|
+
end
|
119
|
+
|
120
|
+
def to_i
|
121
|
+
@value
|
122
|
+
end
|
123
|
+
|
124
|
+
def to_s with_weeks: true
|
125
|
+
return '0' if @value == 0
|
126
|
+
return "-#{abs.to_s}" if @value < 0
|
127
|
+
w = with_weeks && weeks || 0
|
128
|
+
d = days all: !with_weeks
|
129
|
+
h = hours
|
130
|
+
m = minutes
|
131
|
+
s = seconds
|
132
|
+
result = ''
|
133
|
+
result += "#{ w }w" if w != 0
|
134
|
+
result += "#{ d }d" if d != 0
|
135
|
+
result += "#{ h }h" if h != 0
|
136
|
+
result += "#{ m }m" if m != 0
|
137
|
+
result += "#{ s }s" if s != 0
|
138
|
+
result
|
139
|
+
end
|
140
|
+
|
141
|
+
def to_hs
|
142
|
+
return "-#{abs.to_hs}" if @value < 0
|
143
|
+
h = hours all: true
|
144
|
+
m = minutes
|
145
|
+
s = seconds
|
146
|
+
format "%d:%02d:%02d", h, m, s
|
147
|
+
end
|
148
|
+
|
149
|
+
def inspect
|
150
|
+
"\#<Period: #{to_s}>"
|
151
|
+
end
|
152
|
+
|
153
|
+
include Comparable
|
154
|
+
|
155
|
+
def <=> other
|
156
|
+
return nil if other.nil?
|
157
|
+
@value <=> other.value
|
158
|
+
end
|
159
|
+
|
160
|
+
def +@
|
161
|
+
self
|
162
|
+
end
|
163
|
+
|
164
|
+
def -@
|
165
|
+
self.class.make(seconds: -@value)
|
166
|
+
end
|
167
|
+
|
168
|
+
def + other
|
169
|
+
raise TypeError, "Second argument must be a Period!", caller unless Period === other
|
170
|
+
self.class.make(seconds: @value + other.value)
|
171
|
+
end
|
172
|
+
|
173
|
+
def - other
|
174
|
+
raise TypeError, "Second argument must be a Period!", caller unless Period === other
|
175
|
+
self.class.make(seconds: @value - other.value)
|
176
|
+
end
|
177
|
+
|
178
|
+
def * num
|
179
|
+
raise TypeError, "Second argument must be a number!", caller unless Numeric === num
|
180
|
+
self.class.make(seconds: (@value * num).to_i)
|
181
|
+
end
|
182
|
+
|
183
|
+
def / num
|
184
|
+
raise TypeError, "Second argument must be a number!", caller unless Numeric === num
|
185
|
+
self.class.make(seconds: (@value / num).to_i)
|
186
|
+
end
|
187
|
+
|
188
|
+
class Num
|
189
|
+
|
190
|
+
attr_reader :value
|
191
|
+
|
192
|
+
def initialize num
|
193
|
+
@value = num
|
194
|
+
end
|
195
|
+
|
196
|
+
def * period
|
197
|
+
period * @value
|
198
|
+
end
|
199
|
+
|
200
|
+
end
|
201
|
+
|
202
|
+
def coerce num
|
203
|
+
case num
|
204
|
+
when Integer, Float, Rational
|
205
|
+
[ Period::Num::new(num), self ]
|
206
|
+
else
|
207
|
+
raise TypeError, "Cannot coerce Period to a #{ num.class }!", caller
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
end
|
212
|
+
|
213
|
+
class Time
|
214
|
+
|
215
|
+
alias :preperiod_plus :+
|
216
|
+
alias :preperiod_minus :-
|
217
|
+
|
218
|
+
def + arg
|
219
|
+
return self + arg.value if Period === arg
|
220
|
+
preperiod_plus arg
|
221
|
+
end
|
222
|
+
|
223
|
+
def - arg
|
224
|
+
return self + (-arg.value) if Period === arg
|
225
|
+
preperiod_minus arg
|
226
|
+
end
|
227
|
+
|
228
|
+
end
|
229
|
+
|
230
|
+
class Numeric
|
231
|
+
|
232
|
+
def weeks
|
233
|
+
Period::make(weeks: 1) * self
|
234
|
+
end
|
235
|
+
|
236
|
+
def days
|
237
|
+
Period::make(days: 1) * self
|
238
|
+
end
|
239
|
+
|
240
|
+
def hours
|
241
|
+
Period::make(hours: 1) * self
|
242
|
+
end
|
243
|
+
|
244
|
+
def minutes
|
245
|
+
Period::make(minutes: 1) * self
|
246
|
+
end
|
247
|
+
|
248
|
+
def seconds
|
249
|
+
Period::make(seconds: 1) * self
|
250
|
+
end
|
251
|
+
|
252
|
+
end
|
data/lib/extra/uuid.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
class UUID
|
6
|
+
|
7
|
+
BASE = 16
|
8
|
+
BYTES = 16
|
9
|
+
DIGITS = 32
|
10
|
+
|
11
|
+
attr_reader :value
|
12
|
+
|
13
|
+
def initialize value
|
14
|
+
@value = value
|
15
|
+
end
|
16
|
+
|
17
|
+
class << self
|
18
|
+
|
19
|
+
private :new
|
20
|
+
|
21
|
+
def parse src
|
22
|
+
return nil if src == nil
|
23
|
+
raise TypeError, "Source must be a String!", caller unless String === src
|
24
|
+
src = src.strip
|
25
|
+
raise ArgumentError, "Invalid UUID source: #{ src.inspect }!", caller unless /^\h{8}\-\h{4}\-\h{4}\-\h{4}\-\h{12}$/ === src || /^\h{32}$/ === src
|
26
|
+
value = src.gsub('-', '').to_i(BASE)
|
27
|
+
new(value).freeze
|
28
|
+
end
|
29
|
+
|
30
|
+
def generate count = 1, prefix: nil, prefix_length: nil
|
31
|
+
if prefix == nil
|
32
|
+
if prefix_length != nil
|
33
|
+
raise ArgumentError, "Prefix length must be Integer < #{ BYTES }!", caller unless Integer === prefix_length && prefix_length < BYTES
|
34
|
+
prefix = SecureRandom.hex prefix_length
|
35
|
+
else
|
36
|
+
prefix = ''
|
37
|
+
prefix_length = 0
|
38
|
+
end
|
39
|
+
else
|
40
|
+
raise TypeError, "Prefix must be a String!", caller unless String === prefix
|
41
|
+
prefix = prefix.gsub('-', '')
|
42
|
+
prefix_length = prefix.length / 2
|
43
|
+
end
|
44
|
+
result = count.times.map { parse(prefix + SecureRandom.hex(BYTES - prefix_length)) }
|
45
|
+
if count == 1
|
46
|
+
result[0]
|
47
|
+
else
|
48
|
+
result
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def make *args
|
53
|
+
raise ArgumentError, "Method receives 1..5 arguments!", caller unless (1..5) === args.size
|
54
|
+
raise ArgumentError, "Arguments must be Integers!", caller unless args.all? { |a| Integer === a }
|
55
|
+
last = args.pop
|
56
|
+
value = 0
|
57
|
+
value += args[0] << 12 * 8 if args.size > 0
|
58
|
+
value += args[1] << 10 * 8 if args.size > 1
|
59
|
+
value += args[2] << 8 * 8 if args.size > 2
|
60
|
+
value += args[3] << 6 * 8 if args.size > 3
|
61
|
+
value += last
|
62
|
+
new(value).freeze
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
ZERO = make 0
|
68
|
+
|
69
|
+
def to_i
|
70
|
+
@value
|
71
|
+
end
|
72
|
+
|
73
|
+
def to_s
|
74
|
+
raw = @value.to_s(BASE)
|
75
|
+
raw = '0' * (DIGITS - raw.length) + raw if raw.length < DIGITS
|
76
|
+
"#{ raw[0..7] }-#{ raw[8..11] }-#{ raw[12..15] }-#{ raw[16..19] }-#{ raw[20..31] }"
|
77
|
+
end
|
78
|
+
|
79
|
+
def inspect
|
80
|
+
to_s
|
81
|
+
end
|
82
|
+
|
83
|
+
include Comparable
|
84
|
+
|
85
|
+
def <=> other
|
86
|
+
return nil unless UUID === other
|
87
|
+
@value <=> other.value
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'config'
|
4
|
+
require_relative 'logging'
|
5
|
+
require_relative 'preamble'
|
6
|
+
require_relative 'globals'
|
7
|
+
require_relative 'task'
|
8
|
+
|
9
|
+
class Application
|
10
|
+
|
11
|
+
include AppPreamble
|
12
|
+
|
13
|
+
def logger
|
14
|
+
G.logger
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
setup!
|
19
|
+
G.config = @config.freeze
|
20
|
+
G.logger = DualLogger::new self
|
21
|
+
end
|
22
|
+
|
23
|
+
private def tasks!
|
24
|
+
@tasks = Queue::new
|
25
|
+
@files.each do |file|
|
26
|
+
@tasks.push Task::new(self, file)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private def start!
|
31
|
+
@threads = []
|
32
|
+
count = [ @tasks.size, G.config[:threads][:tasks] ].min
|
33
|
+
count.times do
|
34
|
+
@threads << Thread.start do
|
35
|
+
while !@tasks.empty?
|
36
|
+
task = @tasks.pop
|
37
|
+
task.execute
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
@threads.each { |th| th.join }
|
42
|
+
end
|
43
|
+
|
44
|
+
def run
|
45
|
+
preamble!
|
46
|
+
tasks!
|
47
|
+
start!
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|