refactor: improve RunPod client

- Refactor RunPod client to use generics for requests and
responses.
- This improves code readability and maintainability.
- Remove redundant code for building GraphQL queries and
handling HTTP requests.
- Add support for environment variables in pod creation.
- Update example with new API key and environment variables.

Co-authored-by: supermario <mariobassem12@gmail.com>
This commit is contained in:
Mahmoud Emad
2025-01-20 15:01:27 +02:00
parent 0d2307acc8
commit d54a1e5a34
5 changed files with 180 additions and 120 deletions

View File

@@ -6,14 +6,16 @@ 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'
api_key: 'rpa_JDYDWBS0PDTC55T1BYT1PX85CL4D5YEBZ48LETRXyf4gxr'
)!
// Create a new pod
pod_response := rp.create_pod(
name: 'test-pod'
name: 'RunPod Tensorflow'
image_name: 'runpod/tensorflow'
env: [
{"JUPYTER_PASSWORD": "rn51hunbpgtltcpac3ol"}
]
)!
println('Created pod with ID: ${pod_response.id}')

View File

@@ -27,22 +27,6 @@ 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
@@ -52,105 +36,11 @@ 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)
fn (mut rp RunPod) create_pod_request[T, R](request T, response R) !R {
gql := rp.build_query[T, R](request, response)
println('gql: ${gql}')
response := rp.make_request[GqlResponse](.post, '/graphql', gql)!
println('response: ${json.encode(response)}')
return response_type
response_ := rp.make_request[GqlResponse](.post, '/graphql', gql)!
println('response: ${json.encode(response_)}')
return response
// return response.data.pod_find_and_deploy_on_demand
}

View File

@@ -23,11 +23,31 @@ pub mut:
base_url string = 'https://api.runpod.io/v1'
}
enum CloudType {
all
secure
community
}
fn (ct CloudType) to_string() string {
return match ct {
.all {
'ALL'
}
.secure {
'SECURE'
}
.community {
'COMMUNITY'
}
}
}
// Input structure for the mutation
@[params]
pub struct PodFindAndDeployOnDemandRequest {
pub mut:
cloud_type string = 'ALL' @[json: 'cloudType']
cloud_type CloudType = .all @[json: 'cloudType']
gpu_count int = 1 @[json: 'gpuCount']
volume_in_gb int = 40 @[json: 'volumeInGb']
container_disk_in_gb int = 40 @[json: 'containerDiskInGb']
@@ -69,7 +89,10 @@ pub fn new(api_key string) !&RunPod {
// create_endpoint creates a new endpoint
pub fn (mut rp RunPod) create_pod(pod PodFindAndDeployOnDemandRequest) !PodFindAndDeployOnDemandResponse {
response := rp.create_pod_request(pod)!
response_type := PodFindAndDeployOnDemandResponse{}
request_type := pod
response := rp.create_pod_request[PodFindAndDeployOnDemandRequest, PodFindAndDeployOnDemandResponse](request_type,
response_type)!
return response
}

144
lib/clients/runpod/utils.v Normal file
View File

@@ -0,0 +1,144 @@
module runpod
import freeflowuniverse.herolib.core.httpconnection
import json
fn (mut rp RunPod) get_field_name(field FieldData) string {
mut field_name := ''
// 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' {
field_name = attrs[1].trim_space()
continue
}
}
} else {
field_name = field.name
}
return field_name
}
fn (mut rp RunPod) get_request_fields[T](struct_ T) string {
// Start the current level
mut body_ := '{ '
mut fields := []string{}
$for field in T.fields {
mut string_ := ''
string_ += rp.get_field_name(field)
string_ += ': '
$if field.is_enum {
string_ += struct_.$(field.name).to_string()
}
$if field.typ is string {
item := struct_.$(field.name)
string_ += "\"${item}\""
}
$if field.typ is int {
item := struct_.$(field.name)
string_ += '${item}'
}
// TODO: Loop only on the env map
$if field.is_array {
for i in struct_.$(field.name) {
for k, v in i {
string_ += '[{ '
string_ += "key: \"${k}\", value: \"${v}\""
string_ += ' }]'
}
}
}
$if field.is_struct {
rp.get_request_fields(struct_.$(field.name))
}
fields << string_
}
body_ += fields.join(', ')
body_ += ' }'
return body_
}
fn (mut rp RunPod) get_response_fields[R](struct_ R) string {
// Start the current level
mut body_ := '{ '
$for field in R.fields {
$if field.is_struct {
// Recursively process nested structs
body_ += '${field.name} '
body_ += rp.get_response_fields(struct_.$(field.name))
} $else {
body_ += rp.get_field_name(field)
body_ += ' '
}
}
body_ += ' }'
return body_
}
fn (mut rp RunPod) build_query[T, R](request T, response R) string {
// Convert input to JSON
// input_json := json.encode(request)
// Build the GraphQL mutation string
mut request_fields := rp.get_request_fields(request)
mut response_fields := rp.get_response_fields(response)
// Wrap the query correctly
query := 'mutation { podFindAndDeployOnDemand(input: ${request_fields}) ${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
}

View File

@@ -9,6 +9,7 @@ pub fn (mut h HTTPConnection) get_json_generic[T](req Request) !T {
pub fn (mut h HTTPConnection) post_json_generic[T](req Request) !T {
data := h.post_json_str(req)!
println('data: ${data}')
return json.decode(T, data) or { return error("couldn't decode json for ${req} for ${data}") }
}