Skip to content

Commit c9b3e61

Browse files
author
Datata1
committed
adress feedback
1 parent 6ed575f commit c9b3e61

File tree

6 files changed

+111
-508
lines changed

6 files changed

+111
-508
lines changed

examples/create_workspace_with_landscape.py

Lines changed: 42 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,61 @@
11
import asyncio
2+
import time
23

34
from codesphere import CodesphereSDK
45
from codesphere.resources.workspace import WorkspaceCreate
5-
from codesphere.resources.workspace.landscape import ProfileBuilder
6+
from codesphere.resources.workspace.landscape import ProfileBuilder, ProfileConfig
67

78
TEAM_ID = 123 # Replace with your actual team ID
89

910

10-
async def main():
11-
async with CodesphereSDK() as sdk:
12-
plans = await sdk.metadata.list_plans()
13-
plan_id = next(
14-
(p for p in plans if p.title == "Micro" and not p.deprecated), None
15-
).id
16-
17-
payload = WorkspaceCreate(
18-
plan_id,
19-
team_id=TEAM_ID,
20-
name=f"my-unique-landscape-demo-{int(asyncio.time())}",
21-
)
11+
async def get_plan_id(sdk: CodesphereSDK, plan_name: str = "Micro") -> int:
12+
plans = await sdk.metadata.list_plans()
13+
plan = next((p for p in plans if p.title == plan_name and not p.deprecated), None)
14+
if not plan:
15+
raise ValueError(f"Plan '{plan_name}' not found")
16+
return plan.id
2217

23-
print("\nCreating workspace...")
24-
workspace = await sdk.workspaces.create(payload)
25-
print(f"Created workspace: {workspace.name} (ID: {workspace.id})")
2618

27-
try:
28-
print("Waiting for workspace to be running...")
29-
await workspace.wait_until_running(timeout=300.0, poll_interval=5.0)
30-
print("Workspace is now running!")
19+
def build_web_profile(plan_id: int) -> ProfileConfig:
20+
"""Build a simple web service landscape profile."""
21+
return (
22+
ProfileBuilder()
23+
.prepare()
24+
.add_step("npm install", name="Install dependencies")
25+
.done()
26+
.add_reactive_service("web")
27+
.plan(plan_id)
28+
.add_step("npm start")
29+
.add_port(3000, public=True)
30+
.add_path("/", port=3000)
31+
.replicas(1)
32+
.env("NODE_ENV", "production")
33+
.build()
34+
)
3135

32-
print("\nCreating landscape profile...")
33-
profile = (
34-
ProfileBuilder()
35-
.prepare()
36-
.add_step("npm install", name="Install dependencies")
37-
.done()
38-
.add_reactive_service("web")
39-
.plan(plan_id)
40-
.add_step("npm start")
41-
.add_port(3000, public=True)
42-
.add_path("/", port=3000)
43-
.replicas(1)
44-
.env("NODE_ENV", "production")
45-
.done()
46-
.build()
47-
)
4836

49-
profile_name = "production"
50-
await workspace.landscape.save_profile(profile_name, profile)
51-
print(f"Saved profile: {profile_name}")
37+
async def create_workspace(sdk: CodesphereSDK, plan_id: int, name: str):
38+
workspace = await sdk.workspaces.create(
39+
WorkspaceCreate(plan_id=plan_id, team_id=TEAM_ID, name=name)
40+
)
41+
await workspace.wait_until_running(timeout=300.0, poll_interval=5.0)
42+
return workspace
5243

53-
profiles = await workspace.landscape.list_profiles()
54-
print(f"Available profiles: {[p.name for p in profiles]}")
5544

56-
yaml_content = await workspace.landscape.get_profile(profile_name)
57-
print(f"\nGenerated profile YAML:\n{yaml_content}")
45+
async def deploy_landscape(workspace, profile: dict, profile_name: str = "production"):
46+
await workspace.landscape.save_profile(profile_name, profile)
47+
await workspace.landscape.deploy(profile=profile_name)
48+
print("Deployment started!")
5849

