gooddata 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|