feat: add RunPod client
- Add a new RunPod client to the project. - This client allows users to interact with the RunPod API to create and manage pods. - Includes example usage and configuration options.
This commit is contained in:
19
examples/develop/runpod/runpod_example.vsh
Executable file
19
examples/develop/runpod/runpod_example.vsh
Executable file
@@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env -S v -n -w -gc none -no-retry-compilation -cc tcc -d use_openssl -enable-globals run
|
||||||
|
|
||||||
|
// import freeflowuniverse.herolib.core.base
|
||||||
|
import freeflowuniverse.herolib.clients.runpod
|
||||||
|
|
||||||
|
// Example 1: Create client with direct API key
|
||||||
|
mut rp := runpod.get_or_create(
|
||||||
|
name: 'example1'
|
||||||
|
api_key: 'rpa_1G9W44SJM2A70ILYQSPAPEKDCTT181SRZGZK03A22lpazg'
|
||||||
|
)!
|
||||||
|
|
||||||
|
// Create a new pod
|
||||||
|
|
||||||
|
pod_response := rp.create_pod(
|
||||||
|
name: 'test-pod'
|
||||||
|
image_name: 'runpod/tensorflow'
|
||||||
|
)!
|
||||||
|
|
||||||
|
println('Created pod with ID: ${pod_response.id}')
|
||||||
8
lib/clients/runpod/.heroscript
Normal file
8
lib/clients/runpod/.heroscript
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
!!hero_code.generate_client
|
||||||
|
name:'runpod'
|
||||||
|
classname:'RunPod'
|
||||||
|
singleton:0
|
||||||
|
default:1
|
||||||
|
hasconfig:1
|
||||||
|
reset:0
|
||||||
32
lib/clients/runpod/readme.md
Normal file
32
lib/clients/runpod/readme.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# runpod
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
To get started
|
||||||
|
|
||||||
|
```vlang
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import freeflowuniverse.crystallib.clients. runpod
|
||||||
|
|
||||||
|
mut client:= runpod.get()!
|
||||||
|
|
||||||
|
client...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## example heroscript
|
||||||
|
|
||||||
|
|
||||||
|
```hero
|
||||||
|
!!runpod.configure
|
||||||
|
secret: '...'
|
||||||
|
host: 'localhost'
|
||||||
|
port: 8888
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
126
lib/clients/runpod/runpod_factory_.v
Normal file
126
lib/clients/runpod/runpod_factory_.v
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
module runpod
|
||||||
|
|
||||||
|
import freeflowuniverse.herolib.core.base
|
||||||
|
import freeflowuniverse.herolib.core.playbook
|
||||||
|
|
||||||
|
__global (
|
||||||
|
runpod_global map[string]&RunPod
|
||||||
|
runpod_default string
|
||||||
|
)
|
||||||
|
|
||||||
|
/////////FACTORY
|
||||||
|
|
||||||
|
// ArgsGet represents the arguments for getting a RunPod instance
|
||||||
|
@[params]
|
||||||
|
pub struct ArgsGet {
|
||||||
|
pub mut:
|
||||||
|
name string = 'default' // Name of the RunPod configuration
|
||||||
|
api_key string // RunPod API key
|
||||||
|
}
|
||||||
|
|
||||||
|
// get_or_create gets an existing RunPod instance or creates a new one
|
||||||
|
pub fn get_or_create(args_ ArgsGet) !&RunPod {
|
||||||
|
mut args := args_
|
||||||
|
|
||||||
|
if args.name == '' {
|
||||||
|
if runpod_default != '' {
|
||||||
|
args.name = runpod_default
|
||||||
|
} else {
|
||||||
|
args.name = 'default'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return existing instance if available
|
||||||
|
if args.name in runpod_global {
|
||||||
|
return runpod_global[args.name]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load from config if exists
|
||||||
|
mut context := base.context()!
|
||||||
|
if context.hero_config_exists('runpod', args.name) {
|
||||||
|
mut heroscript := context.hero_config_get('runpod', args.name)!
|
||||||
|
play(heroscript: heroscript)!
|
||||||
|
return runpod_global[args.name] or { return error('Failed to load RunPod config') }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new instance if API key provided
|
||||||
|
if args.api_key != '' {
|
||||||
|
mut rp := new(args.api_key)!
|
||||||
|
rp.name = args.name
|
||||||
|
runpod_global[args.name] = rp
|
||||||
|
return rp
|
||||||
|
}
|
||||||
|
|
||||||
|
return error('RunPod API key is required for new instances')
|
||||||
|
}
|
||||||
|
|
||||||
|
// save_config saves the RunPod configuration
|
||||||
|
fn save_config(name string, api_key string) ! {
|
||||||
|
mut context := base.context()!
|
||||||
|
heroscript := "
|
||||||
|
!!runpod.configure
|
||||||
|
name:'${name}'
|
||||||
|
api_key:'${api_key}'
|
||||||
|
"
|
||||||
|
context.hero_config_set('runpod', name, heroscript)!
|
||||||
|
}
|
||||||
|
|
||||||
|
// set stores a RunPod instance in the global map
|
||||||
|
fn set(rp &RunPod) ! {
|
||||||
|
if rp.api_key == '' {
|
||||||
|
return error('RunPod API key is required')
|
||||||
|
}
|
||||||
|
runpod_global[rp.name] = rp
|
||||||
|
save_config(rp.name, rp.api_key)!
|
||||||
|
}
|
||||||
|
|
||||||
|
// PlayArgs represents arguments for playing a RunPod configuration
|
||||||
|
@[params]
|
||||||
|
pub struct PlayArgs {
|
||||||
|
pub mut:
|
||||||
|
name string = 'default'
|
||||||
|
heroscript string // Heroscript configuration
|
||||||
|
plbook ?playbook.PlayBook
|
||||||
|
api_key string // RunPod API key
|
||||||
|
}
|
||||||
|
|
||||||
|
// play processes a RunPod configuration
|
||||||
|
pub fn play(args_ PlayArgs) ! {
|
||||||
|
mut args := args_
|
||||||
|
|
||||||
|
if args.heroscript == '' && args.api_key == '' {
|
||||||
|
return error('Either heroscript or API key is required')
|
||||||
|
}
|
||||||
|
|
||||||
|
// If API key provided directly, create configuration
|
||||||
|
if args.api_key != '' {
|
||||||
|
save_config(args.name, args.api_key)!
|
||||||
|
mut rp := new(args.api_key)!
|
||||||
|
rp.name = args.name
|
||||||
|
set(rp)!
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process heroscript configuration
|
||||||
|
mut plbook := args.plbook or { playbook.new(text: args.heroscript)! }
|
||||||
|
mut actions := plbook.find(filter: 'runpod.configure')!
|
||||||
|
|
||||||
|
if actions.len == 0 {
|
||||||
|
return error('No RunPod configuration found in heroscript')
|
||||||
|
}
|
||||||
|
|
||||||
|
for action in actions {
|
||||||
|
mut params := action.params
|
||||||
|
mut name := params.get_default('name', 'default')!
|
||||||
|
mut api_key := params.get('api_key')!
|
||||||
|
|
||||||
|
mut rp := new(api_key)!
|
||||||
|
rp.name = name
|
||||||
|
set(rp)!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// switch instance to be used for runpod
|
||||||
|
pub fn switch(name string) {
|
||||||
|
runpod_default = name
|
||||||
|
}
|
||||||
156
lib/clients/runpod/runpod_http.v
Normal file
156
lib/clients/runpod/runpod_http.v
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
module runpod
|
||||||
|
|
||||||
|
import freeflowuniverse.herolib.core.httpconnection
|
||||||
|
import json
|
||||||
|
|
||||||
|
fn (mut rp RunPod) httpclient() !&httpconnection.HTTPConnection {
|
||||||
|
mut http_conn := httpconnection.new(
|
||||||
|
name: 'runpod_${rp.name}'
|
||||||
|
url: 'https://api.runpod.io'
|
||||||
|
cache: true
|
||||||
|
retry: 3
|
||||||
|
)!
|
||||||
|
|
||||||
|
// Add authorization header
|
||||||
|
http_conn.default_header.add(.authorization, 'Bearer ${rp.api_key}')
|
||||||
|
return http_conn
|
||||||
|
}
|
||||||
|
|
||||||
|
// Represents the entire mutation and input structure
|
||||||
|
struct PodFindAndDeployOnDemand[T, V] {
|
||||||
|
input T @[json: 'input']
|
||||||
|
response V @[json: 'response']
|
||||||
|
}
|
||||||
|
|
||||||
|
// GraphQL query structs
|
||||||
|
struct GqlQuery {
|
||||||
|
query string
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GqlInput {
|
||||||
|
cloud_type string @[json: 'cloudType']
|
||||||
|
gpu_count int @[json: 'gpuCount']
|
||||||
|
volume_in_gb int @[json: 'volumeInGb']
|
||||||
|
container_disk_in_gb int @[json: 'containerDiskInGb']
|
||||||
|
min_vcpu_count int @[json: 'minVcpuCount']
|
||||||
|
min_memory_in_gb int @[json: 'minMemoryInGb']
|
||||||
|
gpu_type_id string @[json: 'gpuTypeId']
|
||||||
|
name string
|
||||||
|
image_name string @[json: 'imageName']
|
||||||
|
docker_args string @[json: 'dockerArgs']
|
||||||
|
ports string
|
||||||
|
volume_mount_path string @[json: 'volumeMountPath']
|
||||||
|
env []map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// GraphQL response wrapper
|
||||||
|
struct GqlResponse {
|
||||||
|
data GqlResponseData
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GqlResponseData {
|
||||||
|
pod_find_and_deploy_on_demand PodFindAndDeployOnDemandResponse @[json: 'podFindAndDeployOnDemand']
|
||||||
|
}
|
||||||
|
|
||||||
|
fn (mut rp RunPod) get_response_fields[T](response_fields_str_ string, struct_ T) string {
|
||||||
|
mut response_fields_str := response_fields_str_
|
||||||
|
|
||||||
|
// Start the current level
|
||||||
|
response_fields_str += '{'
|
||||||
|
|
||||||
|
$for field in struct_.fields {
|
||||||
|
$if field.is_struct {
|
||||||
|
// Recursively process nested structs
|
||||||
|
response_fields_str += '${field.name}'
|
||||||
|
response_fields_str += ' '
|
||||||
|
response_fields_str += rp.get_response_fields('', struct_.$(field.name))
|
||||||
|
} $else {
|
||||||
|
// Process attributes to fetch the JSON field name or fallback to field name
|
||||||
|
if field.attrs.len > 0 {
|
||||||
|
for attr in field.attrs {
|
||||||
|
attrs := attr.trim_space().split(':')
|
||||||
|
if attrs.len == 2 && attrs[0] == 'json' {
|
||||||
|
response_fields_str += '${attrs[1]}'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
response_fields_str += '${field.name}'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response_fields_str += ' '
|
||||||
|
}
|
||||||
|
// End the current level
|
||||||
|
response_fields_str = response_fields_str.trim_space()
|
||||||
|
response_fields_str += '}'
|
||||||
|
return response_fields_str
|
||||||
|
}
|
||||||
|
|
||||||
|
fn (mut rp RunPod) build_query(request PodFindAndDeployOnDemandRequest, response PodFindAndDeployOnDemandResponse) string {
|
||||||
|
// Convert input to JSON
|
||||||
|
input_json := json.encode(request)
|
||||||
|
|
||||||
|
// Build the GraphQL mutation string
|
||||||
|
response_fields_str := ''
|
||||||
|
mut response_fields := rp.get_response_fields(response_fields_str, response)
|
||||||
|
|
||||||
|
// Wrap the query correctly
|
||||||
|
query := 'mutation { podFindAndDeployOnDemand(input: ${input_json}) ${response_fields} }'
|
||||||
|
|
||||||
|
// Wrap in the final structure
|
||||||
|
gql := GqlQuery{
|
||||||
|
query: query
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the final GraphQL query as a JSON string
|
||||||
|
return json.encode(gql)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum HTTPMethod {
|
||||||
|
get
|
||||||
|
post
|
||||||
|
put
|
||||||
|
delete
|
||||||
|
}
|
||||||
|
|
||||||
|
fn (mut rp RunPod) make_request[T](method HTTPMethod, path string, data string) !T {
|
||||||
|
mut request := httpconnection.Request{
|
||||||
|
prefix: path
|
||||||
|
data: data
|
||||||
|
debug: true
|
||||||
|
dataformat: .json
|
||||||
|
}
|
||||||
|
|
||||||
|
mut http := rp.httpclient()!
|
||||||
|
mut response := T{}
|
||||||
|
|
||||||
|
match method {
|
||||||
|
.get {
|
||||||
|
request.method = .get
|
||||||
|
response = http.get_json_generic[T](request)!
|
||||||
|
}
|
||||||
|
.post {
|
||||||
|
request.method = .post
|
||||||
|
response = http.post_json_generic[T](request)!
|
||||||
|
}
|
||||||
|
.put {
|
||||||
|
request.method = .put
|
||||||
|
response = http.put_json_generic[T](request)!
|
||||||
|
}
|
||||||
|
.delete {
|
||||||
|
request.method = .delete
|
||||||
|
response = http.delete_json_generic[T](request)!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
fn (mut rp RunPod) create_pod_request(request PodFindAndDeployOnDemandRequest) !PodFindAndDeployOnDemandResponse {
|
||||||
|
response_type := PodFindAndDeployOnDemandResponse{}
|
||||||
|
gql := rp.build_query(request, response_type)
|
||||||
|
println('gql: ${gql}')
|
||||||
|
response := rp.make_request[GqlResponse](.post, '/graphql', gql)!
|
||||||
|
println('response: ${json.encode(response)}')
|
||||||
|
return response_type
|
||||||
|
// return response.data.pod_find_and_deploy_on_demand
|
||||||
|
}
|
||||||
140
lib/clients/runpod/runpod_model.v
Normal file
140
lib/clients/runpod/runpod_model.v
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
module runpod
|
||||||
|
|
||||||
|
pub const version = '1.14.3'
|
||||||
|
const singleton = false
|
||||||
|
const default = true
|
||||||
|
|
||||||
|
// heroscript_default returns the default heroscript configuration for RunPod
|
||||||
|
pub fn heroscript_default() !string {
|
||||||
|
return "
|
||||||
|
!!runpod.configure
|
||||||
|
name:'default'
|
||||||
|
api_key:''
|
||||||
|
base_url:'https://api.runpod.io/v1'
|
||||||
|
"
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunPod represents a RunPod client instance
|
||||||
|
@[heap]
|
||||||
|
pub struct RunPod {
|
||||||
|
pub mut:
|
||||||
|
name string = 'default'
|
||||||
|
api_key string
|
||||||
|
base_url string = 'https://api.runpod.io/v1'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input structure for the mutation
|
||||||
|
@[params]
|
||||||
|
pub struct PodFindAndDeployOnDemandRequest {
|
||||||
|
pub mut:
|
||||||
|
cloud_type string = 'ALL' @[json: 'cloudType']
|
||||||
|
gpu_count int = 1 @[json: 'gpuCount']
|
||||||
|
volume_in_gb int = 40 @[json: 'volumeInGb']
|
||||||
|
container_disk_in_gb int = 40 @[json: 'containerDiskInGb']
|
||||||
|
min_vcpu_count int = 2 @[json: 'minVcpuCount']
|
||||||
|
min_memory_in_gb int = 15 @[json: 'minMemoryInGb']
|
||||||
|
gpu_type_id string = 'NVIDIA RTX A6000' @[json: 'gpuTypeId']
|
||||||
|
name string = 'RunPod Tensorflow' @[json: 'name']
|
||||||
|
image_name string = 'runpod/tensorflow' @[json: 'imageName']
|
||||||
|
docker_args string = '' @[json: 'dockerArgs']
|
||||||
|
ports string = '8888/http' @[json: 'ports']
|
||||||
|
volume_mount_path string = '/workspace' @[json: 'volumeMountPath']
|
||||||
|
env []map[string]string = [] @[json: 'env']
|
||||||
|
}
|
||||||
|
|
||||||
|
// Represents the nested machine structure in the response
|
||||||
|
struct Machine {
|
||||||
|
pod_host_id string @[json: 'podHostId']
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response structure for the mutation
|
||||||
|
pub struct PodFindAndDeployOnDemandResponse {
|
||||||
|
pub:
|
||||||
|
id string @[json: 'id']
|
||||||
|
image_name string @[json: 'imageName']
|
||||||
|
env []map[string]string @[json: 'env']
|
||||||
|
machine_id int @[json: 'machineId']
|
||||||
|
machine Machine @[json: 'machine']
|
||||||
|
}
|
||||||
|
|
||||||
|
// new creates a new RunPod client
|
||||||
|
pub fn new(api_key string) !&RunPod {
|
||||||
|
if api_key == '' {
|
||||||
|
return error('API key is required')
|
||||||
|
}
|
||||||
|
return &RunPod{
|
||||||
|
api_key: api_key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create_endpoint creates a new endpoint
|
||||||
|
pub fn (mut rp RunPod) create_pod(pod PodFindAndDeployOnDemandRequest) !PodFindAndDeployOnDemandResponse {
|
||||||
|
response := rp.create_pod_request(pod)!
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
// // list_endpoints lists all endpoints
|
||||||
|
// pub fn (mut rp RunPod) list_endpoints() ![]Endpoint {
|
||||||
|
// response := rp.list_endpoints_request()!
|
||||||
|
// endpoints := json.decode([]Endpoint, response) or {
|
||||||
|
// return error('Failed to parse endpoints from response: ${response}')
|
||||||
|
// }
|
||||||
|
// return endpoints
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // get_endpoint gets an endpoint by ID
|
||||||
|
// pub fn (mut rp RunPod) get_endpoint(id string) !Endpoint {
|
||||||
|
// response := rp.get_endpoint_request(id)!
|
||||||
|
// endpoint := json.decode(Endpoint, response) or {
|
||||||
|
// return error('Failed to parse endpoint from response: ${response}')
|
||||||
|
// }
|
||||||
|
// return endpoint
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // delete_endpoint deletes an endpoint
|
||||||
|
// pub fn (mut rp RunPod) delete_endpoint(id string) ! {
|
||||||
|
// rp.delete_endpoint_request(id)!
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // list_gpu_instances lists available GPU instances
|
||||||
|
// pub fn (mut rp RunPod) list_gpu_instances() ![]GPUInstance {
|
||||||
|
// response := rp.list_gpu_instances_request()!
|
||||||
|
// instances := json.decode([]GPUInstance, response) or {
|
||||||
|
// return error('Failed to parse GPU instances from response: ${response}')
|
||||||
|
// }
|
||||||
|
// return instances
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // run_on_endpoint runs a request on an endpoint
|
||||||
|
// pub fn (mut rp RunPod) run_on_endpoint(endpoint_id string, request RunRequest) !RunResponse {
|
||||||
|
// response := rp.run_on_endpoint_request(endpoint_id, request)!
|
||||||
|
// run_response := json.decode(RunResponse, response) or {
|
||||||
|
// return error('Failed to parse run response: ${response}')
|
||||||
|
// }
|
||||||
|
// return run_response
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // get_run_status gets the status of a run
|
||||||
|
// pub fn (mut rp RunPod) get_run_status(endpoint_id string, run_id string) !RunResponse {
|
||||||
|
// response := rp.get_run_status_request(endpoint_id, run_id)!
|
||||||
|
// run_response := json.decode(RunResponse, response) or {
|
||||||
|
// return error('Failed to parse run status response: ${response}')
|
||||||
|
// }
|
||||||
|
// return run_response
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // cfg_play configures a RunPod instance from heroscript parameters
|
||||||
|
// fn cfg_play(p paramsparser.Params) ! {
|
||||||
|
// mut rp := RunPod{
|
||||||
|
// name: p.get_default('name', 'default')!
|
||||||
|
// api_key: p.get('api_key')!
|
||||||
|
// base_url: p.get_default('base_url', 'https://api.runpod.io/v1')!
|
||||||
|
// }
|
||||||
|
// set(rp)!
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn obj_init(obj_ RunPod) !RunPod {
|
||||||
|
// // never call get here, only thing we can do here is work on object itself
|
||||||
|
// mut obj := obj_
|
||||||
|
// return obj
|
||||||
|
// }
|
||||||
@@ -12,6 +12,20 @@ pub fn (mut h HTTPConnection) post_json_generic[T](req Request) !T {
|
|||||||
return json.decode(T, data) or { return error("couldn't decode json for ${req} for ${data}") }
|
return json.decode(T, data) or { return error("couldn't decode json for ${req} for ${data}") }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
pub fn (mut h HTTPConnection) put_json_generic[T](req Request) !T {
|
||||||
|
// data := h.put_json_str(req)!
|
||||||
|
// return json.decode(T, data) or { return error("couldn't decode json for ${req} for ${data}") }
|
||||||
|
return T{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
pub fn (mut h HTTPConnection) delete_json_generic[T](req Request) !T {
|
||||||
|
// data := h.delete_json_str(req)!
|
||||||
|
// return json.decode(T, data) or { return error("couldn't decode json for ${req} for ${data}") }
|
||||||
|
return T{}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn (mut h HTTPConnection) get_json_list_generic[T](req Request) ![]T {
|
pub fn (mut h HTTPConnection) get_json_list_generic[T](req Request) ![]T {
|
||||||
mut r := []T{}
|
mut r := []T{}
|
||||||
for item in h.get_json_list(req)! {
|
for item in h.get_json_list(req)! {
|
||||||
|
|||||||
Reference in New Issue
Block a user