Skip to content

Commit 1ed60db

Browse files
committed
feat: Add SSH file system access example
1 parent f825f58 commit 1ed60db

10 files changed

+444
-107
lines changed

examples/anthropic_tool_use.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
},
3232
]
3333

34-
def create_environment(args: dict[str, str]) -> str:
34+
async def create_environment(args: dict[str, str]) -> str:
3535
env_class = util.find_most_used_environment_class(gpclient)
3636
if not env_class:
3737
raise Exception("No environment class found. Please create one first.")
@@ -45,7 +45,7 @@ def create_environment(args: dict[str, str]) -> str:
4545
}
4646
).environment.id
4747
print(f"\nCreated environment: {environment_id} - waiting for it to be ready...")
48-
util.wait_for_environment_ready(gpclient, environment_id)
48+
await util.wait_for_environment_ready(gpclient, environment_id)
4949
print(f"\nEnvironment is ready: {environment_id}")
5050
return environment_id
5151

@@ -79,7 +79,7 @@ async def main():
7979
for tool in (c for c in message.content if c.type == "tool_use"):
8080
try:
8181
if tool.name == "create_environment":
82-
environment_id = create_environment(tool.input)
82+
environment_id = await create_environment(tool.input)
8383
messages.append({
8484
"role": "user",
8585
"content": [{

examples/fs_access.py

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
#!/usr/bin/env python
2+
3+
from io import StringIO
4+
import sys
5+
import asyncio
6+
import paramiko
7+
from gitpod import Gitpod
8+
import gitpod.lib as util
9+
from gitpod.types.environment_spec_param import EnvironmentSpecParam
10+
11+
# Examples:
12+
# - ./examples/fs_access.py
13+
# - ./examples/fs_access.py https://github.com/gitpod-io/empty
14+
async def main(cleanup: util.Disposables) -> None:
15+
client = Gitpod()
16+
17+
context_url = sys.argv[1] if len(sys.argv) > 1 else None
18+
19+
env_class = util.find_most_used_environment_class(client)
20+
if not env_class:
21+
print("Error: No environment class found. Please create one first.")
22+
sys.exit(1)
23+
print(f"Found environment class: {env_class.display_name} ({env_class.description})")
24+
25+
print("Generating SSH key pair")
26+
key = paramiko.RSAKey.generate(2048)
27+
private_key_file = StringIO()
28+
key.write_private_key(private_key_file)
29+
private_key_file.seek(0) # Reset position to start
30+
public_key = f"{key.get_name()} {key.get_base64()}"
31+
32+
print("Creating environment with SSH access")
33+
key_id = "fs-access-example"
34+
spec: EnvironmentSpecParam = {
35+
"desired_phase": "ENVIRONMENT_PHASE_RUNNING",
36+
"machine": {"class": env_class.id},
37+
"ssh_public_keys": [{
38+
"id": key_id,
39+
"value": public_key
40+
}]
41+
}
42+
if context_url:
43+
spec["content"] = {
44+
"initializer": {"specs": [{
45+
"contextUrl": {
46+
"url": context_url
47+
}
48+
}]}
49+
}
50+
51+
environment_id = client.environments.create(spec=spec).environment.id
52+
cleanup.add(lambda: client.environments.delete(environment_id=environment_id))
53+
54+
env = util.EnvironmentState(client, environment_id)
55+
cleanup.add(lambda: asyncio.run(env.close()))
56+
57+
print("Waiting for environment to be running")
58+
await env.wait_until_running()
59+
60+
print("Waiting for SSH key to be applied")
61+
await env.wait_for_ssh_key_applied(key_id=key_id, key_value=public_key)
62+
63+
print("Waiting for SSH URL")
64+
ssh_url = await env.wait_for_ssh_url()
65+
66+
print(f"Setting up SSH connection to {ssh_url}")
67+
# Parse ssh://username@host:port format
68+
url_parts = ssh_url.split('://')[-1]
69+
username, rest = url_parts.split('@')
70+
host, port = rest.split(':')
71+
port = int(port)
72+
73+
ssh = paramiko.SSHClient()
74+
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
75+
ssh.connect(
76+
hostname=host,
77+
port=port,
78+
username=username,
79+
pkey=key
80+
)
81+
cleanup.add(lambda: ssh.close())
82+
83+
print("Creating SFTP client")
84+
sftp = ssh.open_sftp()
85+
cleanup.add(lambda: sftp.close())
86+
87+
print("Writing test file")
88+
test_content = "Hello from Gitpod Python SDK!"
89+
with sftp.file('test.txt', 'w') as f:
90+
f.write(test_content)
91+
92+
with sftp.file('test.txt', 'r') as f:
93+
content = f.read()
94+
print(f"File content: {content}")
95+
96+
if __name__ == "__main__":
97+
disposables = util.Disposables()
98+
with disposables:
99+
asyncio.run(main(disposables))

examples/run_command.py

+14-9
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
# Examples:
1010
# - ./examples/run_command.py 'echo "Hello World!"'
1111
# - ./examples/run_command.py 'echo "Hello World!"' https://github.com/gitpod-io/empty
12-
async def main() -> None:
12+
async def main(cleanup: util.Disposables) -> None:
1313
client = Gitpod()
1414

1515
if len(sys.argv) < 2:
@@ -23,6 +23,7 @@ async def main() -> None:
2323
if not env_class:
2424
print("Error: No environment class found. Please create one first.")
2525
sys.exit(1)
26+
print(f"Found environment class: {env_class.display_name} ({env_class.description})")
2627

2728
spec: EnvironmentSpecParam = {
2829
"desired_phase": "ENVIRONMENT_PHASE_RUNNING",
@@ -37,15 +38,19 @@ async def main() -> None:
3738
}]}
3839
}
3940

41+
print("Creating environment")
4042
environment_id = client.environments.create(spec=spec).environment.id
41-
try:
42-
util.wait_for_environment_ready(client, environment_id)
43+
cleanup.add(lambda: client.environments.delete(environment_id=environment_id))
4344

44-
lines = util.run_command(client, environment_id, command)
45-
async for line in lines:
46-
print(line)
47-
finally:
48-
client.environments.delete(environment_id=environment_id)
45+
print("Waiting for environment to be ready")
46+
await util.wait_for_environment_ready(client, environment_id)
47+
48+
print("Running command")
49+
lines = util.run_command(client, environment_id, command)
50+
async for line in lines:
51+
print(line)
4952

5053
if __name__ == "__main__":
51-
asyncio.run(main())
54+
disposables = util.Disposables()
55+
with disposables:
56+
asyncio.run(main(disposables))

examples/run_service.py

+27-21
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
# Examples:
1010
# - ./examples/run_service.py
1111
# - ./examples/run_service.py https://github.com/gitpod-io/empty
12-
async def main() -> None:
12+
async def main(cleanup: util.Disposables) -> None:
1313
client = Gitpod()
1414

1515
context_url = sys.argv[1] if len(sys.argv) > 1 else None
@@ -18,9 +18,9 @@ async def main() -> None:
1818
if not env_class:
1919
print("Error: No environment class found. Please create one first.")
2020
sys.exit(1)
21+
print(f"Found environment class: {env_class.display_name} ({env_class.description})")
2122

2223
port = 8888
23-
2424
spec: EnvironmentSpecParam = {
2525
"desired_phase": "ENVIRONMENT_PHASE_RUNNING",
2626
"machine": {"class": env_class.id},
@@ -39,28 +39,34 @@ async def main() -> None:
3939
}]}
4040
}
4141

