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