diff --git a/examples/develop/runpod/runpod_example.vsh b/examples/develop/runpod/runpod_example.vsh new file mode 100755 index 00000000..30ddfc62 --- /dev/null +++ b/examples/develop/runpod/runpod_example.vsh @@ -0,0 +1,93 @@ +#!/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 +import json +import x.json2 + +// Create client with direct API key +// This uses RUNPOD_API_KEY from environment +mut rp := runpod.get()! + +// Create a new on demand pod +on_demand_pod_response := rp.create_on_demand_pod( + name: 'RunPod Tensorflow' + image_name: 'runpod/tensorflow' + cloud_type: 'ALL' + gpu_count: 1 + volume_in_gb: 5 + container_disk_in_gb: 5 + min_memory_in_gb: 4 + min_vcpu_count: 1 + gpu_type_id: 'NVIDIA RTX A4000' + ports: '8888/http' + volume_mount_path: '/workspace' + env: [ + runpod.EnvironmentVariableInput{ + key: 'JUPYTER_PASSWORD' + value: 'rn51hunbpgtltcpac3ol' + }, + ] +)! + +println('Created pod with ID: ${on_demand_pod_response.id}') + +// create a spot pod +spot_pod_response := rp.create_spot_pod( + port: 1826 + bid_per_gpu: 0.2 + cloud_type: 'SECURE' + gpu_count: 1 + volume_in_gb: 5 + container_disk_in_gb: 5 + min_vcpu_count: 1 + min_memory_in_gb: 4 + gpu_type_id: 'NVIDIA RTX A4000' + name: 'RunPod Pytorch' + image_name: 'runpod/pytorch' + docker_args: '' + ports: '8888/http' + volume_mount_path: '/workspace' + env: [ + runpod.EnvironmentVariableInput{ + key: 'JUPYTER_PASSWORD' + value: 'rn51hunbpgtltcpac3ol' + }, + ] +)! +println('Created spot pod with ID: ${spot_pod_response.id}') + +// stop on-demand pod +stop_on_demand_pod := rp.stop_pod( + pod_id: '${on_demand_pod_response.id}' +)! +println('Stopped on-demand pod with ID: ${stop_on_demand_pod.id}') + +// stop spot pod +stop_spot_pod := rp.stop_pod( + pod_id: '${spot_pod_response.id}' +)! +println('Stopped spot pod with ID: ${stop_spot_pod.id}') + +// start on-demand pod +start_on_demand_pod := rp.start_on_demand_pod(pod_id: '${on_demand_pod_response.id}', gpu_count: 1)! +println('Started on demand pod with ID: ${on_demand_pod_response.id}') + +// start spot pod +start_spot_pod := rp.start_spot_pod( + pod_id: '${spot_pod_response.id}' + gpu_count: 1 + bid_per_gpu: 0.2 +)! +println('Started spot pod with ID: ${spot_pod_response.id}') + +get_pod := rp.get_pod( + pod_id: '${spot_pod_response.id}' +)! +println('Get pod result: ${get_pod}') + +rp.terminate_pod(pod_id: '${spot_pod_response.id}')! +println('pod with id ${spot_pod_response.id} is terminated') + +rp.terminate_pod(pod_id: '${on_demand_pod_response.id}')! +println('pod with id ${on_demand_pod_response.id} is terminated') diff --git a/lib/clients/runpod/.heroscript b/lib/clients/runpod/.heroscript new file mode 100644 index 00000000..b4d99b90 --- /dev/null +++ b/lib/clients/runpod/.heroscript @@ -0,0 +1,8 @@ + +!!hero_code.generate_client + name:'runpod' + classname:'RunPod' + singleton:0 + default:1 + hasconfig:1 + reset:0 \ No newline at end of file diff --git a/lib/clients/runpod/client.v b/lib/clients/runpod/client.v new file mode 100644 index 00000000..3c6b9723 --- /dev/null +++ b/lib/clients/runpod/client.v @@ -0,0 +1,169 @@ +module runpod + +import json + +pub struct EnvironmentVariableInput { +pub mut: + key string + value string +} + +// Represents the nested machine structure in the response +pub struct Machine { +pub: + pod_host_id string @[json: 'podHostId'] +} + +// Response structure for the mutation +pub struct PodResult { +pub: + id string @[json: 'id'] + image_name string @[json: 'imageName'] + env []string @[json: 'env'] + machine_id int @[json: 'machineId'] + machine Machine @[json: 'machine'] + desired_status string @[json: 'desiredStatus'] +} + +// Input structure for the mutation +@[params] +pub struct PodFindAndDeployOnDemandRequest { +pub mut: + 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 @[json: 'name'] + image_name string @[json: 'imageName'] + docker_args string @[json: 'dockerArgs'] + ports string @[json: 'ports'] + volume_mount_path string @[json: 'volumeMountPath'] + env []EnvironmentVariableInput @[json: 'env'] +} + +pub fn (p PodFindAndDeployOnDemandRequest) json_str() string { + return json.encode(p) +} + +// Create On-Demand Pod +pub fn (mut rp RunPod) create_on_demand_pod(input PodFindAndDeployOnDemandRequest) !PodResult { + return rp.create_on_demand_pod_request(input)! +} + +@[params] +pub struct PodRentInterruptableInput { +pub mut: + port int @[json: 'port'] + network_volume_id string @[json: 'networkVolumeId'; omitempty] + start_jupyter bool @[json: 'startJupyter'] + start_ssh bool @[json: 'startSsh'] + bid_per_gpu f32 @[json: 'bidPerGpu'] + cloud_type string @[json: 'cloudType'] + container_disk_in_gb int @[json: 'containerDiskInGb'] + country_code string @[json: 'countryCode'; omitempty] + docker_args string @[json: 'dockerArgs'; omitempty] + env []EnvironmentVariableInput @[json: 'env'] + gpu_count int @[json: 'gpuCount'] + gpu_type_id string @[json: 'gpuTypeId'; omitempty] + image_name string @[json: 'imageName'; omitempty] + min_disk int @[json: 'minDisk'] + min_download int @[json: 'minDownload'] + min_memory_in_gb int @[json: 'minMemoryInGb'] + min_upload int @[json: 'minUpload'] + min_vcpu_count int @[json: 'minVcpuCount'] + name string @[json: 'name'; omitempty] + ports string @[json: 'ports'; omitempty] + stop_after string @[json: 'stopAfter'; omitempty] + support_public_ip bool @[json: 'supportPublicIp'] + template_id string @[json: 'templateId'; omitempty] + terminate_after string @[json: 'terminateAfter'; omitempty] + volume_in_gb int @[json: 'volumeInGb'] + volume_key string @[json: 'volumeKey'; omitempty] + volume_mount_path string @[json: 'volumeMountPath'; omitempty] + data_center_id string @[json: 'dataCenterId'; omitempty] + cuda_version string @[json: 'cudeVersion'; omitempty] + allowed_cuda_versions []string @[json: 'allowedCudaVersions'] +} + +pub fn (p PodRentInterruptableInput) json_str() string { + return json.encode(p) +} + +// Create Spot Pod +pub fn (mut rp RunPod) create_spot_pod(input PodRentInterruptableInput) !PodResult { + return rp.create_spot_pod_request(input)! +} + +@[params] +pub struct PodResumeInput { +pub mut: + pod_id string @[json: 'podId'; required] + gpu_count int @[json: 'gpuCount'] + sync_machine bool @[json: 'syncMachine'] + compute_type string @[json: 'computeType'; omitempty] +} + +pub fn (p PodResumeInput) json_str() string { + return json.encode(p) +} + +// Start On-Demand Pod +pub fn (mut rp RunPod) start_on_demand_pod(input PodResumeInput) !PodResult { + return rp.start_on_demand_pod_request(input)! +} + +@[params] +pub struct PodBidResumeInput { +pub mut: + pod_id string @[json: 'podId'; required] + gpu_count int @[json: 'gpuCount'] + bid_per_gpu f32 @[json: 'bidPerGpu'] +} + +pub fn (p PodBidResumeInput) json_str() string { + return json.encode(p) +} + +// Start Spot Pod +pub fn (mut rp RunPod) start_spot_pod(input PodBidResumeInput) !PodResult { + return rp.start_spot_pod_request(input)! +} + +@[params] +pub struct PodStopInput { +pub: + pod_id string @[json: 'podId'] + increment_version bool @[json: 'incrementVersion'] +} + +pub fn (p PodStopInput) json_str() string { + return json.encode(p) +} + +// Stop Pod +pub fn (mut rp RunPod) stop_pod(input PodStopInput) !PodResult { + return rp.stop_pod_request(input)! +} + +@[params] +pub struct PodTerminateInput { +pub: + pod_id string @[json: 'podId'] +} + +pub fn (mut rp RunPod) terminate_pod(input PodTerminateInput) ! { + rp.terminate_pod_request(input)! +} + +@[params] +pub struct PodFilter { +pub: + pod_id string @[json: 'podId'; required] +} + +pub fn (mut rp RunPod) get_pod(input PodFilter) !PodResult { + return rp.get_pod_request(input)! +} diff --git a/lib/clients/runpod/readme.md b/lib/clients/runpod/readme.md new file mode 100644 index 00000000..a8f0e7fb --- /dev/null +++ b/lib/clients/runpod/readme.md @@ -0,0 +1,42 @@ +# 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 +``` + +**RunPod API Example** + +This script demonstrates creating, stopping, starting, and terminating RunPod pods using the RunPod API. It creates both on-demand and spot pods. + +**Requirements** + +* Environment variable `RUNPOD_API_KEY` set with your RunPod API key + +**How to Run** + +- Find out our example in: examples/develop/runpod/runpod_example.vsh diff --git a/lib/clients/runpod/runpod_factory_.v b/lib/clients/runpod/runpod_factory_.v new file mode 100644 index 00000000..d46a6d37 --- /dev/null +++ b/lib/clients/runpod/runpod_factory_.v @@ -0,0 +1,102 @@ +module runpod + +import freeflowuniverse.herolib.core.base +import freeflowuniverse.herolib.core.playbook +import freeflowuniverse.herolib.ui.console + +__global ( + runpod_global map[string]&RunPod + runpod_default string +) + +/////////FACTORY + +@[params] +pub struct ArgsGet { +pub mut: + name string +} + +fn args_get(args_ ArgsGet) ArgsGet { + mut args := args_ + if args.name == '' { + args.name = runpod_default + } + if args.name == '' { + args.name = 'default' + } + return args +} + +pub fn get(args_ ArgsGet) !&RunPod { + mut args := args_get(args_) + if args.name !in runpod_global { + if args.name == 'default' { + if !config_exists(args) { + if default { + config_save(args)! + } + } + config_load(args)! + } + } + return runpod_global[args.name] or { + println(runpod_global) + panic('could not get config for runpod with name:${args.name}') + } +} + +fn config_exists(args_ ArgsGet) bool { + mut args := args_get(args_) + mut context := base.context() or { panic('bug') } + return context.hero_config_exists('runpod', args.name) +} + +fn config_load(args_ ArgsGet) ! { + mut args := args_get(args_) + mut context := base.context()! + mut heroscript := context.hero_config_get('runpod', args.name)! + play(heroscript: heroscript)! +} + +fn config_save(args_ ArgsGet) ! { + mut args := args_get(args_) + mut context := base.context()! + context.hero_config_set('runpod', args.name, heroscript_default()!)! +} + +fn set(o RunPod) ! { + mut o2 := obj_init(o)! + runpod_global[o.name] = &o2 + runpod_default = o.name +} + +@[params] +pub struct PlayArgs { +pub mut: + heroscript string // if filled in then plbook will be made out of it + plbook ?playbook.PlayBook + reset bool +} + +pub fn play(args_ PlayArgs) ! { + mut args := args_ + + if args.heroscript == '' { + args.heroscript = heroscript_default()! + } + mut plbook := args.plbook or { playbook.new(text: args.heroscript)! } + + mut install_actions := plbook.find(filter: 'runpod.configure')! + if install_actions.len > 0 { + for install_action in install_actions { + mut p := install_action.params + cfg_play(p)! + } + } +} + +// switch instance to be used for runpod +pub fn switch(name string) { + runpod_default = name +} diff --git a/lib/clients/runpod/runpod_http.v b/lib/clients/runpod/runpod_http.v new file mode 100644 index 00000000..ac221765 --- /dev/null +++ b/lib/clients/runpod/runpod_http.v @@ -0,0 +1,354 @@ +module runpod + +import x.json2 +import net.http { Method } +import freeflowuniverse.herolib.core.httpconnection + +// GraphQL response wrapper +struct GqlResponse[T] { +pub mut: + data map[string]T + errors []map[string]string +} + +// #### Internally method doing a network call to create a new on-demand pod. +// - Build the required query based pn the input sent by the user and send the request. +// - Decode the response received from the API into two objects `Data` and `Error`. +// - The data field should contains the pod details same as `PodResult` struct. +// - The error field should contain the error message. +fn (mut rp RunPod) create_on_demand_pod_request(input PodFindAndDeployOnDemandRequest) !PodResult { + mut fields := []Field{} + mut machine_fields := []Field{} + mut output_fields := []Field{} + mut builder := QueryBuilder{} + + machine_fields << new_field(name: 'podHostId') + output_fields << new_field(name: 'id') + output_fields << new_field(name: 'imageName') + output_fields << new_field(name: 'env') + output_fields << new_field(name: 'machineId') + output_fields << new_field(name: 'desiredStatus') + output_fields << new_field(name: 'machine', sub_fields: machine_fields) + fields << new_field( + name: 'podFindAndDeployOnDemand' + arguments: { + 'input': '\$arguments' + } + sub_fields: output_fields + ) + + builder.add_operation( + operation: .mutation + fields: fields + variables: { + '\$arguments': 'PodFindAndDeployOnDemandInput' + } + ) + mut variables := { + 'arguments': json2.Any(type_to_map(input)!) + } + query := builder.build_query(variables: variables) + + response := rp.make_request[GqlResponse[PodResult]](.post, '/graphql', query)! + return response.data['podFindAndDeployOnDemand'] or { + return error('Could not find "podFindAndDeployOnDemand" in response data: ${response.data}') + } +} + +// #### Internally method doing a network call to create a new spot pod. +// - Build the required query based pn the input sent by the user and send the request. +// - Decode the response received from the API into two objects `Data` and `Error`. +// - The data field should contains the pod details same as `PodResult` struct. +// - The error field should contain the error message. +fn (mut rp RunPod) create_spot_pod_request(input PodRentInterruptableInput) !PodResult { + mut fields := []Field{} + mut machine_fields := []Field{} + mut output_fields := []Field{} + mut builder := QueryBuilder{} + + machine_fields << new_field(name: 'podHostId') + output_fields << new_field(name: 'id') + output_fields << new_field(name: 'imageName') + output_fields << new_field(name: 'env') + output_fields << new_field(name: 'machineId') + output_fields << new_field(name: 'desiredStatus') + output_fields << new_field(name: 'machine', sub_fields: machine_fields) + fields << new_field( + name: 'podRentInterruptable' + arguments: { + 'input': '\$arguments' + } + sub_fields: output_fields + ) + + builder.add_operation( + operation: .mutation + fields: fields + variables: { + '\$arguments': 'PodRentInterruptableInput!' + } + ) + mut variables := { + 'arguments': json2.Any(type_to_map(input)!) + } + query := builder.build_query(variables: variables) + + response := rp.make_request[GqlResponse[PodResult]](.post, '/graphql', query)! + return response.data['podRentInterruptable'] or { + return error('Could not find "podRentInterruptable" in response data: ${response.data}') + } +} + +// #### Internally method doing a network call to start on demand pod. +// - Build the required query based pn the input sent by the user and send the request. +// - Decode the response received from the API into two objects `Data` and `Error`. +// - The data field should contains the pod details same as `PodResult` struct. +// - The error field should contain the error message. +fn (mut rp RunPod) start_on_demand_pod_request(input PodResumeInput) !PodResult { + mut fields := []Field{} + mut machine_fields := []Field{} + mut output_fields := []Field{} + mut builder := QueryBuilder{} + + machine_fields << new_field(name: 'podHostId') + output_fields << new_field(name: 'id') + output_fields << new_field(name: 'imageName') + output_fields << new_field(name: 'env') + output_fields << new_field(name: 'machineId') + output_fields << new_field(name: 'desiredStatus') + output_fields << new_field(name: 'machine', sub_fields: machine_fields) + fields << new_field( + name: 'podResume' + arguments: { + 'input': '\$arguments' + } + sub_fields: output_fields + ) + + builder.add_operation( + operation: .mutation + fields: fields + variables: { + '\$arguments': 'PodResumeInput!' + } + ) + mut variables := { + 'arguments': json2.Any(type_to_map(input)!) + } + query := builder.build_query(variables: variables) + + response := rp.make_request[GqlResponse[PodResult]](.post, '/graphql', query)! + return response.data['podResume'] or { + return error('Could not find "podResume" in response data: ${response.data}') + } +} + +// #### Internally method doing a network call to start spot pod. +// - Build the required query based pn the input sent by the user and send the request. +// - Decode the response received from the API into two objects `Data` and `Error`. +// - The data field should contains the pod details same as `PodResult` struct. +// - The error field should contain the error message. +fn (mut rp RunPod) start_spot_pod_request(input PodBidResumeInput) !PodResult { + mut fields := []Field{} + mut machine_fields := []Field{} + mut output_fields := []Field{} + mut builder := QueryBuilder{} + + machine_fields << new_field(name: 'podHostId') + output_fields << new_field(name: 'id') + output_fields << new_field(name: 'imageName') + output_fields << new_field(name: 'env') + output_fields << new_field(name: 'machineId') + output_fields << new_field(name: 'desiredStatus') + output_fields << new_field(name: 'machine', sub_fields: machine_fields) + fields << new_field( + name: 'podBidResume' + arguments: { + 'input': '\$arguments' + } + sub_fields: output_fields + ) + + builder.add_operation( + operation: .mutation + fields: fields + variables: { + '\$arguments': 'PodBidResumeInput!' + } + ) + mut variables := { + 'arguments': json2.Any(type_to_map(input)!) + } + query := builder.build_query(variables: variables) + + response := rp.make_request[GqlResponse[PodResult]](.post, '/graphql', query)! + return response.data['podBidResume'] or { + return error('Could not find "podBidResume" in response data: ${response.data}') + } +} + +// #### Internally method doing a network call to stop a pod. +// - Build the required query based pn the input sent by the user and send the request. +// - Decode the response received from the API into two objects `Data` and `Error`. +// - The data field should contains the pod details same as `PodResult` struct. +// - The error field should contain the error message. +fn (mut rp RunPod) stop_pod_request(input PodStopInput) !PodResult { + mut fields := []Field{} + mut machine_fields := []Field{} + mut output_fields := []Field{} + mut builder := QueryBuilder{} + + machine_fields << new_field(name: 'podHostId') + output_fields << new_field(name: 'id') + output_fields << new_field(name: 'imageName') + output_fields << new_field(name: 'env') + output_fields << new_field(name: 'machineId') + output_fields << new_field(name: 'desiredStatus') + output_fields << new_field(name: 'machine', sub_fields: machine_fields) + fields << new_field( + name: 'podStop' + arguments: { + 'input': '\$arguments' + } + sub_fields: output_fields + ) + + builder.add_operation( + operation: .mutation + fields: fields + variables: { + '\$arguments': 'PodStopInput!' + } + ) + mut variables := { + 'arguments': json2.Any(type_to_map(input)!) + } + query := builder.build_query(variables: variables) + + response := rp.make_request[GqlResponse[PodResult]](.post, '/graphql', query)! + return response.data['podStop'] or { + return error('Could not find "podStop" in response data: ${response.data}') + } +} + +fn (mut rp RunPod) terminate_pod_request(input PodTerminateInput) ! { + mut fields := []Field{} + mut builder := QueryBuilder{} + + fields << new_field( + name: 'podTerminate' + arguments: { + 'input': '\$arguments' + } + ) + + builder.add_operation( + operation: .mutation + fields: fields + variables: { + '\$arguments': 'PodTerminateInput!' + } + ) + mut variables := { + 'arguments': json2.Any(type_to_map(input)!) + } + query := builder.build_query(variables: variables) + + response := rp.make_request[GqlResponse[PodResult]](.post, '/graphql', query)! + _ := response.data['podTerminate'] or { + return error('Could not find "podTerminate" in response data: ${response.data}') + } +} + +fn (mut rp RunPod) get_pod_request(input PodFilter) !PodResult { + mut fields := []Field{} + mut machine_fields := []Field{} + mut output_fields := []Field{} + mut builder := QueryBuilder{} + + machine_fields << new_field(name: 'podHostId') + output_fields << new_field(name: 'id') + output_fields << new_field(name: 'imageName') + output_fields << new_field(name: 'env') + output_fields << new_field(name: 'machineId') + output_fields << new_field(name: 'desiredStatus') + output_fields << new_field(name: 'machine', sub_fields: machine_fields) + fields << new_field( + name: 'pod' + arguments: { + 'input': '\$arguments' + } + sub_fields: output_fields + ) + + builder.add_operation( + operation: .query + fields: fields + variables: { + '\$arguments': 'PodFilter' + } + ) + mut variables := { + 'arguments': json2.Any(type_to_map(input)!) + } + query := builder.build_query(variables: variables) + + response := rp.make_request[GqlResponse[PodResult]](.post, '/graphql', query)! + return response.data['pod'] or { + return error('Could not find "pod" in response data: ${response.data}') + } +} + +// Represents the main structure for interacting with the RunPod API. +// Provides utilities to manage HTTP connections and perform GraphQL queries. +fn (mut rp RunPod) httpclient() !&httpconnection.HTTPConnection { + mut http_conn := httpconnection.new( + name: 'runpod_vclient_${rp.name}' + url: rp.base_url + cache: true + retry: 3 + )! + http_conn.default_header.add(.authorization, 'Bearer ${rp.api_key}') + return http_conn +} + +// Sends an HTTP request to the RunPod API with the specified method, path, and data. +fn (mut rp RunPod) make_request[T](method Method, path string, data string) !T { + mut request := httpconnection.Request{ + prefix: path + data: data + debug: true + dataformat: .json + } + + mut http_client := rp.httpclient()! + mut response := T{} + + match method { + .get { + request.method = .get + response = http_client.get_json_generic[T](request)! + } + .post { + request.method = .post + response = http_client.post_json_generic[T](request)! + } + .put { + request.method = .put + response = http_client.put_json_generic[T](request)! + } + .delete { + request.method = .delete + response = http_client.delete_json_generic[T](request)! + } + else { + return error('unsupported method: ${method}') + } + } + + if response.errors.len > 0 { + return error('Error while sending the request due to: ${response.errors[0]['message']}') + } + + return response +} diff --git a/lib/clients/runpod/runpod_model.v b/lib/clients/runpod/runpod_model.v new file mode 100644 index 00000000..ea3a9606 --- /dev/null +++ b/lib/clients/runpod/runpod_model.v @@ -0,0 +1,43 @@ +module runpod + +import freeflowuniverse.herolib.data.paramsparser +import os + +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:'${os.getenv('RUNPOD_API_KEY')}' + base_url:'https://api.runpod.io/' + " +} + +// 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/' +} + +fn cfg_play(p paramsparser.Params) ! { + // THIS IS EXAMPLE CODE AND NEEDS TO BE CHANGED IN LINE WITH struct above + mut mycfg := RunPod{ + name: p.get_default('name', 'default')! + api_key: p.get_default('api_key', os.getenv('RUNPOD_API_KEY'))! + base_url: p.get_default('base_url', 'https://api.runpod.io/')! + } + set(mycfg)! +} + +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 +} diff --git a/lib/clients/runpod/utils.v b/lib/clients/runpod/utils.v new file mode 100644 index 00000000..bdd147c4 --- /dev/null +++ b/lib/clients/runpod/utils.v @@ -0,0 +1,118 @@ +module runpod + +import freeflowuniverse.herolib.core.httpconnection +import x.json2 + +enum OperationType { + query + mutation +} + +struct QueryBuilder { +pub mut: + operation OperationType + fields []Field + variables map[string]string +} + +struct Field { + name string + arguments map[string]string + sub_fields []Field +} + +@[params] +pub struct NewFieldArgs { +pub: + name string + arguments map[string]string + sub_fields []Field +} + +fn new_field(args NewFieldArgs) Field { + return Field{ + name: args.name + arguments: args.arguments + sub_fields: args.sub_fields + } +} + +fn build_arguments(args map[string]string) string { + if args.len == 0 { + return '' + } + + mut sb := '' + sb += '(' + + for key, value in args { + if value.len == 0 { + continue + } + + sb += '${key}: ${value}, ' + } + + return sb.trim_right(', ') + ')' +} + +fn build_fields(fields []Field) string { + mut sb := ' { ' + for field in fields { + sb += field.name + if field.arguments.len > 0 { + sb += build_arguments(field.arguments) + } + + if field.sub_fields.len > 0 { + sb += build_fields(field.sub_fields) + } + + sb += ' ' + } + sb += ' } ' + return sb +} + +@[params] +pub struct AddOperationArgs { +pub: + operation OperationType + fields []Field + variables map[string]string +} + +fn (mut q QueryBuilder) add_operation(args AddOperationArgs) { + q.operation = args.operation + q.fields = args.fields + q.variables = args.variables.clone() +} + +@[params] +pub struct BuildQueryArgs { +pub: + variables map[string]json2.Any +} + +fn (q QueryBuilder) build_query(args BuildQueryArgs) string { + mut query := '' + query += '${q.operation}' + ' myOperation' + + if q.variables.len > 0 { + query += build_arguments(q.variables) + } + + query += build_fields(q.fields) + + mut q_map := { + 'query': json2.Any(query) + 'variables': json2.Any(args.variables) + } + + return json2.encode(q_map) +} + +fn type_to_map[T](t T) !map[string]json2.Any { + encoded_input := json2.encode(t) + return json2.raw_decode(encoded_input)!.as_map() +} diff --git a/lib/core/httpconnection/connection_methods_generic.v b/lib/core/httpconnection/connection_methods_generic.v index fd63d4e9..897c8039 100644 --- a/lib/core/httpconnection/connection_methods_generic.v +++ b/lib/core/httpconnection/connection_methods_generic.v @@ -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)! {