octaspace 0.1.0
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 +7 -0
- data/CHANGELOG.md +22 -0
- data/MIT-LICENSE +22 -0
- data/README.md +336 -0
- data/lib/octaspace/client.rb +109 -0
- data/lib/octaspace/configuration.rb +136 -0
- data/lib/octaspace/errors.rb +76 -0
- data/lib/octaspace/middleware/url_rotator.rb +92 -0
- data/lib/octaspace/railtie.rb +28 -0
- data/lib/octaspace/resources/accounts.rb +33 -0
- data/lib/octaspace/resources/apps.rb +19 -0
- data/lib/octaspace/resources/base.rb +35 -0
- data/lib/octaspace/resources/idle_jobs.rb +32 -0
- data/lib/octaspace/resources/network.rb +18 -0
- data/lib/octaspace/resources/nodes.rb +63 -0
- data/lib/octaspace/resources/services/machine_rental.rb +48 -0
- data/lib/octaspace/resources/services/render.rb +30 -0
- data/lib/octaspace/resources/services/session_proxy.rb +49 -0
- data/lib/octaspace/resources/services/vpn.rb +30 -0
- data/lib/octaspace/resources/services.rb +39 -0
- data/lib/octaspace/resources/sessions.rb +23 -0
- data/lib/octaspace/response.rb +40 -0
- data/lib/octaspace/transport/base.rb +51 -0
- data/lib/octaspace/transport/faraday_transport.rb +160 -0
- data/lib/octaspace/transport/persistent_transport.rb +88 -0
- data/lib/octaspace/types/account.rb +11 -0
- data/lib/octaspace/types/balance.rb +11 -0
- data/lib/octaspace/types/node.rb +12 -0
- data/lib/octaspace/types/session.rb +11 -0
- data/lib/octaspace/version.rb +5 -0
- data/lib/octaspace.rb +94 -0
- metadata +103 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9dd2b5c9ae96cbc0bef9e36fd492b2a1dbdcf53af596a82193986039ff8a4da9
|
|
4
|
+
data.tar.gz: b0f0a7a1a3301be4f3d1dbe33f60652fc5c9e4b36e709779076d374fb0e16ff4
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 59cb1140ec7e34939b2535857d4d7afd395b8b25d45ac69df2c14c524bd1e14503f969704299fe6aa3d51021fa2491a8f00d0cf4dee3a3d848308a9aa9fb577d
|
|
7
|
+
data.tar.gz: 6b4353b1524bae1e594ef4109a93a371e539d48b157dcdd4e165a1283c60e4a86e719818f62276d3cac2628e4bf9cd419b651f5a4633302d391e3d5fc41f476d
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2026-04-12
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Initial release of the official OctaSpace Ruby SDK.
|
|
13
|
+
- Support for core resources: Accounts, Nodes, Sessions, Apps, Network, and Idle Jobs.
|
|
14
|
+
- Resource-oriented client: `client.nodes.list`, `client.services.session(uuid).stop`.
|
|
15
|
+
- Persistent HTTP connections (keep-alive mode) via `ConnectionPool`.
|
|
16
|
+
- Automatic URL rotation and failover for high availability.
|
|
17
|
+
- Retry with exponential backoff and jitter for transient errors.
|
|
18
|
+
- Typed error hierarchy based on HTTP status codes.
|
|
19
|
+
- Rails integration with automatic client sharing and graceful shutdown.
|
|
20
|
+
- Interactive Playground app for manual testing and diagnostics.
|
|
21
|
+
|
|
22
|
+
[0.1.0]: https://github.com/octaspace/ruby-sdk/compare/v0.1.0...HEAD
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright (c) 2024-present OctaSpace
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included
|
|
14
|
+
in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
# octaspace
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/octaspace)
|
|
4
|
+
[](https://github.com/octaspace/ruby-sdk/actions/workflows/ci.yml)
|
|
5
|
+
[](MIT-LICENSE)
|
|
6
|
+
|
|
7
|
+
Official Ruby SDK for the [OctaSpace API](https://api.octa.space/api-docs).
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Resource-oriented API** — `client.nodes.list`, `client.services.session(uuid).stop`
|
|
12
|
+
- **Keep-alive mode** — persistent connections via `faraday-net_http_persistent` + `connection_pool`
|
|
13
|
+
- **URL rotation / failover** — round-robin across multiple endpoints with per-URL cooldown
|
|
14
|
+
- **Retry with exponential backoff + jitter** — configurable retries on transient failures
|
|
15
|
+
- **Typed error hierarchy** — 12 exception classes mapped from HTTP status codes
|
|
16
|
+
- **`on_request` / `on_response` hooks** — for logging, tracing, APM
|
|
17
|
+
- **Rails integration** — Railtie, shared client, graceful shutdown at_exit
|
|
18
|
+
- **Playground app** — `bin/playground` with live diagnostics page
|
|
19
|
+
- **Ruby ≥ 3.2**, Rails 7.1 / 7.2 / 8.0
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
# Gemfile
|
|
25
|
+
gem "octaspace"
|
|
26
|
+
|
|
27
|
+
# Optional — required for keep_alive: true
|
|
28
|
+
gem "faraday-net_http_persistent"
|
|
29
|
+
gem "connection_pool"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
bundle install
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
require "octaspace"
|
|
40
|
+
|
|
41
|
+
# Public endpoints (no API key required)
|
|
42
|
+
client = OctaSpace::Client.new
|
|
43
|
+
client.network.info # GET /network
|
|
44
|
+
|
|
45
|
+
# Authenticated endpoints
|
|
46
|
+
client = OctaSpace::Client.new(api_key: ENV["OCTA_API_KEY"])
|
|
47
|
+
|
|
48
|
+
# Accounts
|
|
49
|
+
client.accounts.profile # GET /accounts
|
|
50
|
+
client.accounts.balance # GET /accounts/balance
|
|
51
|
+
|
|
52
|
+
# Nodes
|
|
53
|
+
client.nodes.list # GET /nodes
|
|
54
|
+
client.nodes.find(123) # GET /nodes/:id
|
|
55
|
+
client.nodes.reboot(123) # GET /nodes/:id/reboot
|
|
56
|
+
client.nodes.update_prices(123, gpu_hour: 0.5) # PATCH /nodes/:id/prices
|
|
57
|
+
|
|
58
|
+
# Sessions (list)
|
|
59
|
+
client.sessions.list # GET /sessions
|
|
60
|
+
|
|
61
|
+
# Session proxy — operations on a specific session
|
|
62
|
+
session = client.services.session("uuid-here")
|
|
63
|
+
session.info # GET /services/:uuid/info
|
|
64
|
+
session.logs # GET /services/:uuid/logs
|
|
65
|
+
session.stop(score: 5) # POST /services/:uuid/stop
|
|
66
|
+
|
|
67
|
+
# Services
|
|
68
|
+
client.services.mr.list # GET /services/mr
|
|
69
|
+
client.services.mr.create(
|
|
70
|
+
node_id: 1,
|
|
71
|
+
disk_size: 10,
|
|
72
|
+
image: "ubuntu:24.04",
|
|
73
|
+
app: "249b4cb3-3db1-4c06-98a4-772ba88cd81c"
|
|
74
|
+
) # POST /services/mr
|
|
75
|
+
client.services.vpn.list # GET /services/vpn
|
|
76
|
+
client.services.vpn.create(node_id: 1, subkind: "wg") # POST /services/vpn
|
|
77
|
+
client.services.render.list # GET /services/render
|
|
78
|
+
client.services.render.create(node_id: 1, disk_size: 100) # POST /services/render
|
|
79
|
+
|
|
80
|
+
# Apps
|
|
81
|
+
client.apps.list # GET /apps
|
|
82
|
+
|
|
83
|
+
# Network
|
|
84
|
+
client.network.info # GET /network
|
|
85
|
+
|
|
86
|
+
# Idle Jobs
|
|
87
|
+
client.idle_jobs.find(node_id: 69, job_id: 42) # GET /idle_jobs/:node_id/:job_id
|
|
88
|
+
client.idle_jobs.logs(node_id: 69, job_id: 42) # GET /idle_jobs/:node_id/:job_id/logs
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Rails Integration
|
|
92
|
+
|
|
93
|
+
### Initializer
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
# config/initializers/octaspace.rb
|
|
97
|
+
OctaSpace.configure do |config|
|
|
98
|
+
config.api_key = ENV["OCTA_API_KEY"]
|
|
99
|
+
config.keep_alive = true
|
|
100
|
+
config.pool_size = ENV.fetch("RAILS_MAX_THREADS", 5).to_i
|
|
101
|
+
config.logger = Rails.logger
|
|
102
|
+
end
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Shared client
|
|
106
|
+
|
|
107
|
+
`OctaSpace.client` (called without arguments) returns a **lazily-initialized shared client** built from global configuration. It is safe to call it on every request:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
# app/controllers/application_controller.rb
|
|
111
|
+
def octa_client
|
|
112
|
+
OctaSpace.client # returns the same instance each time — no new connections
|
|
113
|
+
end
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Pass arguments to create a **one-off client** instead (e.g., for per-user API keys):
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
OctaSpace.client(api_key: current_user.octa_api_key) # new client, not cached
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Graceful shutdown
|
|
123
|
+
|
|
124
|
+
The Railtie registers an `at_exit` hook that automatically shuts down the shared client's connection pool when the Rails process stops. No manual cleanup needed.
|
|
125
|
+
|
|
126
|
+
## Configuration Reference
|
|
127
|
+
|
|
128
|
+
| Option | Default | Description |
|
|
129
|
+
|---|---|---|
|
|
130
|
+
| `api_key` | `nil` | API key sent as `Authorization` header (optional for public endpoints) |
|
|
131
|
+
| `base_url` | `https://api.octa.space` | Single API endpoint |
|
|
132
|
+
| `base_urls` | `nil` | Multiple endpoints — enables URL rotation |
|
|
133
|
+
| `keep_alive` | `false` | Persistent HTTP connections + pool |
|
|
134
|
+
| `pool_size` | `5` | Connection pool size (keep-alive mode) |
|
|
135
|
+
| `pool_timeout` | `5` | Seconds to wait for a pool slot |
|
|
136
|
+
| `idle_timeout` | `60` | Seconds before an idle connection closes |
|
|
137
|
+
| `open_timeout` | `10` | Seconds to open TCP connection |
|
|
138
|
+
| `read_timeout` | `30` | Seconds to read response |
|
|
139
|
+
| `write_timeout` | `30` | Seconds to write request body |
|
|
140
|
+
| `max_retries` | `2` | Retry attempts on transient failures |
|
|
141
|
+
| `retry_interval` | `0.5` | Base retry interval in seconds |
|
|
142
|
+
| `backoff_factor` | `2.0` | Exponential backoff multiplier |
|
|
143
|
+
| `ssl_verify` | `true` | Verify SSL certificates |
|
|
144
|
+
| `on_request` | `nil` | `callable(req_hash)` — before each request |
|
|
145
|
+
| `on_response` | `nil` | `callable(response)` — after each response |
|
|
146
|
+
| `logger` | `nil` | Ruby `Logger` instance |
|
|
147
|
+
| `log_level` | `:info` | Log level |
|
|
148
|
+
| `user_agent` | auto | `User-Agent` header value |
|
|
149
|
+
|
|
150
|
+
`persistent` is an alias for `keep_alive` for compatibility with Cube internals.
|
|
151
|
+
|
|
152
|
+
## Keep-Alive Mode
|
|
153
|
+
|
|
154
|
+
Requires `faraday-net_http_persistent` and `connection_pool` in your Gemfile.
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
client = OctaSpace::Client.new(
|
|
158
|
+
api_key: ENV["OCTA_API_KEY"],
|
|
159
|
+
keep_alive: true,
|
|
160
|
+
pool_size: ENV.fetch("RAILS_MAX_THREADS", 5).to_i,
|
|
161
|
+
idle_timeout: 120
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Diagnostics
|
|
165
|
+
client.transport_stats
|
|
166
|
+
# => { mode: :persistent, pools: { "https://api.octa.space" => { size: 5, available: 4 } } }
|
|
167
|
+
|
|
168
|
+
# Explicit shutdown (optional — Railtie does this automatically in Rails)
|
|
169
|
+
client.shutdown
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## URL Rotation / Failover
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
client = OctaSpace::Client.new(
|
|
176
|
+
api_key: ENV["OCTA_API_KEY"],
|
|
177
|
+
base_urls: ["https://api.octa.space", "https://api2.octa.space"]
|
|
178
|
+
)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
- Requests are distributed round-robin across healthy endpoints.
|
|
182
|
+
- If an endpoint raises a connection or timeout error, it enters a **30-second cooldown** and traffic shifts to the remaining endpoints.
|
|
183
|
+
- After cooldown, the endpoint is re-admitted automatically.
|
|
184
|
+
|
|
185
|
+
## Hooks
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
OctaSpace.configure do |config|
|
|
189
|
+
config.on_request = ->(req) { Rails.logger.debug "→ #{req[:method].upcase} #{req[:path]}" }
|
|
190
|
+
config.on_response = ->(resp) { Rails.logger.debug "← #{resp.status} (#{resp.request_id})" }
|
|
191
|
+
end
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`on_request` receives `{ method:, path:, params: }`.
|
|
195
|
+
`on_response` receives an `OctaSpace::Response` instance.
|
|
196
|
+
|
|
197
|
+
## Error Handling
|
|
198
|
+
|
|
199
|
+
```ruby
|
|
200
|
+
begin
|
|
201
|
+
client.nodes.find(999_999)
|
|
202
|
+
rescue OctaSpace::NotFoundError => e
|
|
203
|
+
puts "Not found — request_id: #{e.request_id}"
|
|
204
|
+
rescue OctaSpace::AuthenticationError
|
|
205
|
+
puts "Invalid API key"
|
|
206
|
+
rescue OctaSpace::RateLimitError => e
|
|
207
|
+
sleep e.retry_after
|
|
208
|
+
retry
|
|
209
|
+
rescue OctaSpace::ConnectionError, OctaSpace::TimeoutError => e
|
|
210
|
+
puts "Network error: #{e.message}"
|
|
211
|
+
rescue OctaSpace::Error => e
|
|
212
|
+
puts "API error #{e.status}: #{e.message}"
|
|
213
|
+
end
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Error hierarchy
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
OctaSpace::Error
|
|
220
|
+
├── ConfigurationError — missing gems, invalid config
|
|
221
|
+
├── NetworkError — no HTTP response received
|
|
222
|
+
│ ├── ConnectionError — TCP connection refused / failed
|
|
223
|
+
│ └── TimeoutError — open/read timeout
|
|
224
|
+
└── ApiError — HTTP response received with error status
|
|
225
|
+
├── AuthenticationError 401
|
|
226
|
+
├── PermissionError 403
|
|
227
|
+
├── NotFoundError 404
|
|
228
|
+
├── ValidationError 422
|
|
229
|
+
├── RateLimitError 429 → #retry_after (seconds)
|
|
230
|
+
└── ServerError 5xx
|
|
231
|
+
├── BadGatewayError 502
|
|
232
|
+
├── ServiceUnavailableError 503
|
|
233
|
+
└── GatewayTimeoutError 504
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
All `ApiError` subclasses expose:
|
|
237
|
+
|
|
238
|
+
- `#status` — HTTP status code
|
|
239
|
+
- `#request_id` — value of `X-Request-Id` response header
|
|
240
|
+
- `#response` — the raw `OctaSpace::Response` object
|
|
241
|
+
|
|
242
|
+
## Types (Value Objects)
|
|
243
|
+
|
|
244
|
+
The `OctaSpace::Types` namespace provides immutable `Data.define` value objects for domain entities. They are **not** returned by default — resources return raw `response.data` (Hash/Array). Use them explicitly when you want structured objects:
|
|
245
|
+
|
|
246
|
+
```ruby
|
|
247
|
+
response = client.nodes.find(123)
|
|
248
|
+
node = OctaSpace::Types::Node.new(**response.data.transform_keys(&:to_sym))
|
|
249
|
+
|
|
250
|
+
node.online? # => true / false
|
|
251
|
+
node.state # => "online"
|
|
252
|
+
node.id # => 123
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Available types: `Node`, `Account`, `Balance`, `Session`.
|
|
256
|
+
|
|
257
|
+
## Playground App
|
|
258
|
+
|
|
259
|
+
Interactive demo for manual testing against a real API key:
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
OCTA_API_KEY=your_key bin/playground
|
|
263
|
+
# → http://localhost:3000
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Pages:
|
|
267
|
+
|
|
268
|
+
| Route | Content |
|
|
269
|
+
|---|---|
|
|
270
|
+
| `/playground/account` | Profile + balance |
|
|
271
|
+
| `/playground/nodes` | Node list with state badges |
|
|
272
|
+
| `/playground/sessions` | Active sessions |
|
|
273
|
+
| `/playground/services` | Machine Rentals + VPN sessions |
|
|
274
|
+
| `/playground/diagnostics` | Transport mode, pool stats, URL rotator state (auto-refresh every 5s) |
|
|
275
|
+
|
|
276
|
+
## Development
|
|
277
|
+
|
|
278
|
+
### Gem Development
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
bin/console # IRB with gem loaded
|
|
282
|
+
bundle exec standardrb # lint (StandardRB)
|
|
283
|
+
bundle exec rake test # tests only
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Local Standalone Testing
|
|
287
|
+
|
|
288
|
+
You can verify the gem in a clean Ruby environment without Rails:
|
|
289
|
+
|
|
290
|
+
1. Build the gem: `gem build octaspace.gemspec`
|
|
291
|
+
2. Install it locally: `gem install ./octaspace-0.1.0.gem`
|
|
292
|
+
3. Test in IRB:
|
|
293
|
+
|
|
294
|
+
```ruby
|
|
295
|
+
irb
|
|
296
|
+
> require 'octaspace'
|
|
297
|
+
> client = OctaSpace::Client.new # Test public endpoints
|
|
298
|
+
> client.network.info
|
|
299
|
+
> client_auth = OctaSpace::Client.new(api_key: "token") # Test authenticated client
|
|
300
|
+
> client_auth.accounts.profile.data # Retrieving profile data (email, ID, etc.)
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Running tests against multiple Rails versions
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
bundle exec appraisal rails-7-1 rake test
|
|
307
|
+
bundle exec appraisal rails-7-2 rake test
|
|
308
|
+
bundle exec appraisal rails-8-0 rake test
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Dummy Application (Playground)
|
|
312
|
+
|
|
313
|
+
The repository includes a Rails "Dummy" application for manual testing and UI prototyping. It is located in `test/dummy`.`.
|
|
314
|
+
|
|
315
|
+
To run the dummy app:
|
|
316
|
+
|
|
317
|
+
1. Ensure you have an API key: `export OCTA_API_KEY=your_key`
|
|
318
|
+
2. Run the playground script: `bin/playground` (starts Puma on port 3000)
|
|
319
|
+
3. Visit `http://localhost:3000/playground/diagnostics`
|
|
320
|
+
|
|
321
|
+
You can also run it manually from the directory:
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
cd test/dummy
|
|
325
|
+
bin/rails server
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
The dummy app is configured to use the local version of the gem. It is **not** included in the published gem package.
|
|
329
|
+
|
|
330
|
+
## Packaging and Publishing
|
|
331
|
+
|
|
332
|
+
See [PUBLISHING.md](PUBLISHING.md) for instructions on how to package and release new versions of the gem.
|
|
333
|
+
|
|
334
|
+
## License
|
|
335
|
+
|
|
336
|
+
[MIT](MIT-LICENSE) © OctaSpace Team
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OctaSpace
|
|
4
|
+
# Main entry point for the OctaSpace SDK
|
|
5
|
+
#
|
|
6
|
+
# Aggregates all resource groups and constructs the appropriate
|
|
7
|
+
# HTTP transport based on configuration.
|
|
8
|
+
#
|
|
9
|
+
# @example Standard mode (default)
|
|
10
|
+
# client = OctaSpace::Client.new(api_key: ENV["OCTA_API_KEY"])
|
|
11
|
+
# client.nodes.list
|
|
12
|
+
# client.accounts.balance
|
|
13
|
+
#
|
|
14
|
+
# @example Keep-alive mode (persistent connections + pool)
|
|
15
|
+
# client = OctaSpace::Client.new(
|
|
16
|
+
# api_key: ENV["OCTA_API_KEY"],
|
|
17
|
+
# keep_alive: true,
|
|
18
|
+
# pool_size: ENV.fetch("RAILS_MAX_THREADS", 5).to_i
|
|
19
|
+
# )
|
|
20
|
+
#
|
|
21
|
+
# @example Multiple API endpoints with failover
|
|
22
|
+
# client = OctaSpace::Client.new(
|
|
23
|
+
# api_key: ENV["OCTA_API_KEY"],
|
|
24
|
+
# base_urls: ["https://api.octa.space", "https://api2.octa.space"]
|
|
25
|
+
# )
|
|
26
|
+
#
|
|
27
|
+
# @example Without API key (public endpoints only)
|
|
28
|
+
# client = OctaSpace::Client.new
|
|
29
|
+
# client.network.info
|
|
30
|
+
#
|
|
31
|
+
# @example With hooks
|
|
32
|
+
# client = OctaSpace::Client.new(
|
|
33
|
+
# api_key: ENV["OCTA_API_KEY"],
|
|
34
|
+
# on_request: ->(req) { puts "→ #{req[:method].upcase} #{req[:path]}" },
|
|
35
|
+
# on_response: ->(resp) { puts "← #{resp.status}" }
|
|
36
|
+
# )
|
|
37
|
+
class Client
|
|
38
|
+
attr_reader :accounts, :nodes, :sessions, :apps,
|
|
39
|
+
:network, :services, :idle_jobs
|
|
40
|
+
|
|
41
|
+
# @param api_key [String, nil] API key for authentication (optional for public endpoints)
|
|
42
|
+
# @param opts [Hash] Per-instance configuration overrides.
|
|
43
|
+
# Any attribute from OctaSpace::Configuration can be passed here.
|
|
44
|
+
def initialize(api_key: nil, transport: nil, **opts)
|
|
45
|
+
@config = build_config(api_key, opts)
|
|
46
|
+
@transport = transport || build_transport
|
|
47
|
+
@accounts = Resources::Accounts.new(@transport)
|
|
48
|
+
@nodes = Resources::Nodes.new(@transport)
|
|
49
|
+
@sessions = Resources::Sessions.new(@transport)
|
|
50
|
+
@apps = Resources::Apps.new(@transport)
|
|
51
|
+
@network = Resources::Network.new(@transport)
|
|
52
|
+
@services = Resources::Services.new(@transport)
|
|
53
|
+
@idle_jobs = Resources::IdleJobs.new(@transport)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Shut down persistent connections (only relevant in keep_alive mode)
|
|
57
|
+
def shutdown
|
|
58
|
+
@transport.respond_to?(:shutdown) && @transport.shutdown
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Transport diagnostics (pool stats when in keep_alive mode)
|
|
62
|
+
# @return [Hash]
|
|
63
|
+
def transport_stats
|
|
64
|
+
if @transport.respond_to?(:transport_stats)
|
|
65
|
+
@transport.transport_stats
|
|
66
|
+
elsif @transport.respond_to?(:pool_stats)
|
|
67
|
+
{mode: :persistent, pools: @transport.pool_stats}
|
|
68
|
+
elsif @config.urls.size > 1
|
|
69
|
+
{mode: :standard, rotator: @transport.instance_variable_get(:@rotator)&.stats}
|
|
70
|
+
else
|
|
71
|
+
{mode: :standard, url: @config.urls.first}
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
# Merge global config with per-instance overrides
|
|
78
|
+
def build_config(api_key, overrides)
|
|
79
|
+
cfg = OctaSpace.configuration.dup
|
|
80
|
+
cfg.api_key = api_key
|
|
81
|
+
overrides.each do |key, value|
|
|
82
|
+
cfg.public_send(:"#{key}=", value) if cfg.respond_to?(:"#{key}=")
|
|
83
|
+
end
|
|
84
|
+
cfg
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def build_transport
|
|
88
|
+
if @config.keep_alive?
|
|
89
|
+
require_persistent_transport!
|
|
90
|
+
Transport::PersistentTransport.new(@config)
|
|
91
|
+
else
|
|
92
|
+
Transport::FaradayTransport.new(@config)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def require_persistent_transport!
|
|
97
|
+
require "faraday/net_http_persistent"
|
|
98
|
+
require "connection_pool"
|
|
99
|
+
require "octaspace/transport/persistent_transport"
|
|
100
|
+
rescue LoadError => e
|
|
101
|
+
raise ConfigurationError,
|
|
102
|
+
"keep_alive: true requires the following gems.\n" \
|
|
103
|
+
"Add to your Gemfile:\n" \
|
|
104
|
+
" gem 'faraday-net_http_persistent'\n" \
|
|
105
|
+
" gem 'connection_pool'\n\n" \
|
|
106
|
+
"Original error: #{e.message}"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OctaSpace
|
|
4
|
+
# Global and per-client configuration for the OctaSpace SDK
|
|
5
|
+
#
|
|
6
|
+
# @example Global configuration (Rails initializer)
|
|
7
|
+
# OctaSpace.configure do |config|
|
|
8
|
+
# config.api_key = ENV["OCTA_API_KEY"]
|
|
9
|
+
# config.keep_alive = true
|
|
10
|
+
# config.pool_size = ENV.fetch("RAILS_MAX_THREADS", 5).to_i
|
|
11
|
+
# config.logger = Rails.logger
|
|
12
|
+
# end
|
|
13
|
+
class Configuration
|
|
14
|
+
# --- Authentication ---
|
|
15
|
+
|
|
16
|
+
# @return [String, nil] API key for Authorization header
|
|
17
|
+
attr_accessor :api_key
|
|
18
|
+
|
|
19
|
+
# --- Connection ---
|
|
20
|
+
|
|
21
|
+
# @return [String] Base URL for the API (single endpoint)
|
|
22
|
+
attr_accessor :base_url
|
|
23
|
+
|
|
24
|
+
# @return [Array<String>, nil] Multiple API endpoints — enables URL rotation/failover
|
|
25
|
+
attr_accessor :base_urls
|
|
26
|
+
|
|
27
|
+
# @return [Integer] Seconds to wait for connection to open
|
|
28
|
+
attr_accessor :open_timeout
|
|
29
|
+
|
|
30
|
+
# @return [Integer] Seconds to wait for a response
|
|
31
|
+
attr_accessor :read_timeout
|
|
32
|
+
|
|
33
|
+
# @return [Integer] Seconds to wait when writing request body
|
|
34
|
+
attr_accessor :write_timeout
|
|
35
|
+
|
|
36
|
+
# --- Keep-Alive / Persistent connections ---
|
|
37
|
+
# Requires: gem "faraday-net_http_persistent" and gem "connection_pool"
|
|
38
|
+
|
|
39
|
+
# @return [Boolean] Enable persistent HTTP connections with connection pooling
|
|
40
|
+
attr_accessor :keep_alive
|
|
41
|
+
|
|
42
|
+
# @return [Integer] Number of persistent connections in the pool
|
|
43
|
+
attr_accessor :pool_size
|
|
44
|
+
|
|
45
|
+
# @return [Integer] Seconds to wait for a connection from the pool
|
|
46
|
+
attr_accessor :pool_timeout
|
|
47
|
+
|
|
48
|
+
# @return [Integer] Seconds before an idle persistent connection is closed
|
|
49
|
+
attr_accessor :idle_timeout
|
|
50
|
+
|
|
51
|
+
# --- Retry ---
|
|
52
|
+
|
|
53
|
+
# @return [Integer] Maximum number of retries on transient failures
|
|
54
|
+
attr_accessor :max_retries
|
|
55
|
+
|
|
56
|
+
# @return [Float] Base interval in seconds between retries
|
|
57
|
+
attr_accessor :retry_interval
|
|
58
|
+
|
|
59
|
+
# @return [Float] Exponential backoff multiplier
|
|
60
|
+
attr_accessor :backoff_factor
|
|
61
|
+
|
|
62
|
+
# --- Hooks ---
|
|
63
|
+
|
|
64
|
+
# @return [#call, nil] Callable invoked before each request; receives request context hash
|
|
65
|
+
attr_accessor :on_request
|
|
66
|
+
|
|
67
|
+
# @return [#call, nil] Callable invoked after each response; receives OctaSpace::Response
|
|
68
|
+
attr_accessor :on_response
|
|
69
|
+
|
|
70
|
+
# --- Logging ---
|
|
71
|
+
|
|
72
|
+
# @return [Logger, nil] Ruby Logger instance (or any object responding to #debug/#info/#warn/#error)
|
|
73
|
+
attr_accessor :logger
|
|
74
|
+
|
|
75
|
+
# @return [Symbol] Log level (:debug, :info, :warn, :error)
|
|
76
|
+
attr_accessor :log_level
|
|
77
|
+
|
|
78
|
+
# --- SSL ---
|
|
79
|
+
|
|
80
|
+
# @return [Boolean] Verify SSL certificates (set false only in development/test)
|
|
81
|
+
attr_accessor :ssl_verify
|
|
82
|
+
|
|
83
|
+
# --- Identity ---
|
|
84
|
+
|
|
85
|
+
# @return [String] User-Agent header value
|
|
86
|
+
attr_accessor :user_agent
|
|
87
|
+
|
|
88
|
+
DEFAULTS = {
|
|
89
|
+
base_url: "https://api.octa.space",
|
|
90
|
+
open_timeout: 10,
|
|
91
|
+
read_timeout: 30,
|
|
92
|
+
write_timeout: 30,
|
|
93
|
+
keep_alive: false,
|
|
94
|
+
pool_size: 5,
|
|
95
|
+
pool_timeout: 5,
|
|
96
|
+
idle_timeout: 60,
|
|
97
|
+
max_retries: 2,
|
|
98
|
+
retry_interval: 0.5,
|
|
99
|
+
backoff_factor: 2.0,
|
|
100
|
+
ssl_verify: true,
|
|
101
|
+
log_level: :info
|
|
102
|
+
}.freeze
|
|
103
|
+
|
|
104
|
+
def initialize
|
|
105
|
+
DEFAULTS.each { |k, v| public_send(:"#{k}=", v) }
|
|
106
|
+
@user_agent = "octaspace-ruby/#{OctaSpace::VERSION} Ruby/#{RUBY_VERSION}"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Alias: `persistent` is the Cube-internal term; `keep_alive` is the public SDK term
|
|
110
|
+
alias_method :persistent, :keep_alive
|
|
111
|
+
alias_method :persistent=, :keep_alive=
|
|
112
|
+
|
|
113
|
+
# @return [Boolean]
|
|
114
|
+
def keep_alive? = !!keep_alive
|
|
115
|
+
|
|
116
|
+
# Returns effective list of API URLs.
|
|
117
|
+
# base_urls takes priority over base_url; always returns an Array.
|
|
118
|
+
# @return [Array<String>]
|
|
119
|
+
def urls
|
|
120
|
+
candidates = Array(base_urls).map(&:to_s).reject(&:empty?)
|
|
121
|
+
candidates.empty? ? Array(base_url).map(&:to_s).reject(&:empty?) : candidates
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Deep-clone configuration for per-client overrides
|
|
125
|
+
# @return [Configuration]
|
|
126
|
+
def dup
|
|
127
|
+
copy = self.class.new
|
|
128
|
+
instance_variables.each do |var|
|
|
129
|
+
copy.instance_variable_set(var, instance_variable_get(var).dup)
|
|
130
|
+
rescue TypeError
|
|
131
|
+
copy.instance_variable_set(var, instance_variable_get(var))
|
|
132
|
+
end
|
|
133
|
+
copy
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|