Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions lib/octaspace.rb
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
# frozen_string_literal: true

require "octaspace/version"
require "octaspace/configuration"
require "octaspace/errors"
require "octaspace/payload_helpers"
require "octaspace/response"
require "octaspace/json_null"
require "octaspace/money"
require "octaspace/model"
require "octaspace/configuration"
require "octaspace/middleware/url_rotator"
require "octaspace/transport/base"
require "octaspace/transport/faraday_transport"
require "octaspace/types/account"
require "octaspace/types/balance"
require "octaspace/types/node"
require "octaspace/types/machines"
require "octaspace/types/session"
require "octaspace/types/account"
require "octaspace/types/network"
require "octaspace/types/hosting"
require "octaspace/types/stats"
require "octaspace/resources/base"
require "octaspace/resources/accounts"
require "octaspace/resources/nodes"
require "octaspace/resources/sessions"
require "octaspace/resources/apps"
require "octaspace/resources/network"
require "octaspace/resources/stats"
require "octaspace/resources/compute"
require "octaspace/resources/render"
require "octaspace/resources/idle_jobs"
require "octaspace/resources/sessions"
require "octaspace/resources/hosting"
require "octaspace/client"

# OctaSpace Ruby SDK
Expand Down
25 changes: 18 additions & 7 deletions lib/octaspace/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,20 @@ module OctaSpace
# on_response: ->(resp) { puts "← #{resp.status}" }
# )
class Client
attr_reader :accounts, :nodes, :sessions, :apps,
:network, :idle_jobs, :compute, :render
# @return [Resources::Accounts] balance
attr_reader :accounts
# @return [Resources::Compute] machines for rent: list, start, start_batch, restart
attr_reader :compute
# @return [Resources::Hosting] nodes of the account and their prices
attr_reader :hosting
# @return [Resources::Network] public platform statistics
attr_reader :network
# @return [Resources::Render] render nodes: list, start, start_batch
attr_reader :render
# @return [Resources::Sessions] running sessions: list, get, logs, stop, wait_ready, wait_stopped
attr_reader :sessions
# @return [Resources::Stats] public GPU market statistics
attr_reader :stats

# @param api_key [String, nil] API key for authentication (optional for public endpoints)
# @param opts [Hash] Per-instance configuration overrides.
Expand All @@ -45,13 +57,12 @@ def initialize(api_key: nil, transport: nil, **opts)
@config = build_config(api_key, opts)
@transport = transport || build_transport
@accounts = Resources::Accounts.new(@transport)
@nodes = Resources::Nodes.new(@transport)
@sessions = Resources::Sessions.new(@transport)
@apps = Resources::Apps.new(@transport)
@network = Resources::Network.new(@transport)
@idle_jobs = Resources::IdleJobs.new(@transport)
@compute = Resources::Compute.new(@transport)
@hosting = Resources::Hosting.new(@transport)
@network = Resources::Network.new(@transport)
@render = Resources::Render.new(@transport)
@sessions = Resources::Sessions.new(@transport)
@stats = Resources::Stats.new(@transport)
end

# Shut down persistent connections (only relevant in keep_alive mode)
Expand Down
69 changes: 69 additions & 0 deletions lib/octaspace/money.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# frozen_string_literal: true

module OctaSpace
# Conversions for micro-USD, the integer unit of every price, charge and
# balance in the API (1 micro-USD = $0.000001).
#
# @example
# OctaSpace::Money.format(100) # => "$0.000100"
# OctaSpace::Money.to_usd(1_250_000) # => (5/4)
# OctaSpace::Money.from_usd("0.25") # => 250000
module Money
# Number of micro-USD in one dollar
MICRO_USD_PER_USD = 1_000_000

module_function

# Converts an integer amount to dollars, exactly.
# @param amount [Integer] micro-USD (or units of 10^-decimals)
# @param decimals [Integer] decimal places of the unit (6 for micro-USD)
# @return [Rational]
def to_usd(amount, decimals: 6)
Rational(integer!(amount), 10**decimals)
end

# Converts dollars to micro-USD, rounded to the nearest integer
# (e.g. for +hosting.set_prices+).
# @param usd [Integer, Rational, String, Float] a String like "0.25" is
# exact; a Float goes through its shortest decimal form
# @return [Integer]
def from_usd(usd)
value = case usd
when Integer, Rational then usd
when Float
raise ArgumentError, "not a finite dollar amount: #{usd}" unless usd.finite?

Rational(usd.to_s)
when String
text = usd.strip.delete_prefix("$")
raise ArgumentError, "not a dollar amount: #{usd.inspect}" unless text.match?(/\A-?\d+(\.\d+)?\z/)

Rational(text)
else
raise ArgumentError, "not a dollar amount: #{usd.inspect}"
end
(value * MICRO_USD_PER_USD).round
end

# Formats an integer amount as dollars with all decimal places, using
# integer arithmetic: 100 → "$0.000100", -1_250_000 → "-$1.250000".
# @param amount [Integer] micro-USD (or units of 10^-decimals)
# @param decimals [Integer]
# @return [String]
def format(amount, decimals: 6)
amount = integer!(amount)
whole, fraction = amount.abs.divmod(10**decimals)
sign = amount.negative? ? "-" : ""
return "#{sign}$#{whole}" if decimals.zero?

