main
py 199 lines 4.8 KB
Raw
1 import argparse
2 import inspect
3 import secrets
4 from pathlib import Path
5 from typing import TypeVar, Callable, Awaitable, Union, overload, cast
6 from helpers import dotenv, rfc, files
7 import asyncio
8 import threading
9 import queue
10 import sys
11 import nest_asyncio
12
13 nest_asyncio.apply()
14
15 T = TypeVar("T")
16 R = TypeVar("R")
17
18 parser = argparse.ArgumentParser()
19 args = {}
20 dockerman = None
21 runtime_id = None
22
23
24 def initialize():
25 global args
26 if args:
27 return
28 parser.add_argument("--port", type=int, default=None, help="Web UI port")
29 parser.add_argument("--host", type=str, default=None, help="Web UI host")
30 parser.add_argument(
31 "--cloudflare_tunnel",
32 type=bool,
33 default=False,
34 help="Use Cloudflare Tunnel for public URL",
35 )
36 parser.add_argument(
37 "--development", type=bool, default=False, help="Development mode"
38 )
39
40 known, unknown = parser.parse_known_args()
41 args = vars(known)
42 for arg in unknown:
43 if "=" in arg:
44 key, value = arg.split("=", 1)
45 key = key.lstrip("-")
46 args[key] = value
47
48
49 def get_arg(name: str):
50 global args
51 return args.get(name, None)
52
53
54 def has_arg(name: str):
55 global args
56 return name in args
57
58
59 def is_dockerized() -> bool:
60 return bool(get_arg("dockerized"))
61
62
63 def is_development() -> bool:
64 return not is_dockerized()
65
66
67 def get_local_url():
68 if is_dockerized():
69 return "host.docker.internal"
70 return "127.0.0.1"
71
72
73 def get_runtime_id() -> str:
74 global runtime_id
75 if not runtime_id:
76 runtime_id = secrets.token_hex(8)
77 return runtime_id
78
79
80 def get_persistent_id() -> str:
81 id = dotenv.get_dotenv_value("A0_PERSISTENT_RUNTIME_ID")
82 if not id:
83 id = secrets.token_hex(16)
84 dotenv.save_dotenv_value("A0_PERSISTENT_RUNTIME_ID", id)
85 return id
86
87
88 @overload
89 async def call_development_function(
90 func: Callable[..., Awaitable[T]], *args, **kwargs
91 ) -> T: ...
92
93
94 @overload
95 async def call_development_function(func: Callable[..., T], *args, **kwargs) -> T: ...
96
97
98 async def call_development_function(
99 func: Union[Callable[..., T], Callable[..., Awaitable[T]]], *args, **kwargs
100 ) -> T:
101 if is_development():
102 url = _get_rfc_url()
103 password = _get_rfc_password()
104 # Normalize path components to build a valid Python module path across OSes
105 module_path = Path(
106 files.deabsolute_path(func.__code__.co_filename)
107 ).with_suffix("")
108 module = ".".join(module_path.parts) # __module__ is not reliable
109 result = await rfc.call_rfc(
110 url=url,
111 password=password,
112 module=module,
113 function_name=func.__name__,
114 args=list(args),
115 kwargs=kwargs,
116 )
117 return cast(T, result)
118 else:
119 if inspect.iscoroutinefunction(func):
120 return await func(*args, **kwargs)
121 else:
122 return func(*args, **kwargs) # type: ignore
123
124
125 async def handle_rfc(rfc_call: rfc.RFCCall):
126 return await rfc.handle_rfc(rfc_call=rfc_call, password=_get_rfc_password())
127
128
129 def _get_rfc_password() -> str:
130 password = dotenv.get_dotenv_value(dotenv.KEY_RFC_PASSWORD)
131 if not password:
132 raise Exception("No RFC password, cannot handle RFC calls.")
133 return password
134
135
136 def _get_rfc_url() -> str:
137 # Delay import to avoid a circular import with helpers.settings.
138 from helpers import settings
139 set = settings.get_settings()
140 url = set["rfc_url"]
141 if not "://" in url:
142 url = "http://" + url
143 if url.endswith("/"):
144 url = url[:-1]
145 url = url + ":" + str(set["rfc_port_http"])
146 url += "/api/rfc"
147 return url
148
149
150 def call_development_function_sync(
151 func: Union[Callable[..., T], Callable[..., Awaitable[T]]], *args, **kwargs
152 ) -> T:
153 # run async function in sync manner
154 result_queue = queue.Queue()
155
156 def run_in_thread():
157 result = asyncio.run(call_development_function(func, *args, **kwargs))
158 result_queue.put(result)
159
160 thread = threading.Thread(target=run_in_thread)
161 thread.start()
162 thread.join(timeout=30) # wait for thread with timeout
163
164 if thread.is_alive():
165 raise TimeoutError("Function call timed out after 30 seconds")
166
167 result = result_queue.get_nowait()
168 return cast(T, result)
169
170
171 def get_web_ui_port():
172 web_ui_port = (
173 get_arg("port") or int(dotenv.get_dotenv_value("WEB_UI_PORT", 0)) or 5000
174 )
175 return web_ui_port
176
177
178 def get_tunnel_api_port():
179 tunnel_api_port = (
180 get_arg("tunnel_api_port")
181 or int(dotenv.get_dotenv_value("TUNNEL_API_PORT", 0))
182 or 55520
183 )
184 return tunnel_api_port
185
186
187 def get_platform():
188 return sys.platform
189
190
191 def is_windows():
192 return get_platform() == "win32"
193
194
195 def get_terminal_executable():
196 if is_windows():
197 return "powershell.exe"
198 else:
199 return "/bin/bash"