Class: PowerBI::Tenant
- Inherits:
-
Object
- Object
- PowerBI::Tenant
- Defined in:
- lib/power-bi/tenant.rb
Constant Summary collapse
- MAX_PAGE_SIZE =
Fetches paginated data from the Power BI API using OData-style pagination.
Power BI API has a documented limit of 5000 records per request. This method handles pagination automatically by:
-
Requesting 5000 records at a time using $top and $skip
-
If exactly 5000 records are returned, fetching the next page
-
Accumulating all results across pages
-
Deduplicating records by ID (to handle insertions between requests)
Note: $skip-based pagination is not fully protected against deletions between requests (a deleted record may cause a subsequent record to go unseen). This risk is acceptable given the short pagination window.
-
5000- MAX_ITERATIONS =
100
Instance Attribute Summary collapse
-
#admin ⇒ Object
readonly
Returns the value of attribute admin.
-
#capacities ⇒ Object
readonly
Returns the value of attribute capacities.
-
#gateways ⇒ Object
readonly
Returns the value of attribute gateways.
-
#profile_id ⇒ Object
readonly
Returns the value of attribute profile_id.
-
#profiles ⇒ Object
readonly
Returns the value of attribute profiles.
-
#workspaces ⇒ Object
readonly
Returns the value of attribute workspaces.
Instance Method Summary collapse
- #capacity(id) ⇒ Object
- #delete(url, params = {}, use_profile: true) ⇒ Object
- #gateway(id) ⇒ Object
- #get(url, params = {}, use_profile: true) ⇒ Object
- #get_paginated(url, page_size: MAX_PAGE_SIZE, base_params: {}, use_profile: true, max_iterations: MAX_ITERATIONS) ⇒ Object
- #get_raw(url, params = {}, use_profile: true) ⇒ Object
-
#initialize(token_generator, retries: 5, logger: nil) ⇒ Tenant
constructor
A new instance of Tenant.
- #log(message, level: :info) ⇒ Object
- #patch(url, params = {}, use_profile: true) ⇒ Object
- #post(url, params = {}, use_profile: true) ⇒ Object
- #post_file(url, file, params = {}, use_profile: true) ⇒ Object
- #profile(id) ⇒ Object
- #profile=(profile) ⇒ Object
- #workspace(id) ⇒ Object
Constructor Details
#initialize(token_generator, retries: 5, logger: nil) ⇒ Tenant
Returns a new instance of Tenant.
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
# File 'lib/power-bi/tenant.rb', line 5 def initialize(token_generator, retries: 5, logger: nil) @token_generator = token_generator @workspaces = WorkspaceArray.new(self) @gateways = GatewayArray.new(self) @capacities = CapacityArray.new(self) @profiles = ProfileArray.new(self) @logger = logger @profile_id = nil @admin = Admin.new(self) ## WHY RETRIES? ## # It is noticed that once in a while (~0.1% API calls), the Power BI server returns a 500 (internal server error) without apparent reason, just retrying works :-) ################## @retry_options = { max: retries, exceptions: [Errno::ETIMEDOUT, Timeout::Error, Faraday::TimeoutError, Faraday::RetriableResponse, Faraday::ConnectionFailed], methods: [:get, :post, :patch, :delete], retry_statuses: [500], # internal server error interval: 0.2, interval_randomness: 0, backoff_factor: 4, retry_block: -> (env:, options:, retry_count:, exception:, will_retry_in:) { self.log "retrying...!! exception: #{exception} ---- #{exception.}, request URL: #{env.url}" }, } end |
Instance Attribute Details
#admin ⇒ Object (readonly)
Returns the value of attribute admin.
3 4 5 |
# File 'lib/power-bi/tenant.rb', line 3 def admin @admin end |
#capacities ⇒ Object (readonly)
Returns the value of attribute capacities.
3 4 5 |
# File 'lib/power-bi/tenant.rb', line 3 def capacities @capacities end |
#gateways ⇒ Object (readonly)
Returns the value of attribute gateways.
3 4 5 |
# File 'lib/power-bi/tenant.rb', line 3 def gateways @gateways end |
#profile_id ⇒ Object (readonly)
Returns the value of attribute profile_id.
3 4 5 |
# File 'lib/power-bi/tenant.rb', line 3 def profile_id @profile_id end |
#profiles ⇒ Object (readonly)
Returns the value of attribute profiles.
3 4 5 |
# File 'lib/power-bi/tenant.rb', line 3 def profiles @profiles end |
#workspaces ⇒ Object (readonly)
Returns the value of attribute workspaces.
3 4 5 |
# File 'lib/power-bi/tenant.rb', line 3 def workspaces @workspaces end |
Instance Method Details
#capacity(id) ⇒ Object
44 45 46 |
# File 'lib/power-bi/tenant.rb', line 44 def capacity(id) Capacity.new(self, nil, id) end |
#delete(url, params = {}, use_profile: true) ⇒ Object
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 |
# File 'lib/power-bi/tenant.rb', line 151 def delete(url, params = {}, use_profile: true) t0 = Time.now conn = Faraday.new do |f| f.request :retry, @retry_options end response = conn.delete(PowerBI::BASE_URL + url) do |req| req.params = params req.headers['Accept'] = 'application/json' req.headers['authorization'] = "Bearer #{token}" if use_profile add_spp_header(req) end yield req if block_given? end log "Calling (DELETE) #{response.env.url.to_s} - took #{((Time.now - t0) * 1000).to_i} ms - status: #{response.status}" if [400, 401, 404].include? response.status raise NotFoundError end unless [200, 202].include? response.status raise APIError.new("Error calling Power BI API (status #{response.status}): #{response.body}") end unless response.body.empty? JSON.parse(response.body, symbolize_names: true) end end |
#gateway(id) ⇒ Object
40 41 42 |
# File 'lib/power-bi/tenant.rb', line 40 def gateway(id) Gateway.new(self, nil, id) end |
#get(url, params = {}, use_profile: true) ⇒ Object
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/power-bi/tenant.rb', line 57 def get(url, params = {}, use_profile: true) t0 = Time.now conn = Faraday.new do |f| f.request :retry, @retry_options end response = conn.get(PowerBI::BASE_URL + url) do |req| req.params = params req.headers['Accept'] = 'application/json' req.headers['authorization'] = "Bearer #{token}" if use_profile add_spp_header(req) end yield req if block_given? end if response.status == 400 raise NotFoundError end unless [200, 202].include? response.status raise APIError.new("Error calling Power BI API (status #{response.status}): #{response.body}") end log "Calling (GET) #{response.env.url.to_s} - took #{((Time.now - t0) * 1000).to_i} ms - status: #{response.status}" unless response.body.empty? JSON.parse(response.body, symbolize_names: true) end end |
#get_paginated(url, page_size: MAX_PAGE_SIZE, base_params: {}, use_profile: true, max_iterations: MAX_ITERATIONS) ⇒ Object
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 |
# File 'lib/power-bi/tenant.rb', line 215 def get_paginated(url, page_size: MAX_PAGE_SIZE, base_params: {}, use_profile: true, max_iterations: MAX_ITERATIONS) page_size = [page_size, MAX_PAGE_SIZE].min skip = 0 all_data = [] iteration = 0 loop do iteration += 1 if iteration > max_iterations log "WARNING: Reached maximum iteration limit (#{max_iterations}). " \ "Fetched #{all_data.size} records so far. This may indicate an API issue or " \ "an extremely large dataset. Consider using API filters to reduce the result set.", level: :warn break end params = base_params.merge({ '$top' => page_size, '$skip' => skip }) log "Fetching paginated data from #{url} (skip: #{skip}, top: #{page_size}, iteration: #{iteration})" resp = get(url, params, use_profile: use_profile) batch = resp[:value] || [] all_data += batch batch_count = batch.size log "Received #{batch_count} records (total so far: #{all_data.size})" # If we got fewer records than requested, we've reached the last page break if batch_count < page_size skip += batch_count end # Deduplicate by ID to handle any records that were inserted between requests. # Insertions before the current $skip position shift items right, which can # cause duplicates across pages. deduplicated_data = all_data.uniq{|r| r[:id]} if deduplicated_data.size < all_data.size log "Removed #{all_data.size - deduplicated_data.size} duplicate records during deduplication" end deduplicated_data end |
#get_raw(url, params = {}, use_profile: true) ⇒ Object
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
# File 'lib/power-bi/tenant.rb', line 83 def get_raw(url, params = {}, use_profile: true) t0 = Time.now conn = Faraday.new do |f| f.request :retry, @retry_options end response = conn.get(PowerBI::BASE_URL + url) do |req| req.params = params req.headers['authorization'] = "Bearer #{token}" if use_profile add_spp_header(req) end yield req if block_given? end log "Calling (GET - raw) #{response.env.url.to_s} - took #{((Time.now - t0) * 1000).to_i} ms - status: #{response.status}" unless [200, 202].include? response.status raise APIError.new("Error calling Power BI API (status #{response.status}): #{response.body}") end response.body end |
#log(message, level: :info) ⇒ Object
30 31 32 33 34 |
# File 'lib/power-bi/tenant.rb', line 30 def log(, level: :info) if @logger @logger.send(level, ) # hence, the logger needs to implement the 'level' methods end end |
#patch(url, params = {}, use_profile: true) ⇒ Object
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
# File 'lib/power-bi/tenant.rb', line 127 def patch(url, params = {}, use_profile: true) t0 = Time.now conn = Faraday.new do |f| f.request :retry, @retry_options end response = conn.patch(PowerBI::BASE_URL + url) do |req| req.params = params req.headers['Accept'] = 'application/json' req.headers['Content-Type'] = 'application/json' req.headers['authorization'] = "Bearer #{token}" if use_profile add_spp_header(req) end yield req if block_given? end log "Calling (PATCH) #{response.env.url.to_s} - took #{((Time.now - t0) * 1000).to_i} ms - status: #{response.status}" unless [200, 202].include? response.status raise APIError.new("Error calling Power BI API (status #{response.status}): #{response.body}") end unless response.body.empty? JSON.parse(response.body, symbolize_names: true) end end |
#post(url, params = {}, use_profile: true) ⇒ Object
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
# File 'lib/power-bi/tenant.rb', line 103 def post(url, params = {}, use_profile: true) t0 = Time.now conn = Faraday.new do |f| f.request :retry, @retry_options end response = conn.post(PowerBI::BASE_URL + url) do |req| req.params = params req.headers['Accept'] = 'application/json' req.headers['Content-Type'] = 'application/json' req.headers['authorization'] = "Bearer #{token}" if use_profile add_spp_header(req) end yield req if block_given? end log "Calling (POST) #{response.env.url.to_s} - took #{((Time.now - t0) * 1000).to_i} ms - status: #{response.status}" unless [200, 201, 202].include? response.status raise APIError.new("Error calling Power BI API (status #{response.status}): #{response.body}") end unless response.body.empty? JSON.parse(response.body, symbolize_names: true) end end |
#post_file(url, file, params = {}, use_profile: true) ⇒ Object
177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 |
# File 'lib/power-bi/tenant.rb', line 177 def post_file(url, file, params = {}, use_profile: true) t0 = Time.now conn = Faraday.new do |f| f.request :multipart f.request :retry, @retry_options end response = conn.post(PowerBI::BASE_URL + url) do |req| req.params = params req.headers['Accept'] = 'application/json' req.headers['Content-Type'] = 'multipart/form-data' req.headers['authorization'] = "Bearer #{token}" if use_profile add_spp_header(req) end req.body = {value: Faraday::UploadIO.new(file, 'application/octet-stream')} req..timeout = 120 # default is 60 seconds Net::ReadTimeout end log "Calling (POST - file) #{response.env.url.to_s} - took #{((Time.now - t0) * 1000).to_i} ms - status: #{response.status}" if response.status != 202 raise APIError.new("Error calling Power BI API (status #{response.status}): #{response.body}") end JSON.parse(response.body, symbolize_names: true) end |
#profile(id) ⇒ Object
48 49 50 |
# File 'lib/power-bi/tenant.rb', line 48 def profile(id) Profile.new(self, nil, id) end |
#profile=(profile) ⇒ Object
52 53 54 55 |
# File 'lib/power-bi/tenant.rb', line 52 def profile=(profile) @profile_id = profile.is_a?(String) ? profile : profile&.id @workspaces.reload # we need to reload the workspaces because we look through the eyes of the profile end |