solis 0.64.0
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/.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
|