rally-wsapi 0.0.1

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 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: []