42+
print("Creating environment")
4243
environment_id = client.environments.create(spec=spec).environment.id
43-
try:
44-
util.wait_for_environment_ready(client, environment_id)
44+
cleanup.add(lambda: client.environments.delete(environment_id=environment_id))
45+
46+
print("Waiting for environment to be ready")
47+
env = util.EnvironmentState(client, environment_id)
48+
cleanup.add(lambda: asyncio.run(env.close()))
49+
await env.wait_until_running()
4550

46-
lines = util.run_service(client, environment_id, {
47-
"name":"Lama Service",
48-
"description":"Lama Service",
49-
"reference":"lama-service"
50-
}, {
51-
"commands": {
52-
"start":f"curl lama.sh | LAMA_PORT={port} sh",
53-
"ready":f"curl -s http://localhost:{port}"
54-
}
55-
})
51+
print("Starting Lama Service")
52+
lines = util.run_service(client, environment_id, {
53+
"name":"Lama Service",
54+
"description":"Lama Service",
55+
"reference":"lama-service"
56+
}, {
57+
"commands": {
58+
"start":f"curl lama.sh | LAMA_PORT={port} sh",
59+
"ready":f"curl -s http://localhost:{port}"
60+
}
61+
})
5662

57-
port_url = util.wait_for_port_url(client, environment_id, port)
58-
print(f"Lama Service is running at {port_url}")
63+
port_url = await env.wait_for_port_url(port)
64+
print(f"Lama Service is running at {port_url}")
5965

60-
async for line in lines:
61-
print(line)
62-
finally:
63-
client.environments.delete(environment_id=environment_id)
66+
async for line in lines:
67+
print(line)
6468

