gooddata 0.2.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.
- data/LICENSE +22 -0
- data/README.rdoc +97 -0
- data/VERSION +1 -0
- data/bin/gooddata +13 -0
- data/bin/igd.rb +33 -0
- data/lib/gooddata.rb +3 -0
- data/lib/gooddata/client.rb +196 -0
- data/lib/gooddata/command.rb +75 -0
- data/lib/gooddata/commands/api.rb +37 -0
- data/lib/gooddata/commands/auth.rb +74 -0
- data/lib/gooddata/commands/base.rb +80 -0
- data/lib/gooddata/commands/datasets.rb +228 -0
- data/lib/gooddata/commands/help.rb +104 -0
- data/lib/gooddata/commands/profile.rb +9 -0
- data/lib/gooddata/commands/projects.rb +51 -0
- data/lib/gooddata/commands/version.rb +7 -0
- data/lib/gooddata/connection.rb +220 -0
- data/lib/gooddata/extract.rb +13 -0
- data/lib/gooddata/helpers.rb +18 -0
- data/lib/gooddata/model.rb +558 -0
- data/lib/gooddata/models/links.rb +42 -0
- data/lib/gooddata/models/metadata.rb +82 -0
- data/lib/gooddata/models/profile.rb +32 -0
- data/lib/gooddata/models/project.rb +136 -0
- data/lib/gooddata/version.rb +3 -0
- data/test/helper.rb +10 -0
- data/test/test_commands.rb +85 -0
- data/test/test_guessing.rb +46 -0
- data/test/test_model.rb +63 -0
- data/test/test_rest_api_basic.rb +41 -0
- data/test/test_upload.rb +52 -0
- metadata +202 -0
@@ -0,0 +1,42 @@
|
|
1
|
+
module GoodData
|
2
|
+
class Links
|
3
|
+
attr_reader :data
|
4
|
+
|
5
|
+
def initialize(items)
|
6
|
+
@data = {}
|
7
|
+
items.values[0]['links'].each do |item|
|
8
|
+
category = item['category']
|
9
|
+
if @data[category] then
|
10
|
+
if @data[category]['category'] == category then
|
11
|
+
@data[category] = { @data[category]['identifier'] => @data[category] }
|
12
|
+
end
|
13
|
+
@data[category][item['identifier']] = item
|
14
|
+
else
|
15
|
+
@data[category] = item
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def links(category, identifier = nil)
|
21
|
+
return Links.new(GoodData.get(self[category])) unless identifier
|
22
|
+
Links.new GoodData.get(get(category, identifier))
|
23
|
+
end
|
24
|
+
|
25
|
+
def [](category)
|
26
|
+
return @data[category]['link'] if @data[category] && @data[category]['link']
|
27
|
+
@data[category]
|
28
|
+
end
|
29
|
+
|
30
|
+
def is_unique?(category)
|
31
|
+
@data[category]['link'].is_a? String
|
32
|
+
end
|
33
|
+
|
34
|
+
def is_ambiguous?(category)
|
35
|
+
!is_unique?(category)
|
36
|
+
end
|
37
|
+
|
38
|
+
def get(category, identifier)
|
39
|
+
self[category][identifier]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'gooddata/model'
|
2
|
+
|
3
|
+
module GoodData
|
4
|
+
class MdObject
|
5
|
+
MD_OBJ_CTG = 'obj'
|
6
|
+
IDENTIFIERS_CFG = 'instance-identifiers'
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def [](id)
|
10
|
+
raise "Cannot search for nil #{self.class}" unless id
|
11
|
+
if id.is_a? Integer or id =~ /^\d+$/
|
12
|
+
uri = "#{GoodData.project.md.link(MD_OBJ_CTG)}/#{id}"
|
13
|
+
elsif id !~ /\//
|
14
|
+
uri = identifier_to_uri id
|
15
|
+
elsif id =~ /^\//
|
16
|
+
uri = id
|
17
|
+
else
|
18
|
+
raise "Unexpected object id format: expected numeric ID, identifier with no slashes or an URI starting with a slash"
|
19
|
+
end
|
20
|
+
self.new((GoodData.get uri).values[0])
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def identifier_to_uri(id)
|
26
|
+
raise NoProjectError.new "Connect to a project before searching for an object" unless GoodData.project
|
27
|
+
uri = GoodData.project.md[IDENTIFIERS_CFG]
|
28
|
+
response = GoodData.post uri, { 'identifierToUri' => [id ] }
|
29
|
+
response['identifiers'][0]['uri']
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def initialize(json)
|
34
|
+
@json = json
|
35
|
+
end
|
36
|
+
|
37
|
+
def delete
|
38
|
+
raise "Project '#{title}' with id #{uri} is already deleted" if state == :deleted
|
39
|
+
GoodData.delete @json['links']['self']
|
40
|
+
end
|
41
|
+
|
42
|
+
def uri
|
43
|
+
meta['uri']
|
44
|
+
end
|
45
|
+
|
46
|
+
def identifier
|
47
|
+
meta['identifier']
|
48
|
+
end
|
49
|
+
|
50
|
+
def title
|
51
|
+
meta['title']
|
52
|
+
end
|
53
|
+
|
54
|
+
def meta
|
55
|
+
@json['meta']
|
56
|
+
end
|
57
|
+
|
58
|
+
def content
|
59
|
+
@json['content']
|
60
|
+
end
|
61
|
+
|
62
|
+
def project
|
63
|
+
@project ||= Project[uri.gsub(/\/obj\/\d+$/, '')]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
class DataSet < MdObject
|
68
|
+
SLI_CTG = 'singleloadinterface'
|
69
|
+
DS_SLI_CTG = 'dataset-singleloadinterface'
|
70
|
+
|
71
|
+
def sli_enabled?
|
72
|
+
content['mode'] == 'SLI'
|
73
|
+
end
|
74
|
+
|
75
|
+
def sli
|
76
|
+
raise NoProjectError.new "Connect to a project before searching for an object" unless GoodData.project
|
77
|
+
slis = GoodData.project.md.links(Model::LDM_CTG).links(SLI_CTG)[DS_SLI_CTG]
|
78
|
+
uri = slis[identifier]['link']
|
79
|
+
MdObject[uri]
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module GoodData
|
2
|
+
class Profile
|
3
|
+
private_class_method :new
|
4
|
+
attr_reader :user
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def load
|
8
|
+
# GoodData.logger.info "Loading user profile..."
|
9
|
+
Profile.send 'new'
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def projects
|
14
|
+
@json['accountSetting']['links']['projects']
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_json
|
18
|
+
@json
|
19
|
+
end
|
20
|
+
|
21
|
+
def [](key)
|
22
|
+
@json['accountSetting'][key]
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def initialize
|
28
|
+
@json = GoodData.get GoodData.connection.user['profile']
|
29
|
+
@user = @json['accountSetting']['firstName'] + " " + @json['accountSetting']['lastName']
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'zip/zip'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
module GoodData
|
5
|
+
class NoProjectError < RuntimeError ; end
|
6
|
+
|
7
|
+
class Project
|
8
|
+
USERSPROJECTS_PATH = '/gdc/account/profile/%s/projects'
|
9
|
+
PROJECTS_PATH = '/gdc/projects'
|
10
|
+
PROJECT_PATH = '/gdc/projects/%s'
|
11
|
+
SLIS_PATH = '/ldm/singleloadinterface'
|
12
|
+
|
13
|
+
attr_accessor :connection
|
14
|
+
|
15
|
+
class << self
|
16
|
+
# Returns an array of all projects accessible by
|
17
|
+
# current user
|
18
|
+
def all
|
19
|
+
json = GoodData.get GoodData.profile.projects
|
20
|
+
json['projects'].map do |project|
|
21
|
+
Project.new project['project']
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Returns a Project object identified by given string
|
26
|
+
# The following identifiers are accepted
|
27
|
+
# - /gdc/md/<id>
|
28
|
+
# - /gdc/projects/<id>
|
29
|
+
# - <id>
|
30
|
+
#
|
31
|
+
def [](id)
|
32
|
+
if id.to_s !~ /^(\/gdc\/(projects|md)\/)?[a-zA-Z\d]+$/
|
33
|
+
raise ArgumentError.new("wrong type of argument. Should be either project ID or path")
|
34
|
+
end
|
35
|
+
|
36
|
+
id = id.match(/[a-zA-Z\d]+$/)[0] if id =~ /\//
|
37
|
+
|
38
|
+
response = GoodData.get PROJECT_PATH % id
|
39
|
+
Project.new response['project']
|
40
|
+
end
|
41
|
+
|
42
|
+
# Create a project from a given attributes
|
43
|
+
# Expected keys:
|
44
|
+
# - :title (mandatory)
|
45
|
+
# - :summary
|
46
|
+
# - :template (default /projects/blank)
|
47
|
+
#
|
48
|
+
def create(attributes)
|
49
|
+
GoodData.logger.info "Creating project #{attributes[:title]}"
|
50
|
+
|
51
|
+
json = {
|
52
|
+
'meta' => {
|
53
|
+
'title' => attributes[:title],
|
54
|
+
'summary' => attributes[:summary]
|
55
|
+
},
|
56
|
+
'content' => {
|
57
|
+
# 'state' => 'ENABLED',
|
58
|
+
'guidedNavigation' => 1
|
59
|
+
}
|
60
|
+
}
|
61
|
+
|
62
|
+
json['meta']['projectTemplate'] = attributes[:template] if attributes[:template] && !attributes[:template].empty?
|
63
|
+
project = Project.new json
|
64
|
+
project.save
|
65
|
+
project
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def initialize(json)
|
70
|
+
@json = json
|
71
|
+
end
|
72
|
+
|
73
|
+
def save
|
74
|
+
response = GoodData.post PROJECTS_PATH, { 'project' => @json }
|
75
|
+
if uri == nil
|
76
|
+
response = GoodData.get response['uri']
|
77
|
+
@json = response['project']
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def delete
|
82
|
+
raise "Project '#{title}' with id #{uri} is already deleted" if state == :deleted
|
83
|
+
GoodData.delete @json['links']['self']
|
84
|
+
end
|
85
|
+
|
86
|
+
def uri
|
87
|
+
@json['links']['self'] if @json['links'] && @json['links']['self']
|
88
|
+
end
|
89
|
+
|
90
|
+
def title
|
91
|
+
@json['meta']['title'] if @json['meta']
|
92
|
+
end
|
93
|
+
|
94
|
+
def state
|
95
|
+
@json['content']['state'].downcase.to_sym if @json['content'] && @json['content']['state']
|
96
|
+
end
|
97
|
+
|
98
|
+
def md
|
99
|
+
@md ||= Links.new GoodData.get(@json['links']['metadata'])
|
100
|
+
end
|
101
|
+
|
102
|
+
# Creates a data set within the project
|
103
|
+
#
|
104
|
+
# == Usage
|
105
|
+
# p.add_dataset 'Test', [ { 'name' => 'a1', 'type' => 'ATTRIBUTE' ... } ... ]
|
106
|
+
# p.add_dataset 'title' => 'Test', 'columns' => [ { 'name' => 'a1', 'type' => 'ATTRIBUTE' ... } ... ]
|
107
|
+
#
|
108
|
+
def add_dataset(schema, columns = nil)
|
109
|
+
schema = { 'title' => schema, 'columns' => columns } if columns
|
110
|
+
schema = Model::Schema.new schema if schema.is_a? Hash
|
111
|
+
raise ArgumentError.new("Required either schema object or title plus columns array") unless schema.is_a? Model::Schema
|
112
|
+
Model.add_schema schema, self
|
113
|
+
end
|
114
|
+
|
115
|
+
def upload(file, schema)
|
116
|
+
schema.upload file, self
|
117
|
+
end
|
118
|
+
|
119
|
+
def slis
|
120
|
+
link = "#{@json['links']['metadata']}#{SLIS_PATH}"
|
121
|
+
Metadata.new GoodData.get(link)
|
122
|
+
end
|
123
|
+
|
124
|
+
def datasets
|
125
|
+
datasets_uri = "#{md['data']}/sets"
|
126
|
+
response = GoodData.get datasets_uri
|
127
|
+
response['dataSetsInfo']['sets'].map do |ds|
|
128
|
+
DataSet.new ds
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def to_json
|
133
|
+
@json
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'tempfile'
|
3
|
+
|
4
|
+
require 'helper'
|
5
|
+
require 'gooddata/command'
|
6
|
+
|
7
|
+
GoodData.logger = Logger.new(STDOUT)
|
8
|
+
|
9
|
+
class TestRestApiBasic < Test::Unit::TestCase
|
10
|
+
context "datasets command" do
|
11
|
+
SAMPLE_DATASET_CONFIG = {
|
12
|
+
"columns" => [
|
13
|
+
{
|
14
|
+
"type" => "CONNECTION_POINT",
|
15
|
+
"name" => "A1",
|
16
|
+
"title" =>"A1"
|
17
|
+
},
|
18
|
+
{
|
19
|
+
"type" => "ATTRIBUTE",
|
20
|
+
"name" => "A2",
|
21
|
+
"title" => "A2",
|
22
|
+
"folder"=> "Test"
|
23
|
+
},
|
24
|
+
{
|
25
|
+
"type" => "FACT",
|
26
|
+
"name" => "F2",
|
27
|
+
"title" => "F2 \"asdasd\"",
|
28
|
+
"folder"=> "Test"
|
29
|
+
}
|
30
|
+
],
|
31
|
+
"title" => "Test"
|
32
|
+
}
|
33
|
+
|
34
|
+
should "list datasets" do
|
35
|
+
GoodData::Command.run "datasets", [ "--project", "FoodMartDemo" ]
|
36
|
+
end
|
37
|
+
|
38
|
+
should "apply a dataset model" do
|
39
|
+
GoodData::Command.connect
|
40
|
+
project = GoodData::Project.create \
|
41
|
+
:title => "gooddata-ruby TestRestApi #{Time.new.to_i}", :template => '/projectTemplates/empty/1'
|
42
|
+
|
43
|
+
Tempfile.open 'gdrb-test-' do |file|
|
44
|
+
file.puts SAMPLE_DATASET_CONFIG.to_json
|
45
|
+
file.close
|
46
|
+
GoodData::Command.run "datasets:apply", [ "--project", project.uri, file.path ]
|
47
|
+
end
|
48
|
+
project.delete
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
context "projects command" do
|
53
|
+
should "list projects" do
|
54
|
+
GoodData::Command.run "projects", []
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context "api command" do
|
59
|
+
should "perform a test login" do
|
60
|
+
GoodData::Command.run "api:test", []
|
61
|
+
end
|
62
|
+
|
63
|
+
should "get FoodMartDemo metadata" do
|
64
|
+
GoodData::Command.run "api:get", [ '/gdc/md/FoodMartDemo' ]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
context "profile command" do
|
69
|
+
should "show my GoodData profile" do
|
70
|
+
GoodData::Command.run "profile", []
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
context "help command" do
|
75
|
+
should "print help screen" do
|
76
|
+
GoodData::Command.run "help", []
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
context "version command" do
|
81
|
+
should "print version" do
|
82
|
+
GoodData::Command.run "version", []
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'gooddata/command'
|
3
|
+
|
4
|
+
class TestGuesser < Test::Unit::TestCase
|
5
|
+
should "order LDM types as follows: cp, fact, date, attribute" do
|
6
|
+
assert_equal [ :connection_point, :fact, :date, :attribute ], \
|
7
|
+
GoodData::Command::Guesser::sort_types([ :fact, :attribute, :connection_point, :date ])
|
8
|
+
assert_equal [ :fact ], GoodData::Command::Guesser::sort_types([ :fact ])
|
9
|
+
assert_equal [], GoodData::Command::Guesser::sort_types([])
|
10
|
+
end
|
11
|
+
|
12
|
+
should "guess facts, dates and connection points from a simple CSV" do
|
13
|
+
csv = [
|
14
|
+
[ 'cp', 'a1', 'a2', 'd1', 'd2', 'f'],
|
15
|
+
[ '1', 'one', 'huh', '2001-01-02', nil, '-1' ],
|
16
|
+
[ '2', 'two', 'blah', nil, '1970-10-23', '2.3' ],
|
17
|
+
[ '3', 'three', 'bleh', '0000-00-00', nil, '-3.14159'],
|
18
|
+
[ '4', 'one', 'huh', '2010-02-28 08:12:34', '1970-10-23', nil ]
|
19
|
+
]
|
20
|
+
fields = GoodData::Command::Guesser.new(csv).guess(csv.size + 10)
|
21
|
+
|
22
|
+
assert_kind_of Hash, fields, "guesser should return a Hash"
|
23
|
+
fields.each do |field, info|
|
24
|
+
assert_kind_of Array, info, "guess for '%s' is not an Array" % field
|
25
|
+
end
|
26
|
+
|
27
|
+
type_msg_fmt = 'checking guessed types of "%s"'
|
28
|
+
|
29
|
+
assert_equal GoodData::Command::Guesser::sort_types([
|
30
|
+
:connection_point, :fact, :attribute
|
31
|
+
]), fields['cp'], type_msg_fmt % 'cp'
|
32
|
+
|
33
|
+
assert_equal [ :attribute ], fields['a1'], type_msg_fmt % 'a1'
|
34
|
+
assert_equal [ :attribute ], fields['a2'], type_msg_fmt % 'a2'
|
35
|
+
|
36
|
+
assert_equal GoodData::Command::Guesser::sort_types([
|
37
|
+
:attribute, :connection_point, :date
|
38
|
+
]), fields['d1'], type_msg_fmt % 'd1'
|
39
|
+
assert_equal GoodData::Command::Guesser::sort_types([
|
40
|
+
:attribute, :date
|
41
|
+
]), fields['d2'], type_msg_fmt % 'd2'
|
42
|
+
assert_equal GoodData::Command::Guesser::sort_types([
|
43
|
+
:attribute, :connection_point, :fact
|
44
|
+
]), fields['f'], type_msg_fmt % 'f'
|
45
|
+
end
|
46
|
+
end
|
data/test/test_model.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
require 'helper'
|
4
|
+
require 'gooddata/model'
|
5
|
+
require 'gooddata/command'
|
6
|
+
|
7
|
+
GoodData.logger = Logger.new(STDOUT)
|
8
|
+
|
9
|
+
class TestModel < Test::Unit::TestCase
|
10
|
+
COLUMNS = [
|
11
|
+
{ 'type' => 'CONNECTION_POINT', 'name' => 'cp', 'title' => 'CP', 'folder' => 'test' },
|
12
|
+
{ 'type' => 'ATTRIBUTE', 'name' => 'a1', 'title' => 'A1', 'folder' => 'test' },
|
13
|
+
{ 'type' => 'ATTRIBUTE', 'name' => 'a2', 'title' => 'A2', 'folder' => 'test' },
|
14
|
+
{ 'type' => 'DATE', 'name' => 'event', 'title' => 'Event', 'folder' => 'test' },
|
15
|
+
{ 'type' => 'FACT', 'name' => 'f1', 'title' => 'F1', 'folder' => 'test' },
|
16
|
+
{ 'type' => 'FACT', 'name' => 'f2', 'title' => 'F2', 'folder' => 'test' },
|
17
|
+
]
|
18
|
+
SCHEMA = GoodData::Model::Schema.new 'title' => 'test', 'columns' => COLUMNS
|
19
|
+
|
20
|
+
context "GoodData model tools" do
|
21
|
+
# Initialize a GoodData connection using the credential
|
22
|
+
# stored in ~/.gooddata
|
23
|
+
setup do
|
24
|
+
GoodData::Command::connect
|
25
|
+
end
|
26
|
+
|
27
|
+
should "generate identifiers star ting with letters and without ugly characters" do
|
28
|
+
assert_equal 'fact.test.blah', GoodData::Model::Fact.new({ 'name' => 'blah' }, SCHEMA).identifier
|
29
|
+
assert_equal 'attr.test.blah', GoodData::Model::Attribute.new({ 'name' => '1_2_3 blah' }, SCHEMA).identifier
|
30
|
+
assert_equal 'dim.blaz', GoodData::Model::AttributeFolder.new(' b*ĺ*á#ž$').identifier
|
31
|
+
end
|
32
|
+
|
33
|
+
should "create a simple model in a sandbox project using Model.add_dataset" do
|
34
|
+
project = GoodData::Project.create :title => "gooddata-ruby test #{Time.new.to_i}"
|
35
|
+
GoodData.use project
|
36
|
+
objects = GoodData::Model.add_dataset 'Mrkev', COLUMNS
|
37
|
+
|
38
|
+
uris = objects['uris']
|
39
|
+
assert_equal "#{project.md['obj']}/1", uris[0]
|
40
|
+
# fetch last object (temporary objects can be placed at the begining of the list)
|
41
|
+
GoodData.get uris[uris.length - 1]
|
42
|
+
|
43
|
+
# created model should define SLI interface on the 'Mrkev' data set
|
44
|
+
# TODO move this into a standalone test covering gooddata/metadata.rb
|
45
|
+
ds = GoodData::DataSet['dataset.mrkev']
|
46
|
+
assert_not_nil ds
|
47
|
+
|
48
|
+
# clean-up
|
49
|
+
project.delete
|
50
|
+
end
|
51
|
+
|
52
|
+
should "create a simple model in a sandbox project using project.model.add_dataset" do
|
53
|
+
project = GoodData::Project.create :title => "gooddata-ruby test #{Time.new.to_i}"
|
54
|
+
objects = project.add_dataset 'Mrkev', COLUMNS
|
55
|
+
|
56
|
+
uris = objects['uris']
|
57
|
+
assert_equal "#{project.md['obj']}/1", uris[0]
|
58
|
+
# fetch last object (temporary objects can be placed at the begining of the list)
|
59
|
+
GoodData.get uris[uris.length - 1]
|
60
|
+
project.delete
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|