truefoundry 0.1.1__py3-none-any.whl → 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of truefoundry might be problematic. Click here for more details.

Files changed (35) hide show
  1. truefoundry/autodeploy/__init__.py +0 -0
  2. truefoundry/autodeploy/agents/__init__.py +0 -0
  3. truefoundry/autodeploy/agents/base.py +181 -0
  4. truefoundry/autodeploy/agents/developer.py +113 -0
  5. truefoundry/autodeploy/agents/project_identifier.py +124 -0
  6. truefoundry/autodeploy/agents/tester.py +75 -0
  7. truefoundry/autodeploy/cli.py +348 -0
  8. truefoundry/autodeploy/constants.py +22 -0
  9. truefoundry/autodeploy/exception.py +2 -0
  10. truefoundry/autodeploy/logger.py +13 -0
  11. truefoundry/autodeploy/tools/__init__.py +26 -0
  12. truefoundry/autodeploy/tools/ask.py +33 -0
  13. truefoundry/autodeploy/tools/base.py +31 -0
  14. truefoundry/autodeploy/tools/commit.py +139 -0
  15. truefoundry/autodeploy/tools/docker_build.py +109 -0
  16. truefoundry/autodeploy/tools/docker_run.py +150 -0
  17. truefoundry/autodeploy/tools/file_type_counts.py +79 -0
  18. truefoundry/autodeploy/tools/list_files.py +82 -0
  19. truefoundry/autodeploy/tools/read_file.py +66 -0
  20. truefoundry/autodeploy/tools/send_request.py +54 -0
  21. truefoundry/autodeploy/tools/write_file.py +101 -0
  22. truefoundry/autodeploy/utils/diff.py +157 -0
  23. truefoundry/autodeploy/utils/pydantic_compat.py +19 -0
  24. truefoundry/cli/__main__.py +11 -5
  25. truefoundry/deploy/__init__.py +1 -1
  26. truefoundry/deploy/cli/__init__.py +0 -0
  27. truefoundry/deploy/cli/cli.py +99 -0
  28. truefoundry/deploy/cli/deploy.py +184 -0
  29. truefoundry/langchain/__init__.py +1 -1
  30. truefoundry/ml/__init__.py +4 -2
  31. {truefoundry-0.1.1.dist-info → truefoundry-0.2.0.dist-info}/METADATA +13 -5
  32. truefoundry-0.2.0.dist-info/RECORD +36 -0
  33. truefoundry-0.1.1.dist-info/RECORD +0 -10
  34. {truefoundry-0.1.1.dist-info → truefoundry-0.2.0.dist-info}/WHEEL +0 -0
  35. {truefoundry-0.1.1.dist-info → truefoundry-0.2.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,109 @@
1
+ from __future__ import annotations
2
+
3
+ import itertools
4
+ import re
5
+ from typing import Any, Generator, Iterable, Optional
6
+
7
+ import docker
8
+ from docker.models.images import BuildError, json_stream
9
+ from pydantic import Field
10
+ from rich.console import Console, ConsoleOptions, RenderResult
11
+ from rich.padding import Padding
12
+ from rich.text import Text
13
+
14
+ from truefoundry.autodeploy.tools.base import (
15
+ Event,
16
+ Message,
17
+ RequestEvent,
18
+ ResponseEvent,
19
+ Tool,
20
+ )
21
+
22
+
23
+ class DockerBuildLog(Event):
24
+ log: str
25
+
26
+ def render(self, console: Console):
27
+ console.print(Padding.indent(renderable=Text.from_ansi(self.log), level=2))
28
+
29
+
30
+ # vendored from
31
+ # https://github.com/docker/docker-py/blob/9ad4bddc9ee23f3646f256280a21ef86274e39bc/docker/models/images.py#L220
32
+ def _build(docker_client: docker.DockerClient, **kwargs) -> Iterable[DockerBuildLog]:
33
+ resp = docker_client.images.client.api.build(**kwargs)
34
+ if isinstance(resp, str):
35
+ return docker_client.images.get(resp)
36
+ last_event = None
37
+ image_id = None
38
+ result_stream, internal_stream = itertools.tee(json_stream(resp))
39
+ for chunk in internal_stream:
40
+ if "error" in chunk:
41
+ raise BuildError(chunk["error"], result_stream)
42
+ if "stream" in chunk:
43
+ yield DockerBuildLog(log=chunk["stream"])
44
+ match = re.search(
45
+ r"(^Successfully built |sha256:)([0-9a-f]+)$", chunk["stream"]
46
+ )
47
+ if match:
48
+ image_id = match.group(2)
49
+ last_event = chunk
50
+ if image_id:
51
+ return None
52
+ raise BuildError(last_event or "Unknown", result_stream)
53
+
54
+
55
+ class DockerBuild(Tool):
56
+ description = """
57
+ Build a docker image.
58
+ """
59
+
60
+ class Request(RequestEvent):
61
+ dockerfile_path: str = Field(
62
+ ...,
63
+ pattern=r"^[a-zA-Z0-9\.]{1}.*$",
64
+ description="Dockerfile path. ",
65
+ )
66
+ image_tag: str = Field(..., description="image tag")
67
+
68
+ class Response(ResponseEvent):
69
+ error: Optional[str] = Field(None, description="Error raised while building")
70
+ build_logs: Optional[str] = Field(None, description="Build logs")
71
+
72
+ def __rich_console__(
73
+ self, console: Console, options: ConsoleOptions
74
+ ) -> RenderResult:
75
+ none_text = "[italic magenta]None[/]"
76
+ error_text = (
77
+ f"[green]'{self.error}'[/]" if self.error is not None else none_text
78
+ )
79
+ yield Text.from_markup("[bold magenta]Response[/](")
80
+ if self.build_logs is not None:
81
+ yield Text.from_markup(' [yellow]build_logs[/]= "')
82
+ yield Text.from_ansi(self.build_logs)
83
+ yield Text.from_markup('"')
84
+ else:
85
+ yield Text.from_markup(f" [yellow]build_logs[/]={none_text}")
86
+ yield Text.from_markup(f" [yellow]error[/]={error_text}\n)")
87
+
88
+ def __init__(self, project_root_path: str, docker_client: docker.DockerClient):
89
+ self.project_root_path = project_root_path
90
+ self.docker_client = docker_client
91
+
92
+ def run(self, request: DockerBuild.Request) -> Generator[Event, Any, ResponseEvent]:
93
+ yield Message(message="[bold cyan]Processing:[/] Building Docker image...")
94
+ yield Message(message="[bold yellow]Docker build logs:[/]")
95
+ try:
96
+ for message in _build(
97
+ self.docker_client,
98
+ path=self.project_root_path,
99
+ tag=request.image_tag,
100
+ ):
101
+ yield message
102
+ return DockerBuild.Response()
103
+ except BuildError as ex:
104
+ logs = ""
105
+ for log_line in ex.build_log:
106
+ logs += log_line.get("stream", "")
107
+ return DockerBuild.Response(error=str(ex), build_logs=logs[-800:])
108
+ except (docker.errors.APIError, docker.errors.DockerException) as ex:
109
+ return DockerBuild.Response(error=str(ex), build_logs="")
@@ -0,0 +1,150 @@
1
+ from __future__ import annotations
2
+
3
+ import atexit
4
+ from typing import Any, Dict, Generator, List, Optional
5
+
6
+ import docker
7
+ import requests
8
+ from pydantic import Field
9
+ from rich.console import Console, ConsoleOptions, RenderResult
10
+ from rich.padding import Padding
11
+ from rich.text import Text
12
+
13
+ from truefoundry.autodeploy.tools.base import (
14
+ Event,
15
+ Message,
16
+ RequestEvent,
17
+ ResponseEvent,
18
+ Tool,
19
+ )
20
+
21
+
22
+ class DockerRunLog(Event):
23
+ index: int
24
+ log: str
25
+
26
+ def render(self, console: Console):
27
+ # if self.index > 1:
28
+ # print("\r", end="")
29
+ console.print(Padding.indent(renderable=Text.from_ansi(self.log), level=2))
30
+ # console.print("Press control-c to stop waiting for more logs", end="")
31
+
32
+
33
+ class DockerRun(Tool):
34
+ description = """
35
+ Run a docker image
36
+ """
37
+
38
+ class Request(RequestEvent):
39
+ image_tag: str
40
+ ports: Optional[Dict[str, int]] = Field(
41
+ None,
42
+ description="""
43
+ Ports to expose.
44
+ The keys of the dictionary are the ports to bind inside the container in 'port'.
45
+ The values are the ports to open on the host""",
46
+ )
47
+ command: str
48
+
49
+ def render(self, console: Console):
50
+ console.print(
51
+ f"[bold magenta]TrueFoundry[/] is executing the Docker container. Image Tag: [bold green]{self.image_tag}[/], Exposed Port: [bold green]{str(self.ports) if self.ports is not None else 'Not exposed'}[/], Command: [bold green]{self.command}[/]"
52
+ )
53
+
54
+ class Response(ResponseEvent):
55
+ logs: Optional[List[str]] = Field(
56
+ None, description="Logs of the container. Only last 50 chars."
57
+ )
58
+ exit_code: Optional[int] = Field(
59
+ None,
60
+ description="""
61
+ Exit code of the process if the container stops.
62
+ This will not be passed if the container is still running.
63
+ """,
64
+ )
65
+ client_error: Optional[str] = Field(None, description="Docker client error.")
66
+
67
+ def __rich_console__(
68
+ self, console: Console, options: ConsoleOptions
69
+ ) -> RenderResult:
70
+ none_text = "[italic magenta]None[/]"
71
+ error_text = (
72
+ f"[green]'{self.client_error}'[/]"
73
+ if self.client_error is not None
74
+ else none_text
75
+ )
76
+ exit_code_text = (
77
+ "[green]'0'[/]"
78
+ if self.exit_code == 0
79
+ else (
80
+ none_text if self.exit_code is None else f"[red]{self.exit_code}[/]"
81
+ )
82
+ )
83
+
84
+ yield Text.from_markup("[bold magenta]Response[/](")
85
+ if self.logs is not None:
86
+ yield Text.from_markup(' [yellow]logs=[/]"')
87
+ yield Text.from_ansi("".join(self.logs))
88
+ yield Text.from_markup('"')
89
+ else:
90
+ yield Text.from_markup(f" [yellow]logs[/]={none_text}")
91
+
92
+ yield Text.from_markup(f" [yellow]exit_code[/]={exit_code_text}")
93
+ yield Text.from_markup(f" [yellow]client_error[/]={error_text} \n)")
94
+
95
+ def __init__(self, docker_client: docker.DockerClient, environment: Dict):
96
+ self.containers = []
97
+ self.docker_client = docker_client
98
+ self.environment = environment
99
+ atexit.register(self._kill_running_containers)
100
+
101
+ def _kill_running_containers(self):
102
+ if self.containers:
103
+ container = self.containers.pop()
104
+ try:
105
+ container.remove(force=True)
106
+ except docker.errors.APIError:
107
+ pass
108
+
109
+ def run(self, request: DockerRun.Request) -> Generator[Event, Any, ResponseEvent]:
110
+ self._kill_running_containers()
111
+ yield Message(message="[bold cyan]Testing:[/] Running Docker container...")
112
+ try:
113
+ container = self.docker_client.containers.run(
114
+ request.image_tag,
115
+ detach=True,
116
+ remove=False,
117
+ stderr=True,
118
+ ports=request.ports,
119
+ environment=self.environment,
120
+ command=request.command,
121
+ )
122
+ except docker.errors.APIError as ex:
123
+ if ex.is_client_error():
124
+ return DockerRun.Response(client_error=str(ex))
125
+ raise
126
+ yield Message(message="[bold yellow]Docker logs:[/]")
127
+ self.containers.append(container)
128
+ exit_code = None
129
+
130
+ all_logs = []
131
+ logs = container.logs(stream=True)
132
+
133
+ try:
134
+ for i, log in enumerate(logs):
135
+ log = log.decode()
136
+ all_logs.append(log)
137
+ yield DockerRunLog(index=i, log=log)
138
+ except KeyboardInterrupt:
139
+ pass
140
+ else:
141
+ yield Message(message="\n[bold yellow]There are no more logs.[/]")
142
+ exit_code = None
143
+ try:
144
+ exit_code = container.wait(timeout=1).get("StatusCode")
145
+ except (
146
+ requests.exceptions.ReadTimeout,
147
+ requests.exceptions.ConnectionError,
148
+ ):
149
+ ...
150
+ return DockerRun.Response(logs=all_logs, exit_code=exit_code)
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from collections import Counter
5
+ from typing import Any, Dict, Generator
6
+
7
+ import gitignorefile
8
+ from pydantic import Field
9
+ from rich.console import Console
10
+ from rich.table import Table
11
+
12
+ from truefoundry.autodeploy.tools.base import (
13
+ Event,
14
+ Message,
15
+ RequestEvent,
16
+ ResponseEvent,
17
+ Tool,
18
+ )
19
+
20
+
21
+ class ShowFileCount(Event):
22
+ file_types: Dict[str, int]
23
+
24
+ def render(self, console: Console):
25
+ total_files = sum(self.file_types.values())
26
+ console.print(f"Found {total_files} files.")
27
+ table = Table()
28
+ table.add_column("File Type", style="cyan")
29
+ table.add_column("Count", justify="right", style="green")
30
+ for file_type, count in self.file_types.items():
31
+ table.add_row(file_type, str(count))
32
+
33
+ console.print(table)
34
+
35
+
36
+ class FileTypeCounts(Tool):
37
+ description = """
38
+ Get counts of different types of file present.
39
+ """
40
+
41
+ class Request(RequestEvent): ...
42
+
43
+ class Response(ResponseEvent):
44
+ file_types: Dict[str, int] = Field(
45
+ ...,
46
+ description='Counts of different types of files. Ex: {"py": 1} or {"c": 1}',
47
+ )
48
+
49
+ def __init__(self, project_root_path: str):
50
+ self.project_root_path = project_root_path
51
+
52
+ def run(
53
+ self, request: FileTypeCounts.Request
54
+ ) -> Generator[Event, Any, ResponseEvent]:
55
+ counter = Counter()
56
+
57
+ yield Message(
58
+ message="[bold cyan]Processing:[/] Scanning for various file types..."
59
+ )
60
+
61
+ def gitignore(_):
62
+ return False
63
+
64
+ gitignore_path = os.path.join(self.project_root_path, ".gitignore")
65
+ if os.path.exists(gitignore_path):
66
+ gitignore = gitignorefile.parse(path=gitignore_path)
67
+ for root, dirs, ps in os.walk(
68
+ self.project_root_path,
69
+ ):
70
+ root = root[len(self.project_root_path) :]
71
+ if ".git" in dirs:
72
+ dirs.remove(".git")
73
+ counter.update(
74
+ p.split(".")[-1] if len(p) and p[0] != "." else p
75
+ for p in ps
76
+ if not gitignore(os.path.join(root, p).strip(os.path.sep))
77
+ )
78
+ yield ShowFileCount(file_types=counter)
79
+ return FileTypeCounts.Response(file_types=counter)
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from fnmatch import fnmatch
5
+ from typing import Any, Generator, List, Optional
6
+
7
+ import gitignorefile
8
+ from pydantic import Field
9
+
10
+ from truefoundry.autodeploy.tools.base import (
11
+ Event,
12
+ Message,
13
+ RequestEvent,
14
+ ResponseEvent,
15
+ Tool,
16
+ )
17
+
18
+
19
+ class ListFiles(Tool):
20
+ description = """
21
+ List all files.
22
+ If you want to find all json files recuresively under directory a/b
23
+ the subdir should be a/b and pattern will be *.json
24
+
25
+ If you want to find all json files recuresively under current directory
26
+ the subdir should be . and pattern will be *.json
27
+ """
28
+
29
+ class Request(RequestEvent):
30
+ sub_dir: str
31
+ pattern: str = Field(..., description="Glob pattern. Avoid passing '*'")
32
+
33
+ class Response(ResponseEvent):
34
+ paths: List[str] = Field(
35
+ ...,
36
+ description="File paths under the given directory",
37
+ )
38
+ error: Optional[str] = Field(None)
39
+
40
+ def __init__(self, project_root_path: str):
41
+ self.project_root_path = project_root_path
42
+
43
+ def run(self, request: ListFiles.Request) -> Generator[Event, Any, ResponseEvent]:
44
+ yield Message(
45
+ message=f"[bold cyan]Searching:[/] 🔍 Looking for files matching the pattern [magenta]{request.pattern}[/]"
46
+ )
47
+
48
+ paths: List[str] = []
49
+
50
+ def gitignore(_):
51
+ return False
52
+
53
+ gitignore_path = os.path.join(self.project_root_path, ".gitignore")
54
+ if os.path.exists(gitignore_path):
55
+ gitignore = gitignorefile.parse(path=gitignore_path)
56
+
57
+ path = os.path.join(self.project_root_path, request.sub_dir.strip(os.path.sep))
58
+ if not os.path.exists(path):
59
+ return ListFiles.Response(
60
+ paths=None,
61
+ error=f"Incorrect sub_dir {request.sub_dir}. Does not exist",
62
+ )
63
+
64
+ for root, dirs, ps in os.walk(
65
+ path,
66
+ ):
67
+ root = root[len(path) :]
68
+ if ".git" in dirs:
69
+ dirs.remove(".git")
70
+ paths.extend(
71
+ os.path.join(root, p).lstrip(os.path.sep)
72
+ for p in ps
73
+ if fnmatch(p, request.pattern)
74
+ and not gitignore(os.path.join(root, p).strip(os.path.sep))
75
+ )
76
+ if len(paths) > 0:
77
+ yield Message(message=f"[bold green]Success:[/] Found {len(paths)} files.")
78
+ else:
79
+ yield Message(
80
+ message=f"[red]Alert:[/] No files found matching the pattern {request.pattern}."
81
+ )
82
+ return ListFiles.Response(paths=paths)
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from typing import Any, Generator, List, Optional
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+ from truefoundry.autodeploy.tools.base import (
9
+ Event,
10
+ Message,
11
+ RequestEvent,
12
+ ResponseEvent,
13
+ Tool,
14
+ )
15
+
16
+
17
+ class Line(BaseModel):
18
+ line_number: int
19
+ content: str
20
+
21
+
22
+ class ReadFile(Tool):
23
+ description = """
24
+ Read contents of a file.
25
+ Avoid reading *.lock type files as they tend to be large
26
+ """
27
+
28
+ class Request(RequestEvent):
29
+ path: str = Field(
30
+ ...,
31
+ pattern=r"^[a-zA-Z0-9\.]{1}.*$",
32
+ description="File path to open. ",
33
+ )
34
+
35
+ class Response(ResponseEvent):
36
+ data: Optional[List[Line]] = Field(
37
+ None,
38
+ description="Content of the file.",
39
+ )
40
+ error: Optional[str] = Field(
41
+ None,
42
+ description="Error while opening a file.",
43
+ )
44
+
45
+ def __init__(
46
+ self,
47
+ project_root_path: str,
48
+ ):
49
+ self.project_root_path = project_root_path
50
+
51
+ def run(self, request: ReadFile.Request) -> Generator[Event, Any, ResponseEvent]:
52
+ yield Message(
53
+ message=f"[bold cyan]Processing:[/] Reading file at [magenta]{request.path}[/] and extracting details..."
54
+ )
55
+ try:
56
+ with open(
57
+ os.path.join(self.project_root_path, request.path),
58
+ "r",
59
+ encoding="utf8",
60
+ ) as f:
61
+ response = ReadFile.Response(data=[])
62
+ for i, line in enumerate(f):
63
+ response.data.append(Line(line_number=i + 1, content=line))
64
+ return response
65
+ except FileNotFoundError as ex:
66
+ return ReadFile.Response(error=str(ex))
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Generator, Optional
4
+
5
+ import requests
6
+ from pydantic import Field
7
+
8
+ from truefoundry.autodeploy.tools.base import (
9
+ Event,
10
+ Message,
11
+ RequestEvent,
12
+ ResponseEvent,
13
+ Tool,
14
+ )
15
+
16
+
17
+ class SendRequest(Tool):
18
+ description = """
19
+ Send an HTTP request.
20
+ """
21
+
22
+ def __init__(self):
23
+ self.call_count = 0
24
+
25
+ class Request(RequestEvent):
26
+ method: str
27
+ url: str
28
+
29
+ class Response(ResponseEvent):
30
+ response_code: Optional[int] = Field(None, description="Response Code")
31
+ response_body: Optional[str] = None
32
+ error: Optional[str] = Field(None, description="Error.")
33
+
34
+ def run(self, request: SendRequest.Request) -> Generator[Event, Any, ResponseEvent]:
35
+ self.call_count += 1
36
+ yield Message(
37
+ message=f"[bold cyan]Testing:[/] Sending a [magenta]{request.method.upper()}[/] request to [magenta]{request.url}[/]..."
38
+ )
39
+ try:
40
+ response = requests.request(request.method.lower(), url=request.url)
41
+ yield Message(
42
+ message=f"[bold green]Success:[/] Received response with status code [magenta]{response.status_code}[/]"
43
+ )
44
+ return SendRequest.Response(
45
+ response_code=response.status_code,
46
+ response_body=response.text[-50:],
47
+ )
48
+ except Exception as ex:
49
+ yield Message(
50
+ message="[red]Alert:[/] Request could not be completed successfully."
51
+ )
52
+ return SendRequest.Response(
53
+ error=str(ex),
54
+ )
@@ -0,0 +1,101 @@
1
+ # deprecated
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ from typing import Optional
6
+
7
+ from pydantic import BaseModel, Field
8
+ from rich.console import Console
9
+ from rich.prompt import Confirm
10
+
11
+ from truefoundry.autodeploy.tools.base import Tool
12
+ from truefoundry.autodeploy.utils.diff import Diff
13
+
14
+ INTERACTIVE_SESSION = True
15
+
16
+
17
+ class WriteFile(Tool):
18
+ description = """
19
+ Write contents to a file.
20
+ Read first before write.
21
+ """
22
+
23
+ class Request(BaseModel):
24
+ path: str = Field(
25
+ ...,
26
+ pattern=r"^[a-zA-Z0-9\.]{1}.*$",
27
+ description="File path to write.",
28
+ )
29
+ content: str = Field(..., description="Content of the file.")
30
+ justification: str = Field(
31
+ ...,
32
+ description="Justification of why the new content is required. Ensure the justification, justifies the content.",
33
+ )
34
+
35
+ class Response(BaseModel):
36
+ cancellation_reason: Optional[str] = Field(
37
+ None, description="Operation cancelled by user"
38
+ )
39
+
40
+ error: Optional[str] = Field(
41
+ None,
42
+ description="Error while opening a file.",
43
+ )
44
+
45
+ def __init__(self, project_root_path: str):
46
+ self.project_root_path = project_root_path
47
+
48
+ def _user_interaction(
49
+ self, request: WriteFile.Request, console: Console
50
+ ) -> Optional[WriteFile.Response]:
51
+ console.log("You are about to write or edit a file.")
52
+
53
+ console.print(f"Displaying changes in {request.path}")
54
+ prev_content = ""
55
+
56
+ if os.path.exists(os.path.join(self.project_root_path, request.path)):
57
+ with open(
58
+ os.path.join(self.project_root_path, request.path),
59
+ "r",
60
+ encoding="utf8",
61
+ ) as f:
62
+ prev_content = f.read()
63
+
64
+ console.print(
65
+ Diff(
66
+ lhs=prev_content,
67
+ rhs=request.content,
68
+ width=os.get_terminal_size().columns,
69
+ )
70
+ )
71
+ response = Confirm.ask(
72
+ f"Writing file at {request.path}?",
73
+ )
74
+ if not response:
75
+ description = console.input(
76
+ "You chose to cancel. Can you provide a reason why? [green]>> "
77
+ )
78
+ return WriteFile.Response(
79
+ cancellation_reason=description, error="Operation cancelled by user."
80
+ )
81
+ console.log(f"Writing file to {request.path}")
82
+
83
+ def run(
84
+ self,
85
+ request: WriteFile.Request,
86
+ console: Console,
87
+ ) -> WriteFile.Response:
88
+ if INTERACTIVE_SESSION:
89
+ interaction_response = self._user_interaction(request, console)
90
+ if isinstance(interaction_response, WriteFile.Response):
91
+ return interaction_response
92
+ try:
93
+ with open(
94
+ os.path.join(self.project_root_path, request.path),
95
+ "w",
96
+ encoding="utf8",
97
+ ) as f:
98
+ f.write(request.content)
99
+ return WriteFile.Response()
100
+ except FileNotFoundError as ex:
101
+ return WriteFile.Response(error=str(ex))