59-
print("\nDeploying landscape...")
60-
await workspace.landscape.deploy(profile=profile_name)
61-
print("Deployment started!")
6250

63-
finally:
64-
# Cleanup: Delete the workspace
65-
# print("\nCleaning up...")
66-
# await workspace.delete()
67-
# print(f"Deleted workspace: {workspace.name}")
68-
pass
51+
async def main():
52+
async with CodesphereSDK() as sdk:
53+
plan_id = await get_plan_id(sdk)
54+
workspace = await create_workspace(
55+
sdk, plan_id, f"landscape-demo-{int(time.time())}"
56+
)
57+
profile = build_web_profile(plan_id)
58+
await deploy_landscape(workspace, profile)
6959

7060

7161
if __name__ == "__main__":

src/codesphere/resources/workspace/landscape/models.py

Lines changed: 28 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -26,143 +26,72 @@
2626
_VALID_PROFILE_NAME = re.compile(r"^[A-Za-z0-9_-]+$")
2727

2828

29-
class WorkspaceLandscapeManager(_APIOperationExecutor):
30-
"""Manager for workspace landscape operations (Multi Server Deployments)."""
29+
def _validate_profile_name(name: str) -> None:
30+
if not _VALID_PROFILE_NAME.match(name):
31+
raise ValueError(
32+
f"Invalid profile name '{name}'. Must match pattern ^[A-Za-z0-9_-]+$"
33+
)
34+
35+
36+
def _profile_filename(name: str) -> str:
37+
_validate_profile_name(name)
38+
return f"ci.{name}.yml"
39+
3140

41+
class WorkspaceLandscapeManager(_APIOperationExecutor):
3242
def __init__(self, http_client: APIHttpClient, workspace_id: int):
3343
self._http_client = http_client
3444
self._workspace_id = workspace_id
3545
self.id = workspace_id
3646

37-
async def list_profiles(self) -> ResourceList[Profile]:
38-
"""List all available deployment profiles in the workspace.
39-
40-
Profiles are discovered by listing files matching the pattern ci.<profile>.yml
41-
in the workspace root directory.
42-
43-
Returns:
44-
ResourceList of Profile objects.
45-
"""
47+
async def _run_command(self, command: str) -> "CommandOutput":
4648
from ..operations import _EXECUTE_COMMAND_OP
4749
from ..schemas import CommandInput
4850

