administrate-sdk 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/LICENSE +21 -0
- data/README.md +493 -0
- data/lib/administrate/base_model.rb +108 -0
- data/lib/administrate/client.rb +27 -0
- data/lib/administrate/configuration.rb +22 -0
- data/lib/administrate/cursor_iterator.rb +41 -0
- data/lib/administrate/errors.rb +42 -0
- data/lib/administrate/models/account.rb +24 -0
- data/lib/administrate/models/api_token.rb +16 -0
- data/lib/administrate/models/client_model.rb +16 -0
- data/lib/administrate/models/execution.rb +12 -0
- data/lib/administrate/models/instance.rb +17 -0
- data/lib/administrate/models/llm_cost.rb +37 -0
- data/lib/administrate/models/llm_project.rb +10 -0
- data/lib/administrate/models/llm_provider.rb +16 -0
- data/lib/administrate/models/pagination_meta.rb +9 -0
- data/lib/administrate/models/sync_run.rb +23 -0
- data/lib/administrate/models/user.rb +14 -0
- data/lib/administrate/models/webhook.rb +11 -0
- data/lib/administrate/models/workflow.rb +18 -0
- data/lib/administrate/page.rb +25 -0
- data/lib/administrate/resources/account.rb +24 -0
- data/lib/administrate/resources/api_tokens.rb +37 -0
- data/lib/administrate/resources/base_resource.rb +37 -0
- data/lib/administrate/resources/clients.rb +49 -0
- data/lib/administrate/resources/executions.rb +28 -0
- data/lib/administrate/resources/instances.rb +61 -0
- data/lib/administrate/resources/llm_costs.rb +70 -0
- data/lib/administrate/resources/llm_projects.rb +20 -0
- data/lib/administrate/resources/llm_providers.rb +47 -0
- data/lib/administrate/resources/sync_runs.rb +30 -0
- data/lib/administrate/resources/users.rb +33 -0
- data/lib/administrate/resources/webhooks.rb +43 -0
- data/lib/administrate/resources/workflows.rb +29 -0
- data/lib/administrate/transport.rb +101 -0
- data/lib/administrate/version.rb +5 -0
- data/lib/administrate-sdk.rb +3 -0
- data/lib/administrate.rb +42 -0
- metadata +123 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: bf5605805daacbe02a2d9c84fba5cc4c3e6fa7b3d690087e80664b959a0c7c3d
|
|
4
|
+
data.tar.gz: 2053517211d59122b09bddf04f4c2cbd37cc8076cebfd2a6adf2d5362400ca32
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: eedd337177788041d4f65ef2a45ce183de4e7573e86057a0f323fc72c341825fb4205ad02516406a95d34ce4e8bd19a7fbf6f1315fea3eb7af25bf6582799522
|
|
7
|
+
data.tar.gz: dd2f7c9016e94de95a39e9d519e4ce97025eb18dd8368be029244e6973eb87872dbcc13ab891fee2b13af32e4bca496a0bdc8cc1d381038d381e57962552ea2d
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Administrate
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
# Administrate Ruby SDK
|
|
2
|
+
|
|
3
|
+
The official Ruby SDK for the [Administrate.dev](https://administrate.dev) REST API.
|
|
4
|
+
|
|
5
|
+
[Administrate.dev](https://administrate.dev) is AI Agency Management software. It is a monitoring and management platform for AI agencies running n8n and AI automation workflows across multiple clients. It provides a single dashboard to track every workflow, every client, every failure, and all LLM costs, so you can catch problems before clients do and prove the value of your automations.
|
|
6
|
+
|
|
7
|
+
**Key platform features:**
|
|
8
|
+
|
|
9
|
+
- **Multi-instance monitoring** -- See all n8n instances across every client in one place
|
|
10
|
+
- **Error tracking** -- Real-time failure detection with automatic error categorization
|
|
11
|
+
- **LLM cost tracking** -- Connect OpenAI, Anthropic, Azure, and OpenRouter accounts to attribute costs to specific clients
|
|
12
|
+
- **Workflow insights** -- Execution counts, success rates, and time-saved ROI reporting
|
|
13
|
+
- **Sync health** -- Know instantly when a data sync fails
|
|
14
|
+
- **Webhooks & API** -- Full programmatic access for custom integrations
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
Add to your Gemfile:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
gem 'administrate-sdk'
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Then run:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
bundle install
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or install directly:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
gem install administrate-sdk
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Requires Ruby 3.1+.
|
|
37
|
+
|
|
38
|
+
## Quick start
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
require 'administrate-sdk'
|
|
42
|
+
|
|
43
|
+
client = Administrate.new(api_key: 'sk_live_...')
|
|
44
|
+
|
|
45
|
+
# Get account info
|
|
46
|
+
account = client.account.get
|
|
47
|
+
puts "#{account.name} (#{account.plan})"
|
|
48
|
+
|
|
49
|
+
# List all clients with auto-pagination
|
|
50
|
+
client.clients.list.each do |c|
|
|
51
|
+
puts "#{c.name} (#{c.code})"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Check for failed executions across all instances
|
|
55
|
+
client.executions.list(errors_only: true).each do |execution|
|
|
56
|
+
puts "#{execution.workflow_name}: #{execution.error_category}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Get LLM cost summary
|
|
60
|
+
costs = client.llm_costs.summary
|
|
61
|
+
puts "Total: $#{'%.2f' % (costs.data.summary.total_cost_cents / 100.0)}"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Configuration
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
require 'administrate-sdk'
|
|
68
|
+
|
|
69
|
+
client = Administrate.new(
|
|
70
|
+
api_key: 'sk_live_...', # Required. Must start with "sk_live_"
|
|
71
|
+
base_url: 'https://...', # Default: "https://administrate.dev"
|
|
72
|
+
timeout: 30, # Request timeout in seconds. Default: 30
|
|
73
|
+
max_retries: 3 # Retry attempts for failed requests. Default: 3
|
|
74
|
+
)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The SDK uses [Faraday](https://lostisland.github.io/faraday/) under the hood. Retry and backoff are handled automatically via the `faraday-retry` middleware.
|
|
78
|
+
|
|
79
|
+
## API reference
|
|
80
|
+
|
|
81
|
+
All API keys are created in **Settings > Developers** within Administrate.dev. Tokens have three permission levels: `read`, `write`, and `full`.
|
|
82
|
+
|
|
83
|
+
### Account
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
# Get current token info and account summary
|
|
87
|
+
me = client.account.me
|
|
88
|
+
puts "#{me.token.name} (#{me.token.permission})"
|
|
89
|
+
puts "#{me.account.name} (#{me.account.plan})"
|
|
90
|
+
|
|
91
|
+
# Get full account details
|
|
92
|
+
account = client.account.get
|
|
93
|
+
|
|
94
|
+
# Update account settings
|
|
95
|
+
account = client.account.update(
|
|
96
|
+
name: 'My Agency',
|
|
97
|
+
billing_email: 'billing@example.com',
|
|
98
|
+
timezone: 'Australia/Brisbane'
|
|
99
|
+
)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Clients
|
|
103
|
+
|
|
104
|
+
Clients represent the companies you manage automations for.
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
# List all clients (auto-paginates)
|
|
108
|
+
client.clients.list.each do |c|
|
|
109
|
+
puts "#{c.name} (#{c.code})"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Get a client (includes 7-day metrics)
|
|
113
|
+
c = client.clients.get('com_abc123')
|
|
114
|
+
puts "#{c.metrics.success_rate}% success, #{c.metrics.time_saved_minutes} min saved"
|
|
115
|
+
|
|
116
|
+
# Create a client
|
|
117
|
+
c = client.clients.create(
|
|
118
|
+
name: 'Acme Corp',
|
|
119
|
+
code: 'acme',
|
|
120
|
+
contact_email: 'ops@acme.com',
|
|
121
|
+
timezone: 'America/New_York'
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Update a client
|
|
125
|
+
c = client.clients.update('com_abc123', notes: 'Enterprise tier')
|
|
126
|
+
|
|
127
|
+
# Delete a client (requires full permission)
|
|
128
|
+
client.clients.delete('com_abc123')
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Instances
|
|
132
|
+
|
|
133
|
+
Instances are n8n deployments connected to Administrate.
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
# List all instances
|
|
137
|
+
client.instances.list.each do |inst|
|
|
138
|
+
puts "#{inst.name} (#{inst.sync_status})"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Filter by client or sync status
|
|
142
|
+
client.instances.list(client_id: 'com_abc123', sync_status: 'error').each do |inst|
|
|
143
|
+
puts "#{inst.name}: #{inst.last_sync_error}"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Get an instance (includes 7-day metrics)
|
|
147
|
+
inst = client.instances.get('n8n_abc123')
|
|
148
|
+
puts "#{inst.metrics.executions_count} executions, #{inst.metrics.success_rate}% success"
|
|
149
|
+
|
|
150
|
+
# Connect a new n8n instance
|
|
151
|
+
inst = client.instances.create(
|
|
152
|
+
client_id: 'com_abc123',
|
|
153
|
+
name: 'Production n8n',
|
|
154
|
+
base_url: 'https://n8n.acme.com',
|
|
155
|
+
api_key: 'n8n_api_key_here'
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Trigger a sync
|
|
159
|
+
client.instances.sync('n8n_abc123', sync_type: 'all')
|
|
160
|
+
|
|
161
|
+
# Sync all instances at once
|
|
162
|
+
client.instances.sync_all(sync_type: 'workflows')
|
|
163
|
+
|
|
164
|
+
# Update an instance
|
|
165
|
+
inst = client.instances.update('n8n_abc123', name: 'Staging n8n')
|
|
166
|
+
|
|
167
|
+
# Delete an instance
|
|
168
|
+
client.instances.delete('n8n_abc123')
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Workflows
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
# List workflows with filters
|
|
175
|
+
client.workflows.list(client_id: 'com_abc123', active: true).each do |wf|
|
|
176
|
+
puts "#{wf.name} (active: #{wf.is_active})"
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Search by name
|
|
180
|
+
client.workflows.list(search: 'onboarding').each do |wf|
|
|
181
|
+
puts wf.name
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Get a workflow (includes 7-day metrics)
|
|
185
|
+
wf = client.workflows.get('wfl_abc123')
|
|
186
|
+
puts "#{wf.metrics.success_rate}% success, #{wf.metrics.time_saved_minutes} min saved"
|
|
187
|
+
|
|
188
|
+
# Set time-saved estimates (for ROI reporting)
|
|
189
|
+
wf = client.workflows.update(
|
|
190
|
+
'wfl_abc123',
|
|
191
|
+
minutes_saved_per_success: 15,
|
|
192
|
+
minutes_saved_per_failure: 5
|
|
193
|
+
)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Executions
|
|
197
|
+
|
|
198
|
+
Executions are read-only records of workflow runs synced from n8n.
|
|
199
|
+
|
|
200
|
+
```ruby
|
|
201
|
+
# List executions with filters
|
|
202
|
+
client.executions.list(
|
|
203
|
+
client_id: 'com_abc123',
|
|
204
|
+
status: 'failed',
|
|
205
|
+
start_date: '2025-01-01',
|
|
206
|
+
end_date: '2025-01-31'
|
|
207
|
+
).each do |ex|
|
|
208
|
+
puts "#{ex.workflow_name} - #{ex.status} (#{ex.duration_ms}ms)"
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Get only errors
|
|
212
|
+
client.executions.list(errors_only: true).each do |ex|
|
|
213
|
+
puts "#{ex.error_category}: #{ex.workflow_name}"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Get execution details (includes error message and payload)
|
|
217
|
+
ex = client.executions.get('exe_abc123')
|
|
218
|
+
puts ex.error_message
|
|
219
|
+
puts ex.error_payload
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Sync runs
|
|
223
|
+
|
|
224
|
+
```ruby
|
|
225
|
+
# List sync run history
|
|
226
|
+
client.sync_runs.list(instance_id: 'n8n_abc123', status: 'failed').each do |run|
|
|
227
|
+
puts "#{run.sync_type} - #{run.status} (#{run.duration_seconds}s)"
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Get a specific sync run
|
|
231
|
+
run = client.sync_runs.get('syn_abc123')
|
|
232
|
+
|
|
233
|
+
# Get sync health across all instances
|
|
234
|
+
client.sync_runs.health.each do |entry|
|
|
235
|
+
puts "#{entry.instance_name} (#{entry.sync_status})"
|
|
236
|
+
puts " Workflows last synced: #{entry.workflows.last_synced_at}"
|
|
237
|
+
puts " Executions last synced: #{entry.executions.last_synced_at}"
|
|
238
|
+
end
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Users
|
|
242
|
+
|
|
243
|
+
```ruby
|
|
244
|
+
# List team members
|
|
245
|
+
client.users.list.each do |user|
|
|
246
|
+
puts "#{user.name} <#{user.email}> (#{user.role})"
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Get a user
|
|
250
|
+
user = client.users.get('usr_abc123')
|
|
251
|
+
|
|
252
|
+
# Invite a new team member
|
|
253
|
+
invitation = client.users.invite(email: 'new@example.com', role: 'member')
|
|
254
|
+
puts invitation.expires_at
|
|
255
|
+
|
|
256
|
+
# Change a user's role
|
|
257
|
+
user = client.users.update('usr_abc123', role: 'admin')
|
|
258
|
+
|
|
259
|
+
# Remove a user
|
|
260
|
+
client.users.delete('usr_abc123')
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Webhooks
|
|
264
|
+
|
|
265
|
+
```ruby
|
|
266
|
+
# List webhooks
|
|
267
|
+
client.webhooks.list.each do |wh|
|
|
268
|
+
puts "#{wh.url} - #{wh.events.join(', ')} (enabled: #{wh.enabled})"
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Create a webhook
|
|
272
|
+
wh = client.webhooks.create(
|
|
273
|
+
url: 'https://example.com/hook',
|
|
274
|
+
events: ['execution.failed', 'sync.failed'],
|
|
275
|
+
description: 'Slack failure alerts'
|
|
276
|
+
)
|
|
277
|
+
puts wh.secret # Save this -- used to verify webhook signatures
|
|
278
|
+
|
|
279
|
+
# Update a webhook
|
|
280
|
+
wh = client.webhooks.update('whk_abc123', enabled: false)
|
|
281
|
+
|
|
282
|
+
# Regenerate the signing secret (old secret becomes invalid immediately)
|
|
283
|
+
wh = client.webhooks.regenerate_secret('whk_abc123')
|
|
284
|
+
puts wh.secret
|
|
285
|
+
|
|
286
|
+
# Delete a webhook
|
|
287
|
+
client.webhooks.delete('whk_abc123')
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### API tokens
|
|
291
|
+
|
|
292
|
+
```ruby
|
|
293
|
+
# List all tokens
|
|
294
|
+
client.api_tokens.list.each do |token|
|
|
295
|
+
puts "#{token.name} (#{token.permission}) - #{token.token_hint}"
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# Create a token (the plain token is only returned once)
|
|
299
|
+
token = client.api_tokens.create(
|
|
300
|
+
name: 'CI/CD Pipeline',
|
|
301
|
+
permission: 'read',
|
|
302
|
+
ip_allowlist: ['10.0.0.0/8'],
|
|
303
|
+
expires_in: '90_days'
|
|
304
|
+
)
|
|
305
|
+
puts token.token # sk_live_... -- save this immediately
|
|
306
|
+
|
|
307
|
+
# Update a token
|
|
308
|
+
token = client.api_tokens.update('tok_abc123', name: 'Updated Name')
|
|
309
|
+
|
|
310
|
+
# Revoke a token
|
|
311
|
+
client.api_tokens.delete('tok_abc123')
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### LLM providers
|
|
315
|
+
|
|
316
|
+
Connect your AI provider accounts to track costs.
|
|
317
|
+
|
|
318
|
+
```ruby
|
|
319
|
+
# List providers
|
|
320
|
+
client.llm_providers.list.each do |provider|
|
|
321
|
+
puts "#{provider.name} (#{provider.provider_type}) - #{provider.sync_status}"
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Get a provider (includes 7-day metrics)
|
|
325
|
+
provider = client.llm_providers.get('llm_abc123')
|
|
326
|
+
puts "#{provider.metrics.total_cost_cents}c, #{provider.metrics.total_tokens} tokens"
|
|
327
|
+
|
|
328
|
+
# Connect a new provider
|
|
329
|
+
provider = client.llm_providers.create(
|
|
330
|
+
name: 'OpenAI Production',
|
|
331
|
+
provider_type: 'openai', # openai, anthropic, openrouter, or azure
|
|
332
|
+
api_key: 'sk-...',
|
|
333
|
+
organization_id: 'org-...'
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# Trigger a cost sync
|
|
337
|
+
client.llm_providers.sync('llm_abc123')
|
|
338
|
+
|
|
339
|
+
# Update a provider
|
|
340
|
+
provider = client.llm_providers.update('llm_abc123', name: 'OpenAI Staging')
|
|
341
|
+
|
|
342
|
+
# Delete a provider
|
|
343
|
+
client.llm_providers.delete('llm_abc123')
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### LLM projects
|
|
347
|
+
|
|
348
|
+
Projects are discovered automatically when syncing a provider. Assign them to clients to attribute costs.
|
|
349
|
+
|
|
350
|
+
```ruby
|
|
351
|
+
# List projects for a provider
|
|
352
|
+
client.llm_projects.list('llm_abc123').each do |project|
|
|
353
|
+
puts "#{project.name}: #{project.total_cost_cents}c (#{project.client_name})"
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Assign a project to a client
|
|
357
|
+
project = client.llm_projects.update(
|
|
358
|
+
'llm_abc123', 'proj_456', client_id: 'com_abc123'
|
|
359
|
+
)
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### LLM costs
|
|
363
|
+
|
|
364
|
+
```ruby
|
|
365
|
+
# Get cost summary (defaults to last 7 days)
|
|
366
|
+
costs = client.llm_costs.summary
|
|
367
|
+
puts "Total: $#{'%.2f' % (costs.data.summary.total_cost_cents / 100.0)}"
|
|
368
|
+
puts "Tokens: #{costs.data.summary.total_tokens}"
|
|
369
|
+
|
|
370
|
+
# Breakdown by provider
|
|
371
|
+
costs.data.providers.each do |p|
|
|
372
|
+
puts " #{p.name}: $#{'%.2f' % (p.cost_cents / 100.0)}"
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# Breakdown by model
|
|
376
|
+
costs.data.models.each do |m|
|
|
377
|
+
puts " #{m.model}: $#{'%.2f' % (m.cost_cents / 100.0)}"
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Daily trend
|
|
381
|
+
costs.data.daily.each do |day|
|
|
382
|
+
puts " #{day.date}: $#{'%.2f' % (day.cost_cents / 100.0)}"
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
# Custom date range
|
|
386
|
+
costs = client.llm_costs.summary(
|
|
387
|
+
start_date: '2025-01-01',
|
|
388
|
+
end_date: '2025-01-31'
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
# Costs by client
|
|
392
|
+
client.llm_costs.by_client.data.each do |entry|
|
|
393
|
+
puts "#{entry.name}: $#{'%.2f' % (entry.cost_cents / 100.0)}"
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# Costs by provider
|
|
397
|
+
client.llm_costs.by_provider.data.each do |entry|
|
|
398
|
+
puts "#{entry.name}: $#{'%.2f' % (entry.cost_cents / 100.0)}"
|
|
399
|
+
end
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
## Pagination
|
|
403
|
+
|
|
404
|
+
All `.list` methods return an `Enumerable` iterator that handles pagination automatically. By default, the API returns 25 items per page (max 100).
|
|
405
|
+
|
|
406
|
+
```ruby
|
|
407
|
+
# Auto-paginate through all results
|
|
408
|
+
client.clients.list.each do |c|
|
|
409
|
+
puts c.name
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# Control page size
|
|
413
|
+
client.clients.list(per_page: 100).each do |c|
|
|
414
|
+
puts c.name
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# Get a single page
|
|
418
|
+
page = client.clients.list(per_page: 10).first_page
|
|
419
|
+
puts "#{page.meta.total} total, #{page.meta.total_pages} pages"
|
|
420
|
+
page.each do |c|
|
|
421
|
+
puts c.name
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# Use any Enumerable method
|
|
425
|
+
names = client.clients.list.map(&:name)
|
|
426
|
+
active = client.workflows.list.select(&:is_active)
|
|
427
|
+
first_five = client.users.list.first(5)
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
## Error handling
|
|
431
|
+
|
|
432
|
+
The SDK raises typed exceptions for all API errors:
|
|
433
|
+
|
|
434
|
+
```ruby
|
|
435
|
+
require 'administrate-sdk'
|
|
436
|
+
|
|
437
|
+
client = Administrate.new(api_key: 'sk_live_...')
|
|
438
|
+
|
|
439
|
+
begin
|
|
440
|
+
c = client.clients.get('com_nonexistent')
|
|
441
|
+
rescue Administrate::NotFoundError => e
|
|
442
|
+
puts "Not found: #{e.message}"
|
|
443
|
+
rescue Administrate::AuthenticationError
|
|
444
|
+
puts 'Invalid API key'
|
|
445
|
+
rescue Administrate::RateLimitError => e
|
|
446
|
+
puts "Rate limited. Retry after #{e.retry_after}s"
|
|
447
|
+
rescue Administrate::ValidationError => e
|
|
448
|
+
puts "Invalid params: #{e.body}"
|
|
449
|
+
rescue Administrate::APIError => e
|
|
450
|
+
puts "API error #{e.status_code}: #{e.message}"
|
|
451
|
+
end
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
**Exception hierarchy:**
|
|
455
|
+
|
|
456
|
+
| Exception | Status code | Description |
|
|
457
|
+
|---|---|---|
|
|
458
|
+
| `Administrate::Error` | -- | Base exception for all SDK errors |
|
|
459
|
+
| `Administrate::APIError` | Any non-2xx | Base for all HTTP API errors |
|
|
460
|
+
| `Administrate::AuthenticationError` | 401 | Invalid or missing API key |
|
|
461
|
+
| `Administrate::PermissionDeniedError` | 403 | Insufficient token permissions |
|
|
462
|
+
| `Administrate::NotFoundError` | 404 | Resource does not exist |
|
|
463
|
+
| `Administrate::ValidationError` | 422 | Invalid request parameters |
|
|
464
|
+
| `Administrate::RateLimitError` | 429 | Rate limit exceeded (has `retry_after`) |
|
|
465
|
+
| `Administrate::InternalServerError` | 5xx | Server-side error |
|
|
466
|
+
| `Administrate::ConnectionError` | -- | Failed to connect to the API |
|
|
467
|
+
| `Administrate::TimeoutError` | -- | Request timed out |
|
|
468
|
+
|
|
469
|
+
All `APIError` subclasses expose `status_code`, `response` (the raw Faraday response), and `body` (parsed JSON or text).
|
|
470
|
+
|
|
471
|
+
## Retries
|
|
472
|
+
|
|
473
|
+
The SDK automatically retries failed requests with exponential backoff:
|
|
474
|
+
|
|
475
|
+
- **429 (rate limited)** -- Retries after the duration specified in the `Retry-After` header
|
|
476
|
+
- **5xx (server errors)** -- Retries with exponential backoff (0.5s, 1s, 2s, ...)
|
|
477
|
+
- **Connection errors and timeouts** -- Retried with the same backoff schedule
|
|
478
|
+
|
|
479
|
+
By default, the SDK retries up to 3 times. Set `max_retries: 0` to disable:
|
|
480
|
+
|
|
481
|
+
```ruby
|
|
482
|
+
client = Administrate.new(api_key: 'sk_live_...', max_retries: 0)
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
## Requirements
|
|
486
|
+
|
|
487
|
+
- Ruby 3.1+
|
|
488
|
+
- [faraday](https://lostisland.github.io/faraday/) >= 2.0
|
|
489
|
+
- [faraday-retry](https://github.com/lostisland/faraday-retry) >= 2.0
|
|
490
|
+
|
|
491
|
+
## License
|
|
492
|
+
|
|
493
|
+
MIT
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Administrate
|
|
4
|
+
class BaseModel
|
|
5
|
+
class << self
|
|
6
|
+
def attribute(*names)
|
|
7
|
+
@attributes ||= []
|
|
8
|
+
names.each do |name|
|
|
9
|
+
@attributes << name.to_sym
|
|
10
|
+
attr_reader name
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def nested(name, klass, array: false)
|
|
15
|
+
@nested_fields ||= {}
|
|
16
|
+
@nested_fields[name.to_sym] = { klass: klass, array: array }
|
|
17
|
+
attr_reader name
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def attributes
|
|
21
|
+
@attributes || []
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def nested_fields
|
|
25
|
+
@nested_fields || {}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def inherited(subclass)
|
|
29
|
+
super
|
|
30
|
+
subclass.instance_variable_set(:@attributes, (attributes || []).dup)
|
|
31
|
+
subclass.instance_variable_set(:@nested_fields, (nested_fields || {}).dup)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
attr_reader :extra
|
|
36
|
+
|
|
37
|
+
def initialize(data = {})
|
|
38
|
+
data = data.transform_keys(&:to_sym) if data.is_a?(Hash)
|
|
39
|
+
known_keys = self.class.attributes + self.class.nested_fields.keys
|
|
40
|
+
@extra = {}
|
|
41
|
+
|
|
42
|
+
known_keys.each do |key|
|
|
43
|
+
value = data[key]
|
|
44
|
+
nested_config = self.class.nested_fields[key]
|
|
45
|
+
|
|
46
|
+
if nested_config && !value.nil?
|
|
47
|
+
value = if nested_config[:array]
|
|
48
|
+
Array(value).map { |item| item.is_a?(Hash) ? nested_config[:klass].new(item) : item }
|
|
49
|
+
elsif value.is_a?(Hash)
|
|
50
|
+
nested_config[:klass].new(value)
|
|
51
|
+
else
|
|
52
|
+
value
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
instance_variable_set(:"@#{key}", value)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
data.each do |key, value|
|
|
60
|
+
@extra[key] = value unless known_keys.include?(key.to_sym)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def to_h
|
|
65
|
+
result = {}
|
|
66
|
+
(self.class.attributes + self.class.nested_fields.keys).each do |key|
|
|
67
|
+
value = instance_variable_get(:"@#{key}")
|
|
68
|
+
result[key] = serialize_value(value)
|
|
69
|
+
end
|
|
70
|
+
result.merge(@extra)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def [](key)
|
|
74
|
+
key = key.to_sym
|
|
75
|
+
if (self.class.attributes + self.class.nested_fields.keys).include?(key)
|
|
76
|
+
instance_variable_get(:"@#{key}")
|
|
77
|
+
else
|
|
78
|
+
@extra[key]
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def ==(other)
|
|
83
|
+
return false unless other.is_a?(self.class)
|
|
84
|
+
|
|
85
|
+
to_h == other.to_h
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def inspect
|
|
89
|
+
attrs = (self.class.attributes + self.class.nested_fields.keys).map do |key|
|
|
90
|
+
"#{key}=#{instance_variable_get(:"@#{key}").inspect}"
|
|
91
|
+
end
|
|
92
|
+
"#<#{self.class.name} #{attrs.join(', ')}>"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def serialize_value(value)
|
|
98
|
+
case value
|
|
99
|
+
when BaseModel
|
|
100
|
+
value.to_h
|
|
101
|
+
when Array
|
|
102
|
+
value.map { |item| serialize_value(item) }
|
|
103
|
+
else
|
|
104
|
+
value
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Administrate
|
|
4
|
+
class Client
|
|
5
|
+
attr_reader :account, :users, :webhooks, :api_tokens, :clients,
|
|
6
|
+
:instances, :workflows, :executions, :sync_runs,
|
|
7
|
+
:llm_providers, :llm_projects, :llm_costs
|
|
8
|
+
|
|
9
|
+
def initialize(api_key:, base_url: DEFAULT_BASE_URL, timeout: DEFAULT_TIMEOUT, max_retries: DEFAULT_MAX_RETRIES)
|
|
10
|
+
config = Configuration.new(api_key: api_key, base_url: base_url, timeout: timeout, max_retries: max_retries)
|
|
11
|
+
transport = Transport.new(config)
|
|
12
|
+
|
|
13
|
+
@account = Resources::Account.new(transport)
|
|
14
|
+
@users = Resources::Users.new(transport)
|
|
15
|
+
@webhooks = Resources::Webhooks.new(transport)
|
|
16
|
+
@api_tokens = Resources::ApiTokens.new(transport)
|
|
17
|
+
@clients = Resources::Clients.new(transport)
|
|
18
|
+
@instances = Resources::Instances.new(transport)
|
|
19
|
+
@workflows = Resources::Workflows.new(transport)
|
|
20
|
+
@executions = Resources::Executions.new(transport)
|
|
21
|
+
@sync_runs = Resources::SyncRuns.new(transport)
|
|
22
|
+
@llm_providers = Resources::LlmProviders.new(transport)
|
|
23
|
+
@llm_projects = Resources::LlmProjects.new(transport)
|
|
24
|
+
@llm_costs = Resources::LlmCosts.new(transport)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Administrate
|
|
4
|
+
DEFAULT_BASE_URL = 'https://administrate.dev'
|
|
5
|
+
API_VERSION = 'v1'
|
|
6
|
+
DEFAULT_TIMEOUT = 30
|
|
7
|
+
DEFAULT_MAX_RETRIES = 3
|
|
8
|
+
|
|
9
|
+
class Configuration
|
|
10
|
+
attr_reader :api_key, :base_url, :timeout, :max_retries
|
|
11
|
+
|
|
12
|
+
def initialize(api_key:, base_url: DEFAULT_BASE_URL, timeout: DEFAULT_TIMEOUT, max_retries: DEFAULT_MAX_RETRIES)
|
|
13
|
+
raise Administrate::Error, 'api_key is required' if api_key.nil? || api_key.empty?
|
|
14
|
+
raise Administrate::Error, "api_key must start with 'sk_live_'" unless api_key.start_with?('sk_live_')
|
|
15
|
+
|
|
16
|
+
@api_key = api_key
|
|
17
|
+
@base_url = base_url
|
|
18
|
+
@timeout = timeout
|
|
19
|
+
@max_retries = max_retries
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|