"#{sign}$#{whole}.#{fraction.to_s.rjust(decimals, "0")}"
end

def integer!(amount)
raise ArgumentError, "not an integer amount: #{amount.inspect}" unless amount.is_a?(Integer)

amount
end
private_class_method :integer!
end
end
33 changes: 0 additions & 33 deletions lib/octaspace/payload_helpers.rb

This file was deleted.

29 changes: 9 additions & 20 deletions lib/octaspace/resources/accounts.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,20 @@

module OctaSpace
module Resources
# Account-related API endpoints
# The account the API key belongs to
#
# @example
# client.accounts.profile
# client.accounts.balance
# client.accounts.balance.formatted # => "$2.147661"
class Accounts < Base
# Fetch the authenticated user's profile
# GET /accounts
# @return [OctaSpace::Response]
def profile
get("/accounts")
end

# Fetch the authenticated user's balance
# Account balance.
# GET /accounts/balance
# @return [OctaSpace::Response]
#
# The API has no "who am I" endpoint, so this is also the way to check
# that an API key is valid. It is read from a replica, so a recent charge
# or top-up may show up with a delay.
# @return [OctaSpace::Types::Balance]
def balance
get("/accounts/balance")
end

# Generate / create a new wallet for the authenticated user
# POST /accounts
# @return [OctaSpace::Response]
def generate_wallet
post("/accounts")
Types::Balance.new(get("/accounts/balance").data)
end
end
end
Expand Down
19 changes: 0 additions & 19 deletions lib/octaspace/resources/apps.rb

This file was deleted.

58 changes: 58 additions & 0 deletions lib/octaspace/resources/hosting.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# frozen_string_literal: true

module OctaSpace
module Resources
# Nodes the account hosts
#
# @example
# client.hosting.nodes.each { |node| puts "#{node.number} #{node.status}" }
# client.hosting.set_prices("M95S-WZG8-7", base: OctaSpace::Money.from_usd("0.25"))
class Hosting < Base
PRICE_FIELDS = %i[base storage traffic min_rent_minutes].freeze

# GPU fields nodes may send as strings in +system.gpus+
HOSTING_GPU_FIELDS = (GPU_INTEGER_FIELDS + %w[mem_free_mb fan_speed gpu_temperature gpu_utilization power_draw_watt]).freeze

# Online nodes of the account with their state, hardware and prices.
# GET /hosting/nodes
#
# Offline nodes are not listed, and there is no per-node endpoint.
# @return [Array<OctaSpace::Types::HostingNode>]
def nodes
data = JsonNull.null_strings!(Array(get("/hosting/nodes").data))
JsonNull.gpu_numbers!(data.map { |node| node.is_a?(Hash) ? node["system"] : nil }.compact, HOSTING_GPU_FIELDS)
Types::HostingNode.wrap_all(data)
end

# Updates some prices of a node.
# PATCH /hosting/nodes/{number}/prices (→ 204)
#
# Only the given fields are sent; at least one is required. Amounts are
# integer micro-USD (convert dollars with {OctaSpace::Money.from_usd}).
# Ranges: base and traffic 100..1_000_000_000, storage 0..1_000_000_000,
# min_rent_minutes 0..60. The API refuses other values with
# {OctaSpace::ValidationError}, and an unknown node with
# {OctaSpace::NotFoundError}.
#
# 204 means the prices are stored; listings (+hosting.nodes+,
# +compute.list+) show them after replication, usually within seconds.
#
# @param number [String] node number; case and dashes are ignored
# @param base [Integer, nil]
# @param storage [Integer, nil]
# @param traffic [Integer, nil]
# @param min_rent_minutes [Integer, nil]
# @return [nil]
def set_prices(number, **prices)
body = compact_params(prices, PRICE_FIELDS)
raise ArgumentError, "set_prices needs at least one of #{PRICE_FIELDS.join(", ")}" if body.empty?

body.each do |field, value|
raise ArgumentError, "#{field} must be an Integer (micro-USD or minutes), got #{value.inspect}" unless value.is_a?(Integer)
end
patch("/hosting/nodes/#{encode(number)}/prices", body: body)
nil
end
end
end
end
32 changes: 0 additions & 32 deletions lib/octaspace/resources/idle_jobs.rb

This file was deleted.

12 changes: 7 additions & 5 deletions lib/octaspace/resources/network.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@

module OctaSpace
module Resources
# Network information endpoint
# Public platform statistics; no API key needed
#
# @example
# client.network.info
# client.network.info.nodes.online
class Network < Base
# Fetch combined network information (blockchain, market, nodes, power, etc.)
# Platform statistics snapshot.
# GET /network
# @return [OctaSpace::Response]
#
# Raises {OctaSpace::ServiceUnavailableError} while it is unavailable.
# @return [OctaSpace::Types::Network]
def info
get("/network", auth: :none)
Types::Network.new(get("/network", auth: :none).data)
end
end
end
Expand Down
Loading
Loading