6569
if __name__ == "__main__":
66-
asyncio.run(main())
70+
disposables = util.Disposables()
71+
with disposables:
72+
asyncio.run(main(disposables))

pyproject.toml

+3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ dependencies = [
1414
"anyio>=3.5.0, <5",
1515
"distro>=1.7.0, <2",
1616
"sniffio",
17+
"paramiko>=3.5.1",
1718
]
1819
requires-python = ">= 3.8"
1920
classifiers = [
@@ -55,6 +56,8 @@ dev-dependencies = [
5556
"importlib-metadata>=6.7.0",
5657
"rich>=13.7.1",
5758
"nest_asyncio==1.6.0",
59+
"paramiko>=3.5.1",
60+
"anthropic>=0.45.2",
5861
]
5962

6063
[tool.rye.scripts]

requirements-dev.lock

+22
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,30 @@
1111
-e file:.
1212
annotated-types==0.6.0
1313
# via pydantic
14+
anthropic==0.45.2
1415
anyio==4.4.0
16+
# via anthropic
1517
# via gitpod-sdk
1618
# via httpx
1719
argcomplete==3.1.2
1820
# via nox
21+
bcrypt==4.2.1
22+
# via paramiko
1923
certifi==2023.7.22
2024
# via httpcore
2125
# via httpx
26+
cffi==1.17.1
27+
# via cryptography
28+
# via pynacl
2229
colorlog==6.7.0
2330
# via nox
31+
cryptography==44.0.1
32+
# via paramiko
2433
dirty-equals==0.6.0
2534
distlib==0.3.7
2635
# via virtualenv
2736
distro==1.8.0
37+
# via anthropic
2838
# via gitpod-sdk
2939
exceptiongroup==1.2.2
3040
# via anyio
@@ -36,6 +46,7 @@ h11==0.14.0
3646
httpcore==1.0.2
3747
# via httpx
3848
httpx==0.28.1
49+
# via anthropic
3950
# via gitpod-sdk
4051
# via respx
4152
idna==3.4
@@ -44,6 +55,8 @@ idna==3.4
4455
importlib-metadata==7.0.0
4556
iniconfig==2.0.0
4657
# via pytest
58+
jiter==0.8.2
59+
# via anthropic
4760
markdown-it-py==3.0.0
4861
# via rich
4962
mdurl==0.1.2
@@ -58,16 +71,23 @@ nox==2023.4.22
5871
packaging==23.2
5972
# via nox
6073
# via pytest
74+
paramiko==3.5.1
75+
# via gitpod-sdk
6176
platformdirs==3.11.0
6277
# via virtualenv
6378
pluggy==1.5.0
6479
# via pytest
80+
pycparser==2.22
81+
# via cffi
6582
pydantic==2.10.3
83+
# via anthropic
6684
# via gitpod-sdk
6785
pydantic-core==2.27.1
6886
# via pydantic
6987
pygments==2.18.0
7088
# via rich
89+
pynacl==1.5.0
90+
# via paramiko
7191
pyright==1.1.392.post0
7292
pytest==8.3.3
7393
# via pytest-asyncio
@@ -84,13 +104,15 @@ setuptools==68.2.2
84104
six==1.16.0
85105
# via python-dateutil
86106
sniffio==1.3.0
107+
# via anthropic
87108
# via anyio
88109
# via gitpod-sdk
89110
time-machine==2.9.0
90111
tomli==2.0.2
91112
# via mypy
92113
# via pytest
93114
typing-extensions==4.12.2
115+
# via anthropic
94116
# via anyio
95117
# via gitpod-sdk
96118
# via mypy

requirements.lock

+13
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,16 @@ annotated-types==0.6.0
1414
anyio==4.4.0
1515
# via gitpod-sdk
1616
# via httpx
17+
bcrypt==4.2.1
18+
# via paramiko
1719
certifi==2023.7.22
1820
# via httpcore
1921
# via httpx
22+
cffi==1.17.1
23+
# via cryptography
24+
# via pynacl
25+
cryptography==44.0.1
26+
# via paramiko
2027
distro==1.8.0
2128
# via gitpod-sdk
2229
exceptiongroup==1.2.2
@@ -30,10 +37,16 @@ httpx==0.28.1
3037
idna==3.4
3138
# via anyio
3239
# via httpx
40+
paramiko==3.5.1
41+
# via gitpod-sdk
42+
pycparser==2.22
43+
# via cffi
3344
pydantic==2.10.3
3445
# via gitpod-sdk
3546
pydantic-core==2.27.1
3647
# via pydantic
48+
pynacl==1.5.0
49+
# via paramiko
3750
sniffio==1.3.0
3851
# via anyio
3952
# via gitpod-sdk

0 commit comments

Comments
 (0)