Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lib: fetch pricing from API #62

Closed
wants to merge 2 commits into from
Closed

lib: fetch pricing from API #62

wants to merge 2 commits into from

Conversation

ghost
Copy link

@ghost ghost commented Dec 19, 2022

Pricing is a bit different than the other resource types. It only provides a single API endpoint /pricing. On this it only returns a single dictionary. All other resource types return an array of resources and have a /<resource>/<id> endpoint for single resources.

Our concept of <X> and <X>Resource classes currently maps to the latter concept.

I tried two different approaches for a user of our client:

Stick to the same concept as closely as possible. This is the one implemented now.

pricing = client.pricings.fetch
expect(pricing.currency).to be_a String

Only use a Pricing class or forward all calls from PricingResource to Pricing.

expect(client.pricing.currency).to be_a String

The second approach in my opinion would be nicer for a user, but had some complications. Without special additions to the code the client will on each call to client.pricing issue a new API request. This would cause needlessly many API requests if a user checks a few prices. A fix to this would be that the Client has to cache some information (pricing or pricing resource). Currently, it just creates a new instance of <X>Resource whenever an attribute is called.

Closes #28

@ghost ghost marked this pull request as draft December 20, 2022 15:16
@ghost
Copy link
Author

ghost commented Dec 20, 2022

Had a talk with @Kjarrigan. expect(client.pricing.currency).to be_a String is the better interface. We can cache the latest pricing inside the client with @pricing ||= .... If we need to refresh the prices during the runtime of a program, we can implement reload as proposed by #9.

We don't have to include EntryLoader and AbstractResource in Pricing if we send the request manually and parse the response manually - which is not too much effort.

We also had the idea to implement a cost estimation function. The best draft for that so far is:

# returns a single number
client.pricing.image.estimated_cost('fsn1', size_gb: 100)
client.pricing.server.estimated_cost('CX11', 'fsn1', runtime_hours: 100)
# ...

We'd also like to filter prices by resource type and/or region. The draft for that is (as far as I remember):

# returns a matrix (represented in some way) of the remaining dimensions,
# e.g. a list of hashes of the resolved matrix
client.pricing.image(region: 'fsn1')
client.pricing.server(name: 'cx11', region: 'fsn1')
# ...

Both approaches at the same time do not work together, but they can be combined in multiple ways.

Implement image, server etc. as a function with optional parameters:

# Allow to create a price matrix and then apply missing dimensions on the price matrix
client.pricing.image(region: 'fsn1').estimated_cost(size_gb: 100)
client.pricing.server(region: 'fsn1').estimated_cost(name: 'cx11', runtime_hours: 100)
# ...

# This would raise an exception, because not filtered down to a single dimension
# or alternatively we could also allow to return a matrix of estimated prices for different regions
# But that would mean that sometimes we return a single number, sometimes a matrix...
client.pricing.server(region: 'fsn1').estimated_cost(runtime_hours: 100)

Implement image, server etc. as a mere accessor with a filter function:

# get estimated price (single number)
client.pricing.image.estimated_cost('fsn1', size_gb: 100)

# get price overview, optionally filtered on some dimensions (matrix)
client.pricing.server
client.pricing.server.filter(name: 'cx11')
client.pricing.server.filter(region: 'fsn1')
client.pricing.server.filter(name: 'cx11', region: 'fsn1')  # still a matrix, with a single element

To me, the second approach looks cleaner. It does not have surprises when using it.

@ghost
Copy link
Author

ghost commented Dec 21, 2022

Example use for the first prototype. But it still needs work.

require 'hcloud'

client = Hcloud::Client.new(token: ENV['HCLOUD_TOKEN'], auto_pagination: true)

p client.pricing.server_types.estimated_cost('fsn1', name: 'cx11', runtime_hours: 300)

client.pricing.server_types.filter(location: 'hel1').each do |info|
  p "#{info[:name]} in #{info[:location]} costs EUR #{info[:prices][:price_monthly][:net]} per month"
end

# as a side-effect this also works (no name required in estimated_cost)
p client.pricing.server_types.filter(name: 'cx11').estimated_cost('fsn1', runtime_hours: 300)

# but it feels a bit strange that this does not work (since location is a required argument)
p client.pricing.server_types.filter(location: 'fsn1', name: 'cx11') \
  .estimated_cost(runtime_hours: 300)

# this of course works. Nobody would filter for the same location twice in a single line,
# but it might come up when combining filters and cost estimates in longer classes
p client.pricing.server_types.filter(location: 'fsn1', name: 'cx11') \
  .estimated_cost('fsn1', runtime_hours: 300)

# this fails, because the filtered data does not contain fsn1 anymore
p client.pricing.server_types.filter(location: 'hel1', name: 'cx11') \
  .estimated_cost('fsn1', runtime_hours: 300)

# I'm not so happy that this raises an exception
# (because without name it does not filter the cost data down to a single row).
# But the alternatives are: We sometimes return a number, sometimes a matrix of numbers.
# Or we always return a matrix of prices, which is a bit annoying for the user.
p client.pricing.server_types.estimated_cost('fsn1', runtime_hours: 300)

@ghost
Copy link
Author

ghost commented Feb 19, 2024

Quite complex design it seems and currently no need for it, closing.

@ghost ghost closed this Feb 19, 2024
This pull request was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Feature] Add Pricing
0 participants