rally-wsapi 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f8186336211f73579be3d906c7fd2e159636fd23
4
+ data.tar.gz: f4145b3095c6822f9d7f94d6eb85bd0907b3fdda
5
+ SHA512:
6
+ metadata.gz: c85d7b133bdd3005a97ba1c07026d6ffa0d7d3cb2e83f8ffb1ad5655b8bf45464bff79c0160c3da47eb05b4bb83fcc2522111f4f056070cfc460f9c59281ba13
7
+ data.tar.gz: 47334a19d03a163f2aefe565b3c3f21e1c6c6a59d1fb1fa6e96feac91051563c08ca8b4c7ee3cfb67aa896497d061acfb53b8c4441e7fafc9f626a6f132af47e
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ gem 'rake'
2
+ gem 'multi_json'
3
+ gem 'faraday'
4
+
5
+ group :release do
6
+ gem 'jeweler'
7
+ end
8
+
9
+ group :test do
10
+ gem 'rspec'
11
+ gem 'webmock'
12
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,77 @@
1
+ GEM
2
+ specs:
3
+ addressable (2.3.6)
4
+ builder (3.2.2)
5
+ crack (0.4.2)
6
+ safe_yaml (~> 1.0.0)
7
+ descendants_tracker (0.0.4)
8
+ thread_safe (~> 0.3, >= 0.3.1)
9
+ diff-lcs (1.2.5)
10
+ faraday (0.9.0)
11
+ multipart-post (>= 1.2, < 3)
12
+ git (1.2.8)
13
+ github_api (0.11.3)
14
+ addressable (~> 2.3)
15
+ descendants_tracker (~> 0.0.1)
16
+ faraday (~> 0.8, < 0.10)
17
+ hashie (>= 1.2)
18
+ multi_json (>= 1.7.5, < 2.0)
19
+ nokogiri (~> 1.6.0)
20
+ oauth2
21
+ hashie (3.2.0)
22
+ highline (1.6.21)
23
+ jeweler (2.0.1)
24
+ builder
25
+ bundler (>= 1.0)
26
+ git (>= 1.2.5)
27
+ github_api
28
+ highline (>= 1.6.15)
29
+ nokogiri (>= 1.5.10)
30
+ rake
31
+ rdoc
32
+ json (1.8.1)
33
+ jwt (1.0.0)
34
+ mini_portile (0.6.0)
35
+ multi_json (1.10.1)
36
+ multi_xml (0.5.5)
37
+ multipart-post (2.0.0)
38
+ nokogiri (1.6.3.1)
39
+ mini_portile (= 0.6.0)
40
+ oauth2 (1.0.0)
41
+ faraday (>= 0.8, < 0.10)
42
+ jwt (~> 1.0)
43
+ multi_json (~> 1.3)
44
+ multi_xml (~> 0.5)
45
+ rack (~> 1.2)
46
+ rack (1.5.2)
47
+ rake (10.3.2)
48
+ rdoc (4.1.1)
49
+ json (~> 1.4)
50
+ rspec (3.0.0)
51
+ rspec-core (~> 3.0.0)
52
+ rspec-expectations (~> 3.0.0)
53
+ rspec-mocks (~> 3.0.0)
54
+ rspec-core (3.0.4)
55
+ rspec-support (~> 3.0.0)
56
+ rspec-expectations (3.0.4)
57
+ diff-lcs (>= 1.2.0, < 2.0)
58
+ rspec-support (~> 3.0.0)
59
+ rspec-mocks (3.0.4)
60
+ rspec-support (~> 3.0.0)
61
+ rspec-support (3.0.4)
62
+ safe_yaml (1.0.3)
63
+ thread_safe (0.3.4)
64
+ webmock (1.18.0)
65
+ addressable (>= 2.3.6)
66
+ crack (>= 0.3.2)
67
+
68
+ PLATFORMS
69
+ ruby
70
+
71
+ DEPENDENCIES
72
+ faraday
73
+ jeweler
74
+ multi_json
75
+ rake
76
+ rspec
77
+ webmock
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Flowdock
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # rally-wsapi
2
+
3
+ Rally WSAPI client written in Ruby.
4
+
5
+ ## Usage
6
+
7
+ In order to authenticate to WSAPI, you will need to obtain an API key for the Rally account. This can be done via [Rally OAuth](https://github.com/RallySoftware/rally-oauth-examples), for example.
8
+
9
+ Once you have the API key, you can initialize the session object with the key:
10
+ ```
11
+ s = Wsapi::Session.new("deadbeefdeadbeef")
12
+ ```
13
+
14
+ The constructor also accepts the following options:
15
+ * `:workspace_id`, if not given, user's default workspace is used for queries
16
+ * `:version`, WSAPI version, default is `3.0`
17
+
18
+
19
+ ## Method reference for Session
20
+
21
+ Some methods accept an optional hash that can have the following options:
22
+ * `:query`, add conditions for fetching objects. E.g. `(UserName = "John")`, see WSAPI documentation for details about the syntax
23
+ * `:start`, default: `1`, fetch results starting from given number
24
+ * `:workspace`, override workspace setting of the session
25
+ * `:pagesize`, default: `200`, page size for results
26
+ * `:fetch`, default: `true`, fetch full objects
27
+
28
+ #### Get the authenticated user
29
+ ```
30
+ get_current_user
31
+ ```
32
+
33
+ #### Get a user
34
+ ```
35
+ get_user(user_id)
36
+ ```
37
+
38
+ #### Get a user by username
39
+ ```
40
+ get_user_by_username(username)
41
+ ```
42
+
43
+ #### Get the subscription of the authenticated user
44
+ ```
45
+ get_user_subscription
46
+ ```
47
+
48
+ #### Get a subscription
49
+ ```
50
+ get_project(subscription_id)
51
+ ```
52
+
53
+ #### Get a project
54
+ ```
55
+ get_project(project_id)
56
+ ```
57
+
58
+ #### Get projects of the authenticated user
59
+ ```
60
+ get_projects(opts = {})
61
+ ```
62
+
63
+ #### Get team members in a project
64
+ ```
65
+ get_team_members(project_id, opts = {})
66
+ ```
67
+
68
+ #### Get editors in a project
69
+ ```
70
+ get_editors(project_id, opts = {})
71
+ ```
72
+
73
+ ## Result objects
74
+
75
+ There's a couple of convenience classes for the following object types:
76
+
77
+ * `User`
78
+ * `Subscription`
79
+ * `Project`
80
+
81
+ Other object types are represented by the generic `Object` class which the specific types above extend.
82
+
83
+ #### Object
84
+
85
+ Methods:
86
+ * `id`, identifier of the object
87
+ * `name`, name of the object
88
+ * `url`, URL of the object
89
+ * `workspace`, name of the object's workspace
90
+
91
+
92
+ #### User
93
+
94
+ Methods:
95
+ * `username`, username
96
+ * `first_name`, first name
97
+ * `last_name`, last name
98
+ * `name`, full name
99
+ * `email`, email address
100
+ * `admin?`, is the user admin in the subscription?
101
+
102
+ #### Subscription
103
+
104
+ Methods:
105
+ * `subscription_id`, subcription identifier
106
+
107
+ #### Project
108
+
109
+ Methods:
110
+ * `subscription`, `Subscription` of the project
111
+
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ require 'jeweler'
2
+
3
+ Jeweler::Tasks.new do |s|
4
+ s.name = "rally-wsapi"
5
+ s.summary = "Simple client for Rally WSAPI"
6
+ s.email = "antti@flowdock.com"
7
+ s.homepage = "http://github.com/flowdock/rally-wsapi"
8
+ s.description = "Simple client for Rally WSAPI"
9
+ s.authors = ["Antti Pitkänen"]
10
+ s.files = FileList["[A-Z]*", "{bin,generators,lib,test}/**/*"]
11
+ s.licenses = ["MIT"]
12
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
@@ -0,0 +1 @@
1
+ require_relative './wsapi/session'
@@ -0,0 +1,42 @@
1
+ require 'multi_json'
2
+
3
+ require_relative './models/object'
4
+ require_relative './models/subscription'
5
+ require_relative './models/user'
6
+ require_relative './models/project'
7
+
8
+ module Wsapi
9
+ class Mapper
10
+ def self.get_errors(json)
11
+ if result = json["QueryResult"]
12
+ result["Errors"]
13
+ elsif result = json["OperationResult"]
14
+ result["Errors"]
15
+ else
16
+ []
17
+ end
18
+ end
19
+
20
+ def self.get_object(response)
21
+ json = MultiJson.load(response.body)
22
+ if get_errors(json).empty? && json.size == 1
23
+ Wsapi::Object.from_data(json.keys.first, json.values.first)
24
+ else
25
+ raise ApiError.new("Errors: #{get_errors(json).inspect}", response)
26
+ end
27
+ rescue MultiJson::LoadError
28
+ raise ApiError.new("Invalid JSON response from WSAPI: #{response.body}", response)
29
+ end
30
+
31
+ def self.get_objects(response)
32
+ json = MultiJson.load(response.body)
33
+ if get_errors(json).empty? && query_result = json["QueryResult"]
34
+ query_result["Results"].map { |object| Wsapi::Object.from_data(object["_type"], object) }
35
+ else
36
+ raise ApiError.new("Errors: #{get_errors(json).inspect}", response)
37
+ end
38
+ rescue MultiJson::LoadError
39
+ raise ApiError.new("Invalid JSON response from WSAPI: #{response.body}", response)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,33 @@
1
+ module Wsapi
2
+ class Object
3
+ attr_reader :raw_data
4
+
5
+ def initialize(raw_data)
6
+ @raw_data = raw_data
7
+ end
8
+
9
+ def name
10
+ @raw_data['_refObjectName']
11
+ end
12
+
13
+ def id
14
+ @raw_data['ObjectID']
15
+ end
16
+
17
+ def url
18
+ @raw_data['_ref']
19
+ end
20
+
21
+ def workspace
22
+ @raw_data["Workspace"]["_refObjectName"]
23
+ end
24
+
25
+ def self.from_data(type, raw_data)
26
+ if type && Wsapi.const_defined?(type)
27
+ Wsapi.const_get(type).new(raw_data)
28
+ else
29
+ Object.new(raw_data)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,7 @@
1
+ module Wsapi
2
+ class Project < Wsapi::Object
3
+ def subscription
4
+ @subscription ||= Wsapi::Subscription.new(@raw_data["Subscription"])
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Wsapi
2
+ class Subscription < Wsapi::Object
3
+ def subscription_id
4
+ @raw_data["SubscriptionID"].to_s
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,27 @@
1
+ module Wsapi
2
+ class User < Wsapi::Object
3
+ def username
4
+ @raw_data["UserName"]
5
+ end
6
+
7
+ def first_name
8
+ @raw_data["FirstName"]
9
+ end
10
+
11
+ def last_name
12
+ @raw_data["LastName"]
13
+ end
14
+
15
+ def name
16
+ "#{@raw_data['FirstName']} #{@raw_data['LastName']}"
17
+ end
18
+
19
+ def email
20
+ @raw_data["EmailAddress"]
21
+ end
22
+
23
+ def admin?
24
+ @raw_data["SubscriptionAdmin"]
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,156 @@
1
+ require 'multi_json'
2
+ require 'faraday'
3
+
4
+ require_relative './mapper'
5
+
6
+ module Wsapi
7
+ class StandardErrorWithResponse < StandardError
8
+ attr_reader :response
9
+ def initialize(msg, response = nil)
10
+ @response = response
11
+ super(msg)
12
+ end
13
+ end
14
+ class AuthorizationError < StandardErrorWithResponse; end
15
+ class ApiError < StandardErrorWithResponse; end
16
+ class ObjectNotFoundError < StandardErrorWithResponse; end
17
+ class IpAddressLimited < StandardErrorWithResponse; end
18
+
19
+ WSAPI_URL = ENV['WSAPI_URL'] || 'https://rally1.rallydev.com/slm/webservice/'
20
+
21
+ class WsapiAuthentication < Faraday::Middleware
22
+ def initialize(logger, session_id)
23
+ @session_id = session_id
24
+ super(logger)
25
+ end
26
+
27
+ def call(env)
28
+ env[:request_headers]['ZSESSIONID'] = @session_id
29
+ @app.call(env)
30
+ end
31
+ end
32
+
33
+ class Session
34
+ attr_accessor :workspace_id
35
+
36
+ def initialize(session_id, opts = {})
37
+ @api_version = opts[:version] || "3.0"
38
+ @session_id = session_id
39
+ @workspace_id = opts[:workspace_id]
40
+ @conn = Faraday.new(ssl: { verify: false} ) do |faraday|
41
+ faraday.request :url_encoded # form-encode POST params
42
+ faraday.use WsapiAuthentication, @session_id
43
+ faraday.adapter Faraday.default_adapter # make requests with Net::HTTP
44
+ end
45
+ end
46
+
47
+ def get_user_subscription
48
+ response = wsapi_request(wsapi_resource_url("Subscription"))
49
+ Mapper.get_object(response)
50
+ end
51
+
52
+ def get_subscription(id)
53
+ response = wsapi_request(wsapi_resource_url("Subscription/#{id}"))
54
+ Mapper.get_object(response)
55
+ end
56
+
57
+ def get_projects(opts = {})
58
+ fetch_with_pages(opts) do |page_query|
59
+ wsapi_request(wsapi_resource_url("Project"), opts.merge(page_query))
60
+ end
61
+ end
62
+
63
+ def get_project(id)
64
+ response = wsapi_request(wsapi_resource_url("Project/#{id}"))
65
+ Mapper.get_object(response)
66
+ end
67
+
68
+ def get_current_user
69
+ response = wsapi_request(wsapi_resource_url("User"))
70
+ Mapper.get_object(response)
71
+ end
72
+
73
+ def get_user(id)
74
+ response = wsapi_request(wsapi_resource_url("User/#{id}"))
75
+ Mapper.get_object(response)
76
+ end
77
+
78
+ def get_user_by_username(username)
79
+ response = wsapi_request(wsapi_resource_url("User"), query: "(UserName = \"#{username}\")", pagesize: 1)
80
+ (Mapper.get_objects(response) ||[]).first
81
+ end
82
+
83
+ def get_team_members(project_id, opts = {})
84
+ fetch_with_pages(opts) do |page_query|
85
+ wsapi_request(wsapi_resource_url("Project/#{project_id}/TeamMembers"), opts.merge(page_query))
86
+ end
87
+ end
88
+
89
+ def get_editors(project_id, opts = {})
90
+ fetch_with_pages(opts) do |page_query|
91
+ wsapi_request(wsapi_resource_url("Project/#{project_id}/Editors"), opts.merge(page_query))
92
+ end
93
+ end
94
+
95
+ private
96
+
97
+ def workspace_url
98
+ wsapi_resource_url("Workspace/#{@workspace_id}")
99
+ end
100
+
101
+ def wsapi_resource_url(resource)
102
+ File.join(WSAPI_URL, "v#{@api_version}", resource)
103
+ end
104
+
105
+ def wsapi_request(url, opts = {})
106
+ response = @conn.get do |req|
107
+ req.url url
108
+ req.params['workspace'] = workspace_url if @workspace_id
109
+ req.params['query'] = opts[:query] if opts[:query]
110
+ req.params['start'] = opts[:start] || 1
111
+ req.params['pagesize'] = opts[:pagesize] || 200
112
+ req.params['fetch'] = opts[:fetch] || true # by default, fetch full objects
113
+ end
114
+ raise AuthorizationError.new("Unauthorized", response) if response.status == 401
115
+ raise ApiError.new("Internal server error", response) if response.status == 500
116
+ raise ObjectNotFoundError.new("Object not found") if object_not_found?(response)
117
+ raise IpAddressLimited.new("IP Address limited", response) if ip_address_limited?(response)
118
+ response
119
+ end
120
+
121
+ def ip_address_limited?(response)
122
+ limit_message = /Your IP address, (?:\d+\.?)+, is not within the allowed range that your subscription administrator has configured./
123
+ response.status > 401 && response.body.match(limit_message)
124
+ end
125
+
126
+ def object_not_found?(response)
127
+ if response.status == 200
128
+ result = MultiJson.load(response.body)["OperationResult"]
129
+ if result && error = result["Errors"].first
130
+ error.match("Cannot find object to read")
131
+ else
132
+ false
133
+ end
134
+ else
135
+ false
136
+ end
137
+ end
138
+
139
+ def fetch_with_pages(opts = {}, &block)
140
+ page_query = {
141
+ start: opts[:start] || 1,
142
+ pagesize: opts[:pagesize] || 100
143
+ }
144
+ resultCount = nil
145
+ objects = []
146
+ while(!resultCount || resultCount > objects.size) do
147
+ response = yield(page_query)
148
+ resultCount = MultiJson.load(response.body)["QueryResult"]["TotalResultCount"]
149
+ objects += Mapper.get_objects(response)
150
+ page_query[:start] += page_query[:pagesize]
151
+ end
152
+ objects
153
+ end
154
+ end
155
+ end
156
+
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rally-wsapi
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Antti Pitkänen
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-09-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: multi_json
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: faraday
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: Simple client for Rally WSAPI
56
+ email: antti@flowdock.com
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files:
60
+ - LICENSE
61
+ - README.md
62
+ files:
63
+ - Gemfile
64
+ - Gemfile.lock
65
+ - LICENSE
66
+ - README.md
67
+ - Rakefile
68
+ - VERSION
69
+ - lib/rally-wsapi.rb
70
+ - lib/wsapi/mapper.rb
71
+ - lib/wsapi/models/object.rb
72
+ - lib/wsapi/models/project.rb
73
+ - lib/wsapi/models/subscription.rb
74
+ - lib/wsapi/models/user.rb
75
+ - lib/wsapi/session.rb
76
+ homepage: http://github.com/flowdock/rally-wsapi
77
+ licenses:
78
+ - MIT
79
+ metadata: {}
80
+ post_install_message:
81
+ rdoc_options: []
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ requirements: []
95
+ rubyforge_project:
96
+ rubygems_version: 2.2.1
97
+ signing_key:
98
+ specification_version: 4
99
+ summary: Simple client for Rally WSAPI
100
+ test_files: []