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:
Mahmoud Emad
2025-01-19 22:20:47 +02:00
parent 03e5a56d62
commit 0d2307acc8
7 changed files with 495 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
!!hero_code.generate_client
name:'runpod'
classname:'RunPod'
singleton:0
default:1
hasconfig:1
reset:0

View 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
```

View 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
}

View 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
}

View 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
// }

View File

@@ -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}") }
}
// 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 {
mut r := []T{}
for item in h.get_json_list(req)! {