Class: PowerBI::Tenant

Inherits:
Object
  • Object
show all
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:

  1. Requesting 5000 records at a time using $top and $skip

  2. If exactly 5000 records are returned, fetching the next page

  3. Accumulating all results across pages

  4. 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

Instance Method Summary collapse

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.message}, request URL: #{env.url}" },
  }
end

Instance Attribute Details

#adminObject (readonly)

Returns the value of attribute admin.



3
4
5
# File 'lib/power-bi/tenant.rb', line 3

def admin
  @admin
end

#capacitiesObject (readonly)

Returns the value of attribute capacities.



3
4
5
# File 'lib/power-bi/tenant.rb', line 3

def capacities
  @capacities
end

#gatewaysObject (readonly)

Returns the value of attribute gateways.



3
4
5
# File 'lib/power-bi/tenant.rb', line 3

def gateways
  @gateways
end

#profile_idObject (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

#profilesObject (readonly)

Returns the value of attribute profiles.



3
4
5
# File 'lib/power-bi/tenant.rb', line 3

def profiles
  @profiles
end

#workspacesObject (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(message, level: :info)
  if @logger
    @logger.send(level, message) # 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.options.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

#workspace(id) ⇒ Object



36
37
38
# File 'lib/power-bi/tenant.rb', line 36

def workspace(id)
  Workspace.new(self, nil, id)
end