Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,28 @@ for await (const output of execResult.output) {

process.exit(0)
```

Working with Tags
----------------

You can create machines with tags and filter machines by tags:

```typescript
import { ForeverVM } from '@forevervm/sdk'

const fvm = new ForeverVM({ token: process.env.FOREVERVM_TOKEN })

// Create a machine with tags
const machineResponse = await fvm.createMachine({
tags: {
env: 'production',
owner: 'user123',
project: 'demo'
}
})

// List machines filtered by tags
const productionMachines = await fvm.listMachines({
tags: { env: 'production' }
})
```
52 changes: 48 additions & 4 deletions javascript/sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import type {
ApiExecResponse,
ApiExecResultResponse,
ApiExecResultStreamResponse,
CreateMachineRequest,
CreateMachineResponse,
ListMachinesRequest,
ListMachinesResponse,
WhoamiResponse,
} from './types'
Expand Down Expand Up @@ -80,12 +82,12 @@ export class ForeverVM {
return await this.#get('/v1/whoami')
}

async createMachine(): Promise<CreateMachineResponse> {
return await this.#post('/v1/machine/new')
async createMachine(request: CreateMachineRequest = {}): Promise<CreateMachineResponse> {
return await this.#post('/v1/machine/new', request)
}

async listMachines(): Promise<ListMachinesResponse> {
return await this.#get('/v1/machine/list')
async listMachines(request: ListMachinesRequest = {}): Promise<ListMachinesResponse> {
Comment thread
paulgb marked this conversation as resolved.
return await this.#post('/v1/machine/list', request)
}

async exec(
Expand Down Expand Up @@ -176,4 +178,46 @@ if (import.meta.vitest) {
}
}
})

test('createMachine with tags', async () => {
const fvm = new ForeverVM({ token: FOREVERVM_TOKEN, baseUrl: FOREVERVM_API_BASE })

// Create machine with tags
const taggedMachine = await fvm.createMachine({
tags: { env: 'test', purpose: 'sdk-test' },
})
expect(taggedMachine.machine_name).toBeDefined()

// List machines and verify tags
const machines = await fvm.listMachines()
const foundTagged = machines.machines.find(({ name }) => name === taggedMachine.machine_name)
expect(foundTagged).toBeDefined()
expect(foundTagged?.tags).toBeDefined()
expect(foundTagged?.tags?.env).toBe('test')
expect(foundTagged?.tags?.purpose).toBe('sdk-test')
})

test('listMachines with tag filter', async () => {
const fvm = new ForeverVM({ token: FOREVERVM_TOKEN, baseUrl: FOREVERVM_API_BASE })

// Create an untagged machine
const untaggedMachine = await fvm.createMachine()
expect(untaggedMachine.machine_name).toBeDefined()

// Create a uniquely tagged machine
const uniqueTag = `test-${Date.now()}`
const taggedMachine = await fvm.createMachine({
tags: { unique: uniqueTag },
})
expect(taggedMachine.machine_name).toBeDefined()

// List machines with the unique tag filter
const filteredMachines = await fvm.listMachines({
tags: { unique: uniqueTag },
})

// Verify only our machine with the unique tag is returned
expect(filteredMachines.machines.length).toBe(1)
expect(filteredMachines.machines[0].tags?.unique).toBe(uniqueTag)
})
}
9 changes: 9 additions & 0 deletions javascript/sdk/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ export interface WhoamiResponse {
account: string
}

export interface CreateMachineRequest {
tags?: Record<string, string>
}

export interface CreateMachineResponse {
machine_name: string
}
Expand All @@ -54,6 +58,11 @@ export interface Machine {
running: boolean
has_pending_instructions: boolean
expires_at?: string
tags?: Record<string, string>
}

export interface ListMachinesRequest {
tags?: Record<string, string>
}

export interface ListMachinesResponse {
Expand Down
19 changes: 14 additions & 5 deletions python/sdk/forevervm_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from .config import API_BASE_URL
from .repl import Repl
from .types import (
CreateMachineRequest,
CreateMachineResponse,
ExecResponse,
ExecResultResponse,
Expand Down Expand Up @@ -88,11 +89,19 @@ def whoami(self):
def whoami_async(self):
return self._get_async("/v1/whoami", type=WhoamiResponse)

def create_machine(self):
return self._post("/v1/machine/new", type=CreateMachineResponse)

def create_machine_async(self):
return self._post_async("/v1/machine/new", type=CreateMachineResponse)
def create_machine(self, tags: dict[str, str] = None):
request: CreateMachineRequest = {}
if tags:
request["tags"] = tags
return self._post("/v1/machine/new", type=CreateMachineResponse, data=request)

def create_machine_async(self, tags: dict[str, str] = None):
request: CreateMachineRequest = {}
if tags:
request["tags"] = tags
return self._post_async(
"/v1/machine/new", type=CreateMachineResponse, data=request
)

def list_machines(self):
return self._get("/v1/machine/list", type=ListMachinesResponse)
Expand Down
5 changes: 5 additions & 0 deletions python/sdk/forevervm_sdk/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ class WhoamiResponse(TypedDict):
account: str


class CreateMachineRequest(TypedDict, total=False):
tags: Dict[str, str]


class CreateMachineResponse(TypedDict):
machine_name: str

Expand All @@ -19,6 +23,7 @@ class Machine(TypedDict):
running: bool
has_pending_instructions: bool
expires_at: Optional[str]
tags: Dict[str, str]


class ListMachinesResponse(TypedDict):
Expand Down
53 changes: 53 additions & 0 deletions python/sdk/tests/test_connect.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,56 @@ async def test_exec_timeout():
instruction_seq = result["instruction_seq"]
exec_result = await fvm.exec_result_async(machine_name, instruction_seq)
assert "Timed out" in exec_result["result"]["error"]


def test_machine_tags():
fvm = ForeverVM(FOREVERVM_TOKEN, base_url=FOREVERVM_API_BASE)

# Create machine with tags
tags = {"environment": "test", "purpose": "sdk-test"}
machine = fvm.create_machine(tags=tags)
assert machine["machine_name"]
machine_name = machine["machine_name"]

# Verify the tags are returned when listing machines
machines = fvm.list_machines()["machines"]
tagged_machine = next((m for m in machines if m["name"] == machine_name), None)
assert tagged_machine is not None
assert "tags" in tagged_machine
assert tagged_machine["tags"] == tags

# Create another machine with different tags
tags2 = {"environment": "test", "version": "1.0.0"}
machine2 = fvm.create_machine(tags=tags2)
assert machine2["machine_name"]
machine_name2 = machine2["machine_name"]

# Verify both machines with their respective tags
machines = fvm.list_machines()["machines"]
tagged_machine1 = next((m for m in machines if m["name"] == machine_name), None)
tagged_machine2 = next((m for m in machines if m["name"] == machine_name2), None)

assert tagged_machine1 is not None
assert tagged_machine2 is not None
assert tagged_machine1["tags"] == tags
assert tagged_machine2["tags"] == tags2


@pytest.mark.asyncio
async def test_machine_tags_async():
fvm = ForeverVM(FOREVERVM_TOKEN, base_url=FOREVERVM_API_BASE)

# Create machine with tags asynchronously
tags = {"environment": "test-async", "purpose": "async-test"}
machine = await fvm.create_machine_async(tags=tags)
assert machine["machine_name"]
machine_name = machine["machine_name"]

# Verify the tags are returned when listing machines asynchronously
machines = await fvm.list_machines_async()
machines_list = machines["machines"]
tagged_machine = next((m for m in machines_list if m["name"] == machine_name), None)

assert tagged_machine is not None
assert "tags" in tagged_machine
assert tagged_machine["tags"] == tags