49-
command_data = CommandInput(command="ls -1 *.yml 2>/dev/null || true")
50-
result: CommandOutput = await self._execute_operation(
51-
_EXECUTE_COMMAND_OP, data=command_data
51+
return await self._execute_operation(
52+
_EXECUTE_COMMAND_OP, data=CommandInput(command=command)
5253
)
5354

55+
async def list_profiles(self) -> ResourceList[Profile]:
56+
result = await self._run_command("ls -1 *.yml 2>/dev/null || true")
57+
5458
profiles: List[Profile] = []
5559
if result.output:
5660
for line in result.output.strip().split("\n"):
57-
line = line.strip()
58-
if match := _PROFILE_FILE_PATTERN.match(line):
59-
profile_name = match.group(1)
60-
profiles.append(Profile(name=profile_name))
61+
if match := _PROFILE_FILE_PATTERN.match(line.strip()):
62+
profiles.append(Profile(name=match.group(1)))
6163

6264
return ResourceList[Profile](root=profiles)
6365

6466
async def save_profile(self, name: str, config: Union[ProfileConfig, str]) -> None:
65-
"""Save a profile configuration to the workspace.
66-
67-
Args:
68-
name: Profile name (must match pattern ^[A-Za-z0-9_-]+$).
69-
config: ProfileConfig instance or YAML string.
67+
filename = _profile_filename(name)
7068

71-
Raises:
72-
ValueError: If the profile name is invalid.
73-
"""
74-
from ..operations import _EXECUTE_COMMAND_OP
75-
from ..schemas import CommandInput
76-
77-
if not _VALID_PROFILE_NAME.match(name):
78-
raise ValueError(
79-
f"Invalid profile name '{name}'. Must match pattern ^[A-Za-z0-9_-]+$"
80-
)
81-
82-
# Convert ProfileConfig to YAML if needed
8369
if isinstance(config, ProfileConfig):
8470
yaml_content = config.to_yaml()
8571
else:
8672
yaml_content = config
8773

88-
# Escape single quotes in YAML content for shell
89-
escaped_content = yaml_content.replace("'", "'\"'\"'")
90-
91-
# Write the profile file
92-
filename = f"ci.{name}.yml"
93-
command = f"cat > {filename} << 'PROFILE_EOF'\n{yaml_content}PROFILE_EOF"
94-
95-
command_data = CommandInput(command=command)
96-
await self._execute_operation(_EXECUTE_COMMAND_OP, data=command_data)
97-
98-
async def get_profile(self, name: str) -> str:
99-
"""Get the raw YAML content of a profile.
100-
101-
Args:
102-
name: Profile name.
103-
104-
Returns:
105-
YAML content of the profile as a string.
106-
107-
Raises:
108-
ValueError: If the profile name is invalid.
109-
"""
110-
from ..operations import _EXECUTE_COMMAND_OP
111-
from ..schemas import CommandInput
112-
113-
if not _VALID_PROFILE_NAME.match(name):
114-
raise ValueError(
115-
f"Invalid profile name '{name}'. Must match pattern ^[A-Za-z0-9_-]+$"
116-
)
117-
118-
filename = f"ci.{name}.yml"
119-
command_data = CommandInput(command=f"cat {filename}")
120-
result: CommandOutput = await self._execute_operation(
121-
_EXECUTE_COMMAND_OP, data=command_data
74+
body = yaml_content if yaml_content.endswith("\n") else yaml_content + "\n"
75+
await self._run_command(
76+
f"cat > {filename} << 'PROFILE_EOF'\n{body}PROFILE_EOF\n"
12277
)
12378

79+
async def get_profile(self, name: str) -> str:
80+
result = await self._run_command(f"cat {_profile_filename(name)}")
12481
return result.output
12582

12683
async def delete_profile(self, name: str) -> None:
127-
"""Delete a profile from the workspace.
128-
129-
Args:
130-
name: Profile name to delete.
131-
132-
Raises:
133-
ValueError: If the profile name is invalid.
134-
"""
135-
from ..operations import _EXECUTE_COMMAND_OP
136-
from ..schemas import CommandInput
137-
138-
if not _VALID_PROFILE_NAME.match(name):
139-
raise ValueError(
140-
f"Invalid profile name '{name}'. Must match pattern ^[A-Za-z0-9_-]+$"
141-
)
142-
143-
filename = f"ci.{name}.yml"
144-
command_data = CommandInput(command=f"rm -f {filename}")
145-
await self._execute_operation(_EXECUTE_COMMAND_OP, data=command_data)
84+
await self._run_command(f"rm -f {_profile_filename(name)}")
14685

14786
async def deploy(self, profile: Optional[str] = None) -> None:
148-
"""Deploy the landscape.
149-
150-
Args:
151-
profile: Optional deployment profile name (must match pattern ^[A-Za-z0-9-_]+$).
152-
"""
15387
if profile is not None:
88+
_validate_profile_name(profile)
15489
await self._execute_operation(_DEPLOY_WITH_PROFILE_OP, profile=profile)
15590
else:
15691
await self._execute_operation(_DEPLOY_OP)
15792

15893
async def teardown(self) -> None:
159-
"""Teardown the landscape."""
16094
await self._execute_operation(_TEARDOWN_OP)
16195

16296
async def scale(self, services: Dict[str, int]) -> None:
163-
"""Scale landscape services.
164-
165-
Args:
166-
services: A dictionary mapping service names to replica counts (minimum 1).
167-
"""
16897
await self._execute_operation(_SCALE_OP, data=services)

0 commit comments

Comments
 (0)