solis 0.64.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.travis.yml +6 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +287 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/examples/after_hooks.rb +24 -0
- data/examples/config.yml.template +15 -0
- data/examples/read_from_shacl.rb +22 -0
- data/examples/read_from_shacl_abv.rb +84 -0
- data/examples/read_from_sheet.rb +22 -0
- data/lib/solis/config_file.rb +91 -0
- data/lib/solis/error/cursor_error.rb +6 -0
- data/lib/solis/error/general_error.rb +6 -0
- data/lib/solis/error/invalid_attribute_error.rb +6 -0
- data/lib/solis/error/invalid_datatype_error.rb +6 -0
- data/lib/solis/error/not_found_error.rb +6 -0
- data/lib/solis/error/query_error.rb +6 -0
- data/lib/solis/error.rb +3 -0
- data/lib/solis/graph.rb +360 -0
- data/lib/solis/model.rb +565 -0
- data/lib/solis/options.rb +19 -0
- data/lib/solis/query/construct.rb +93 -0
- data/lib/solis/query/filter.rb +133 -0
- data/lib/solis/query/run.rb +97 -0
- data/lib/solis/query.rb +347 -0
- data/lib/solis/resource.rb +37 -0
- data/lib/solis/shape/data_types.rb +280 -0
- data/lib/solis/shape/reader/csv.rb +12 -0
- data/lib/solis/shape/reader/file.rb +16 -0
- data/lib/solis/shape/reader/sheet.rb +777 -0
- data/lib/solis/shape/reader/simple_sheets/sheet.rb +59 -0
- data/lib/solis/shape/reader/simple_sheets/worksheet.rb +173 -0
- data/lib/solis/shape/reader/simple_sheets.rb +40 -0
- data/lib/solis/shape.rb +189 -0
- data/lib/solis/sparql_adaptor.rb +318 -0
- data/lib/solis/store/sparql/client/query.rb +35 -0
- data/lib/solis/store/sparql/client.rb +41 -0
- data/lib/solis/version.rb +3 -0
- data/lib/solis.rb +13 -0
- data/solis.gemspec +50 -0
- metadata +304 -0
@@ -0,0 +1,59 @@
|
|
1
|
+
class Sheet
|
2
|
+
include Enumerable
|
3
|
+
def initialize(worksheet)
|
4
|
+
@worksheet = worksheet
|
5
|
+
@last_sync = Time.now
|
6
|
+
end
|
7
|
+
|
8
|
+
def title
|
9
|
+
@worksheet.title
|
10
|
+
end
|
11
|
+
|
12
|
+
def each
|
13
|
+
@worksheet.rows(skip=1).each do |row|
|
14
|
+
yield make_record_from_row(row)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def header
|
19
|
+
@header ||= @worksheet.rows[0].map{|m| m.downcase.gsub(' ','_').gsub('#', '_hash').gsub('%','_percent')}
|
20
|
+
end
|
21
|
+
|
22
|
+
def [](index)
|
23
|
+
row_as_record(@worksheet.rows(skip=0)[index])
|
24
|
+
end
|
25
|
+
|
26
|
+
def count
|
27
|
+
@worksheet.num_rows - 1
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
def sync
|
32
|
+
if (Time.now.to_i - @last_sync.to_i) > 600
|
33
|
+
@worksheet.synchronize
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def row_as_record(row)
|
38
|
+
sync
|
39
|
+
result = []
|
40
|
+
return result if row.nil?
|
41
|
+
if row && row.is_a?(Array) && row.first.is_a?(Array)
|
42
|
+
result = row.map do |m|
|
43
|
+
m.is_a?(Array) ? make_record_from_row(m) : m
|
44
|
+
end
|
45
|
+
else
|
46
|
+
result = make_record_from_row(row)
|
47
|
+
end
|
48
|
+
result
|
49
|
+
end
|
50
|
+
|
51
|
+
def make_record_from_row(row)
|
52
|
+
return nil if row.nil?
|
53
|
+
result = {}
|
54
|
+
row.each_index do |i|
|
55
|
+
result.store(self.header[i], row[i])
|
56
|
+
end
|
57
|
+
result
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
class Worksheet
|
2
|
+
attr_reader :properties, :title, :spreadsheet
|
3
|
+
|
4
|
+
def initialize(spreadsheet, properties)
|
5
|
+
@spreadsheet = spreadsheet
|
6
|
+
set_properties(properties)
|
7
|
+
@cells = nil
|
8
|
+
@input_values = nil
|
9
|
+
@numeric_values = nil
|
10
|
+
@modified = Set.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def sheet_id
|
14
|
+
@properties.sheet_id
|
15
|
+
end
|
16
|
+
|
17
|
+
def gid
|
18
|
+
sheet_id.to_s
|
19
|
+
end
|
20
|
+
|
21
|
+
def [](*args)
|
22
|
+
(row, col) = parse_cell_args(args)
|
23
|
+
cells[[row, col]] || ''
|
24
|
+
end
|
25
|
+
|
26
|
+
def rows(skip = 0)
|
27
|
+
nc = num_cols
|
28
|
+
result = ((1 + skip)..num_rows).map do |row|
|
29
|
+
(1..nc).map { |col| self[row, col] }.freeze
|
30
|
+
end
|
31
|
+
result.freeze
|
32
|
+
end
|
33
|
+
|
34
|
+
def num_rows
|
35
|
+
reload_cells unless @cells
|
36
|
+
# Memoizes it because this can be bottle-neck.
|
37
|
+
# https://github.com/gimite/google-drive-ruby/pull/49
|
38
|
+
@num_rows ||=
|
39
|
+
@input_values
|
40
|
+
.reject { |(_r, _c), v| v.empty? }
|
41
|
+
.map { |(r, _c), _v| r }
|
42
|
+
.max ||
|
43
|
+
0
|
44
|
+
end
|
45
|
+
|
46
|
+
# Column number of the right-most non-empty column.
|
47
|
+
def num_cols
|
48
|
+
reload_cells unless @cells
|
49
|
+
# Memoizes it because this can be bottle-neck.
|
50
|
+
# https://github.com/gimite/google-drive-ruby/pull/49
|
51
|
+
@num_cols ||=
|
52
|
+
@input_values
|
53
|
+
.reject { |(_r, _c), v| v.empty? }
|
54
|
+
.map { |(_r, c), _v| c }
|
55
|
+
.max ||
|
56
|
+
0
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
def parse_cell_args(args)
|
61
|
+
if args.size == 1 && args[0].is_a?(String)
|
62
|
+
cell_name_to_row_col(args[0])
|
63
|
+
elsif args.size == 2 && args[0].is_a?(Integer) && args[1].is_a?(Integer)
|
64
|
+
if args[0] >= 1 && args[1] >= 1
|
65
|
+
args
|
66
|
+
else
|
67
|
+
raise(
|
68
|
+
ArgumentError,
|
69
|
+
format(
|
70
|
+
'Row/col must be >= 1 (1-origin), but are %d/%d',
|
71
|
+
args[0], args[1]
|
72
|
+
)
|
73
|
+
)
|
74
|
+
end
|
75
|
+
else
|
76
|
+
raise(
|
77
|
+
ArgumentError,
|
78
|
+
format(
|
79
|
+
"Arguments must be either one String or two Integer's, but are %p",
|
80
|
+
args
|
81
|
+
)
|
82
|
+
)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def cell_name_to_row_col(cell_name)
|
87
|
+
unless cell_name.is_a?(String)
|
88
|
+
raise(
|
89
|
+
ArgumentError, format('Cell name must be a string: %p', cell_name)
|
90
|
+
)
|
91
|
+
end
|
92
|
+
unless cell_name.upcase =~ /^([A-Z]+)(\d+)$/
|
93
|
+
raise(
|
94
|
+
ArgumentError,
|
95
|
+
format(
|
96
|
+
'Cell name must be only letters followed by digits with no ' \
|
97
|
+
'spaces in between: %p',
|
98
|
+
cell_name
|
99
|
+
)
|
100
|
+
)
|
101
|
+
end
|
102
|
+
col = 0
|
103
|
+
Regexp.last_match(1).each_byte do |b|
|
104
|
+
# 0x41: "A"
|
105
|
+
col = col * 26 + (b - 0x41 + 1)
|
106
|
+
end
|
107
|
+
row = Regexp.last_match(2).to_i
|
108
|
+
[row, col]
|
109
|
+
end
|
110
|
+
|
111
|
+
def cells
|
112
|
+
reload_cells unless @cells
|
113
|
+
@cells
|
114
|
+
end
|
115
|
+
|
116
|
+
def set_properties(properties)
|
117
|
+
@properties = properties
|
118
|
+
@title = @remote_title = properties.title
|
119
|
+
@index = properties.index
|
120
|
+
if properties.grid_properties.nil?
|
121
|
+
@max_rows = @max_cols = 0
|
122
|
+
else
|
123
|
+
@max_rows = properties.grid_properties.row_count
|
124
|
+
@max_cols = properties.grid_properties.column_count
|
125
|
+
end
|
126
|
+
@meta_modified = false
|
127
|
+
end
|
128
|
+
|
129
|
+
def reload_cells
|
130
|
+
response =
|
131
|
+
@spreadsheet.sheets_service.get_spreadsheet(
|
132
|
+
@spreadsheet.id,
|
133
|
+
ranges: "'%s'" % @remote_title,
|
134
|
+
fields: 'sheets.data.rowData.values(formattedValue,userEnteredValue,effectiveValue)'
|
135
|
+
)
|
136
|
+
update_cells_from_api_sheet(response.sheets[0])
|
137
|
+
end
|
138
|
+
|
139
|
+
def update_cells_from_api_sheet(api_sheet)
|
140
|
+
rows_data = api_sheet.data[0].row_data || []
|
141
|
+
|
142
|
+
@num_rows = rows_data.size
|
143
|
+
@num_cols = 0
|
144
|
+
@cells = {}
|
145
|
+
@input_values = {}
|
146
|
+
@numeric_values = {}
|
147
|
+
|
148
|
+
rows_data.each_with_index do |row_data, r|
|
149
|
+
next if !row_data.values
|
150
|
+
@num_cols = row_data.values.size if row_data.values.size > @num_cols
|
151
|
+
row_data.values.each_with_index do |cell_data, c|
|
152
|
+
k = [r + 1, c + 1]
|
153
|
+
@cells[k] = cell_data.formatted_value || ''
|
154
|
+
@input_values[k] = extended_value_to_str(cell_data.user_entered_value)
|
155
|
+
@numeric_values[k] =
|
156
|
+
cell_data.effective_value && cell_data.effective_value.number_value ?
|
157
|
+
cell_data.effective_value.number_value.to_f : nil
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
@modified.clear
|
162
|
+
end
|
163
|
+
|
164
|
+
def extended_value_to_str(extended_value)
|
165
|
+
return '' if !extended_value
|
166
|
+
value =
|
167
|
+
extended_value.number_value ||
|
168
|
+
extended_value.string_value ||
|
169
|
+
extended_value.bool_value ||
|
170
|
+
extended_value.formula_value
|
171
|
+
value.to_s
|
172
|
+
end
|
173
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'google/apis/sheets_v4'
|
2
|
+
require_relative 'simple_sheets/sheet'
|
3
|
+
require_relative 'simple_sheets/worksheet'
|
4
|
+
|
5
|
+
class SimpleSheets
|
6
|
+
attr_accessor :key
|
7
|
+
attr_reader :sheets_service, :id
|
8
|
+
|
9
|
+
def initialize(key, spreadsheet_id)
|
10
|
+
@id = spreadsheet_id
|
11
|
+
@key = key
|
12
|
+
@sheets_service = Google::Apis::SheetsV4::SheetsService.new
|
13
|
+
@sheets_service.key = @key
|
14
|
+
end
|
15
|
+
|
16
|
+
def worksheets
|
17
|
+
spreadsheet_api = @sheets_service.get_spreadsheet(@id, fields: 'sheets.properties')
|
18
|
+
spreadsheet_api.sheets.map { |s| Worksheet.new(self, s.properties) }
|
19
|
+
#TODO: catch not found
|
20
|
+
rescue Google::Apis::ClientError => e
|
21
|
+
case e.status_code
|
22
|
+
when 404
|
23
|
+
raise "Sheet with id #{@id} NOT FOUND"
|
24
|
+
else
|
25
|
+
raise "An error occured reading sheet with id #{@id}. HTTP status code = #{e.status_code}, reason = '#{e.header.reason_phrase}'"
|
26
|
+
end
|
27
|
+
rescue Exception => e
|
28
|
+
raise e
|
29
|
+
end
|
30
|
+
|
31
|
+
def worksheet_by_title(title)
|
32
|
+
worksheets.find { |ws| ws.title == title }
|
33
|
+
end
|
34
|
+
|
35
|
+
def worksheet_by_sheet_id(sheet_id)
|
36
|
+
sheet_id = sheet_id.to_i
|
37
|
+
worksheets.find { |ws| ws.sheet_id == sheet_id }
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
data/lib/solis/shape.rb
ADDED
@@ -0,0 +1,189 @@
|
|
1
|
+
require_relative 'shape/reader/file'
|
2
|
+
require_relative 'shape/reader/sheet'
|
3
|
+
require_relative 'shape/data_types'
|
4
|
+
|
5
|
+
module Solis
|
6
|
+
module Shape
|
7
|
+
def self.from_graph(graph)
|
8
|
+
class << self
|
9
|
+
def parse_graph(graph)
|
10
|
+
shapes = {}
|
11
|
+
#puts query.execute(graph).to_csv
|
12
|
+
|
13
|
+
query.execute(graph) do |solution|
|
14
|
+
parse_solution(shapes, solution)
|
15
|
+
end
|
16
|
+
|
17
|
+
# shapes = add_missing_attributes(shapes)
|
18
|
+
shapes
|
19
|
+
rescue Solis::Error::GeneralError => e
|
20
|
+
raise "Unable to parse shapes: #{e.message}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def lookup_datatype(datatype, node)
|
24
|
+
if datatype =~ /^http:\/\/www.w3.org\/2001\/XMLSchema#/
|
25
|
+
case datatype
|
26
|
+
when /^http:\/\/www.w3.org\/2001\/XMLSchema#anyURI/
|
27
|
+
:string
|
28
|
+
when /http:\/\/www.w3.org\/2001\/XMLSchema#duration/
|
29
|
+
:duration
|
30
|
+
when /http:\/\/www.w3.org\/2001\/XMLSchema#integer/
|
31
|
+
:integer
|
32
|
+
when /http:\/\/www.w3.org\/2001\/XMLSchema#int/
|
33
|
+
:integer
|
34
|
+
when /http:\/\/www.w3.org\/2001\/XMLSchema#dateTime/
|
35
|
+
:datetime
|
36
|
+
when /http:\/\/www.w3.org\/2001\/XMLSchema#date/
|
37
|
+
:date
|
38
|
+
when /http:\/\/www.w3.org\/2001\/XMLSchema#time/
|
39
|
+
:time
|
40
|
+
when /http:\/\/www.w3.org\/2001\/XMLSchema#gYear/
|
41
|
+
:year
|
42
|
+
when /http:\/\/www.w3.org\/2001\/XMLSchema#string/
|
43
|
+
:string
|
44
|
+
when /http:\/\/www.w3.org\/2001\/XMLSchema#boolean/
|
45
|
+
:boolean
|
46
|
+
when /http:\/\/www.w3.org\/2001\/XMLSchema#float/
|
47
|
+
:float
|
48
|
+
when /http:\/\/www.w3.org\/2001\/XMLSchema#double/
|
49
|
+
:double
|
50
|
+
else
|
51
|
+
#puts datatype.split('#').last.to_sym
|
52
|
+
:string
|
53
|
+
end
|
54
|
+
elsif datatype =~ /http:\/\/schema.org\//
|
55
|
+
case datatype
|
56
|
+
when /http:\/\/schema.org\/temporalCoverage/
|
57
|
+
:temporal_coverage
|
58
|
+
else
|
59
|
+
#puts datatype.split('#').last.to_sym
|
60
|
+
:string
|
61
|
+
end
|
62
|
+
elsif datatype =~ /http:\/\/www.w3.org\/2006\/time/
|
63
|
+
case datatype
|
64
|
+
when /http:\/\/www.w3.org\/2006\/time#DateTimeInterval/
|
65
|
+
:datetime_interval
|
66
|
+
else
|
67
|
+
#puts datatype.split('#').last.to_sym
|
68
|
+
:string
|
69
|
+
end
|
70
|
+
elsif datatype.nil? && node.is_a?(RDF::URI)
|
71
|
+
node.value.split('/').last.gsub(/Shape$/, '').to_sym
|
72
|
+
elsif datatype =~ /^http:\/\/www.w3.org\/1999\/02\/22-rdf-syntax-ns/
|
73
|
+
case datatype
|
74
|
+
when /http:\/\/www.w3.org\/1999\/02\/22-rdf-syntax-ns#langString/
|
75
|
+
:lang_string
|
76
|
+
when /http:\/\/www.w3.org\/1999\/02\/22-rdf-syntax-ns#JSON/
|
77
|
+
:json
|
78
|
+
else
|
79
|
+
:string
|
80
|
+
end
|
81
|
+
else
|
82
|
+
:string
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def parse_solution(shapes, solution)
|
87
|
+
shape_rdf = solution.targetClass
|
88
|
+
shape_node = solution.targetNode if solution.bound?(:targetNode)
|
89
|
+
shape_name = solution.className.value
|
90
|
+
attribute_name = solution.attributeName.value if solution.bound?(:attributeName)
|
91
|
+
comment = solution.comment.value if solution.bound?(:comment)
|
92
|
+
attribute_rdf = solution.attributePath.value if solution.bound?(:attributePath)
|
93
|
+
attribute_datatype_rdf = solution.attributeDatatype.value if solution.bound?(:attributeDatatype)
|
94
|
+
attribute_min_count = solution.bound?(:attributeMinCount) ? solution.attributeMinCount.value.to_i : 0
|
95
|
+
attribute_max_count = solution.bound?(:attributeMaxCount) ? solution.attributeMaxCount.value.to_i : nil
|
96
|
+
attribute_node_kind = solution.attributeNodeKind if solution.bound?(:attributeNodeKind)
|
97
|
+
attribute_node = solution.attributeNode if solution.bound?(:attributeNode)
|
98
|
+
attribute_class = solution.attributeClass if solution.bound?(:attributeClass)
|
99
|
+
attribute_comment = solution.attributeComment if solution.bound?(:attributeComment)
|
100
|
+
# if solution.bound?(:attributeOr)
|
101
|
+
# pp solution
|
102
|
+
# end
|
103
|
+
|
104
|
+
attribute_node = attribute_class if attribute_name && attribute_datatype_rdf.nil? && attribute_node.nil?
|
105
|
+
|
106
|
+
shape = shapes.key?(shape_name) ? shapes[shape_name] : { target_class: nil, target_node: nil, attributes: {} }
|
107
|
+
shape[:target_class] = shape_rdf
|
108
|
+
shape[:target_node] = shape_node
|
109
|
+
shape[:comment] = comment
|
110
|
+
|
111
|
+
shape[:attributes][attribute_name] = {
|
112
|
+
path: attribute_rdf,
|
113
|
+
datatype_rdf: attribute_datatype_rdf,
|
114
|
+
datatype: lookup_datatype(attribute_datatype_rdf, attribute_node),
|
115
|
+
mincount: attribute_min_count,
|
116
|
+
maxcount: attribute_max_count,
|
117
|
+
node: attribute_node,
|
118
|
+
node_kind: attribute_node_kind,
|
119
|
+
class: attribute_class,
|
120
|
+
comment: attribute_comment
|
121
|
+
}
|
122
|
+
|
123
|
+
shape[:attributes].delete_if { |k, _| k.nil? }
|
124
|
+
shapes[shape_name] = shape
|
125
|
+
end
|
126
|
+
|
127
|
+
def query
|
128
|
+
SPARQL.parse %(
|
129
|
+
PREFIX sh: <http://www.w3.org/ns/shacl#>
|
130
|
+
PREFIX rdfv: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
|
131
|
+
|
132
|
+
SELECT ?targetClass ?targetNode ?comment ?className ?attributePath ?attributeName ?attributeDatatype
|
133
|
+
?attributeMinCount ?attributeMaxCount ?attributeOr ?attributeClass
|
134
|
+
?attributeNode ?attributeNodeKind ?attributeComment ?o
|
135
|
+
WHERE {
|
136
|
+
|
137
|
+
?s a sh:NodeShape;
|
138
|
+
sh:targetClass ?targetClass ;
|
139
|
+
sh:node ?targetNode ;
|
140
|
+
sh:description ?comment ;
|
141
|
+
sh:name ?className .
|
142
|
+
OPTIONAL{ ?s sh:property ?attributes .
|
143
|
+
?attributes sh:name ?attributeName ;
|
144
|
+
sh:path ?attributePath ;
|
145
|
+
OPTIONAL{ ?attributes sh:datatype ?attributeDatatype } .
|
146
|
+
OPTIONAL{ ?attributes sh:minCount ?attributeMinCount } .
|
147
|
+
OPTIONAL{ ?attributes sh:maxCount ?attributeMaxCount } .
|
148
|
+
OPTIONAL{ ?attributes sh:or ?attributeOr } .
|
149
|
+
OPTIONAL{ ?attributes sh:class ?attributeClass } .
|
150
|
+
OPTIONAL{ ?attributes sh:nodeKind ?attributeNodeKind } .
|
151
|
+
OPTIONAL{ ?attributes sh:node ?attributeNode } .
|
152
|
+
OPTIONAL{ ?attributes sh:description ?attributeComment } .
|
153
|
+
}.
|
154
|
+
}
|
155
|
+
)
|
156
|
+
end
|
157
|
+
|
158
|
+
def add_missing_attributes(shapes)
|
159
|
+
shapes.each do |shape|
|
160
|
+
if shape[1].is_a?(Hash)
|
161
|
+
graph_name = shape[1][:target_class].value.split(shape[1][:target_class].path).first
|
162
|
+
attributes = shape[1][:attributes]
|
163
|
+
unless attributes.key?('id')
|
164
|
+
if shape[1][:target_class] == shape[1][:target_node]
|
165
|
+
attributes['id'] = {
|
166
|
+
"path": "#{graph_name}/id",
|
167
|
+
"datatype_rdf": "http://www.w3.org/2001/XMLSchema#string",
|
168
|
+
"datatype": "string",
|
169
|
+
"mincount": 1,
|
170
|
+
"maxcount": 1,
|
171
|
+
"node": nil,
|
172
|
+
"node_kind": nil,
|
173
|
+
"class": nil,
|
174
|
+
"comment": RDF::Literal.new("UUID", datatype: RDF::XSD.string)
|
175
|
+
}
|
176
|
+
|
177
|
+
shape[1][:attributes] = attributes
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
shapes
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
parse_graph(graph)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|