utils.py 40 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592
  1. """General utility functions."""
  2. from __future__ import annotations
  3. import contextlib
  4. import inspect
  5. import json
  6. import os
  7. import platform
  8. import random
  9. import re
  10. import shutil
  11. import signal
  12. import string
  13. import subprocess
  14. import sys
  15. from collections import defaultdict
  16. from pathlib import Path
  17. from subprocess import DEVNULL, PIPE, STDOUT
  18. from types import ModuleType
  19. from typing import (
  20. TYPE_CHECKING,
  21. Any,
  22. Callable,
  23. Dict,
  24. List,
  25. Optional,
  26. Tuple,
  27. Type,
  28. Union,
  29. _GenericAlias, # type: ignore # type: ignore
  30. )
  31. from urllib.parse import urlparse
  32. import plotly.graph_objects as go
  33. import psutil
  34. import typer
  35. import uvicorn
  36. from plotly.io import to_json
  37. from redis import Redis
  38. from rich.console import Console
  39. from rich.prompt import Prompt
  40. from pynecone import constants
  41. from pynecone.base import Base
  42. if TYPE_CHECKING:
  43. from pynecone.app import App
  44. from pynecone.components.component import ImportDict
  45. from pynecone.config import Config
  46. from pynecone.event import Event, EventHandler, EventSpec
  47. from pynecone.var import Var
  48. # Shorthand for join.
  49. join = os.linesep.join
  50. # Console for pretty printing.
  51. console = Console()
  52. # Union of generic types.
  53. GenericType = Union[Type, _GenericAlias]
  54. # Valid state var types.
  55. PrimitiveType = Union[int, float, bool, str, list, dict, tuple]
  56. StateVar = Union[PrimitiveType, Base, None]
  57. def deprecate(msg: str):
  58. """Print a deprecation warning.
  59. Args:
  60. msg: The deprecation message.
  61. """
  62. console.print(f"[yellow]DeprecationWarning: {msg}[/yellow]")
  63. def get_args(alias: _GenericAlias) -> Tuple[Type, ...]:
  64. """Get the arguments of a type alias.
  65. Args:
  66. alias: The type alias.
  67. Returns:
  68. The arguments of the type alias.
  69. """
  70. return alias.__args__
  71. def is_generic_alias(cls: GenericType) -> bool:
  72. """Check whether the class is a generic alias.
  73. Args:
  74. cls: The class to check.
  75. Returns:
  76. Whether the class is a generic alias.
  77. """
  78. # For older versions of Python.
  79. if isinstance(cls, _GenericAlias):
  80. return True
  81. with contextlib.suppress(ImportError):
  82. from typing import _SpecialGenericAlias # type: ignore
  83. if isinstance(cls, _SpecialGenericAlias):
  84. return True
  85. # For newer versions of Python.
  86. try:
  87. from types import GenericAlias # type: ignore
  88. return isinstance(cls, GenericAlias)
  89. except ImportError:
  90. return False
  91. def is_union(cls: GenericType) -> bool:
  92. """Check if a class is a Union.
  93. Args:
  94. cls: The class to check.
  95. Returns:
  96. Whether the class is a Union.
  97. """
  98. with contextlib.suppress(ImportError):
  99. from typing import _UnionGenericAlias # type: ignore
  100. return isinstance(cls, _UnionGenericAlias)
  101. return cls.__origin__ == Union if is_generic_alias(cls) else False
  102. def get_base_class(cls: GenericType) -> Type:
  103. """Get the base class of a class.
  104. Args:
  105. cls: The class.
  106. Returns:
  107. The base class of the class.
  108. """
  109. if is_union(cls):
  110. return tuple(get_base_class(arg) for arg in get_args(cls))
  111. return get_base_class(cls.__origin__) if is_generic_alias(cls) else cls
  112. def _issubclass(cls: GenericType, cls_check: GenericType) -> bool:
  113. """Check if a class is a subclass of another class.
  114. Args:
  115. cls: The class to check.
  116. cls_check: The class to check against.
  117. Returns:
  118. Whether the class is a subclass of the other class.
  119. """
  120. # Special check for Any.
  121. if cls_check == Any:
  122. return True
  123. if cls in [Any, Callable]:
  124. return False
  125. # Get the base classes.
  126. cls_base = get_base_class(cls)
  127. cls_check_base = get_base_class(cls_check)
  128. # The class we're checking should not be a union.
  129. if isinstance(cls_base, tuple):
  130. return False
  131. # Check if the types match.
  132. return cls_check_base == Any or issubclass(cls_base, cls_check_base)
  133. def _isinstance(obj: Any, cls: GenericType) -> bool:
  134. """Check if an object is an instance of a class.
  135. Args:
  136. obj: The object to check.
  137. cls: The class to check against.
  138. Returns:
  139. Whether the object is an instance of the class.
  140. """
  141. return isinstance(obj, get_base_class(cls))
  142. def rm(path: str):
  143. """Remove a file or directory.
  144. Args:
  145. path: The path to the file or directory.
  146. """
  147. if os.path.isdir(path):
  148. shutil.rmtree(path)
  149. elif os.path.isfile(path):
  150. os.remove(path)
  151. def cp(src: str, dest: str, overwrite: bool = True) -> bool:
  152. """Copy a file or directory.
  153. Args:
  154. src: The path to the file or directory.
  155. dest: The path to the destination.
  156. overwrite: Whether to overwrite the destination.
  157. Returns:
  158. Whether the copy was successful.
  159. """
  160. if src == dest:
  161. return False
  162. if not overwrite and os.path.exists(dest):
  163. return False
  164. if os.path.isdir(src):
  165. rm(dest)
  166. shutil.copytree(src, dest)
  167. else:
  168. shutil.copyfile(src, dest)
  169. return True
  170. def mv(src: str, dest: str, overwrite: bool = True) -> bool:
  171. """Move a file or directory.
  172. Args:
  173. src: The path to the file or directory.
  174. dest: The path to the destination.
  175. overwrite: Whether to overwrite the destination.
  176. Returns:
  177. Whether the move was successful.
  178. """
  179. if src == dest:
  180. return False
  181. if not overwrite and os.path.exists(dest):
  182. return False
  183. rm(dest)
  184. shutil.move(src, dest)
  185. return True
  186. def mkdir(path: str):
  187. """Create a directory.
  188. Args:
  189. path: The path to the directory.
  190. """
  191. if not os.path.exists(path):
  192. os.makedirs(path)
  193. def ln(src: str, dest: str, overwrite: bool = False) -> bool:
  194. """Create a symbolic link.
  195. Args:
  196. src: The path to the file or directory.
  197. dest: The path to the destination.
  198. overwrite: Whether to overwrite the destination.
  199. Returns:
  200. Whether the link was successful.
  201. """
  202. if src == dest:
  203. return False
  204. if not overwrite and (os.path.exists(dest) or os.path.islink(dest)):
  205. return False
  206. if os.path.isdir(src):
  207. rm(dest)
  208. os.symlink(src, dest, target_is_directory=True)
  209. else:
  210. os.symlink(src, dest)
  211. return True
  212. def kill(pid):
  213. """Kill a process.
  214. Args:
  215. pid: The process ID.
  216. """
  217. os.kill(pid, signal.SIGTERM)
  218. def which(program: str) -> Optional[str]:
  219. """Find the path to an executable.
  220. Args:
  221. program: The name of the executable.
  222. Returns:
  223. The path to the executable.
  224. """
  225. return shutil.which(program)
  226. def get_config() -> Config:
  227. """Get the app config.
  228. Returns:
  229. The app config.
  230. """
  231. from pynecone.config import Config
  232. sys.path.append(os.getcwd())
  233. try:
  234. return __import__(constants.CONFIG_MODULE).config
  235. except ImportError:
  236. return Config(app_name="") # type: ignore
  237. def check_node_version(min_version):
  238. """Check the version of Node.js.
  239. Args:
  240. min_version: The minimum version of Node.js required.
  241. Returns:
  242. Whether the version of Node.js is high enough.
  243. """
  244. try:
  245. # Run the node -v command and capture the output
  246. result = subprocess.run(
  247. ["node", "-v"], stdout=subprocess.PIPE, stderr=subprocess.PIPE
  248. )
  249. # The output will be in the form "vX.Y.Z", so we can split it on the "v" character and take the second part
  250. version = result.stdout.decode().strip().split("v")[1]
  251. # Compare the version numbers
  252. return version.split(".") >= min_version.split(".")
  253. except Exception:
  254. return False
  255. def get_package_manager() -> str:
  256. """Get the package manager executable.
  257. Returns:
  258. The path to the package manager.
  259. Raises:
  260. FileNotFoundError: If bun or npm is not installed.
  261. Exit: If the app directory is invalid.
  262. """
  263. # Check that the node version is valid.
  264. if not check_node_version(constants.MIN_NODE_VERSION):
  265. console.print(
  266. f"[red]Node.js version {constants.MIN_NODE_VERSION} or higher is required to run Pynecone."
  267. )
  268. raise typer.Exit()
  269. # On Windows, we use npm instead of bun.
  270. if platform.system() == "Windows":
  271. npm_path = which("npm")
  272. if npm_path is None:
  273. raise FileNotFoundError("Pynecone requires npm to be installed on Windows.")
  274. return npm_path
  275. # On other platforms, we use bun.
  276. return os.path.expandvars(get_config().bun_path)
  277. def get_app() -> ModuleType:
  278. """Get the app module based on the default config.
  279. Returns:
  280. The app based on the default config.
  281. """
  282. config = get_config()
  283. module = ".".join([config.app_name, config.app_name])
  284. sys.path.insert(0, os.getcwd())
  285. return __import__(module, fromlist=(constants.APP_VAR,))
  286. def create_config(app_name: str):
  287. """Create a new pcconfig file.
  288. Args:
  289. app_name: The name of the app.
  290. """
  291. # Import here to avoid circular imports.
  292. from pynecone.compiler import templates
  293. with open(constants.CONFIG_FILE, "w") as f:
  294. f.write(templates.PCCONFIG.format(app_name=app_name))
  295. def initialize_gitignore():
  296. """Initialize the template .gitignore file."""
  297. # The files to add to the .gitignore file.
  298. files = constants.DEFAULT_GITIGNORE
  299. # Subtract current ignored files.
  300. if os.path.exists(constants.GITIGNORE_FILE):
  301. with open(constants.GITIGNORE_FILE, "r") as f:
  302. files -= set(f.read().splitlines())
  303. # Add the new files to the .gitignore file.
  304. with open(constants.GITIGNORE_FILE, "a") as f:
  305. f.write(join(files))
  306. def initialize_app_directory(app_name: str):
  307. """Initialize the app directory on pc init.
  308. Args:
  309. app_name: The name of the app.
  310. """
  311. console.log("Initializing the app directory.")
  312. cp(constants.APP_TEMPLATE_DIR, app_name)
  313. mv(
  314. os.path.join(app_name, constants.APP_TEMPLATE_FILE),
  315. os.path.join(app_name, app_name + constants.PY_EXT),
  316. )
  317. cp(constants.ASSETS_TEMPLATE_DIR, constants.APP_ASSETS_DIR)
  318. def initialize_web_directory():
  319. """Initialize the web directory on pc init."""
  320. console.log("Initializing the web directory.")
  321. rm(os.path.join(constants.WEB_TEMPLATE_DIR, constants.NODE_MODULES))
  322. rm(os.path.join(constants.WEB_TEMPLATE_DIR, constants.PACKAGE_LOCK))
  323. cp(constants.WEB_TEMPLATE_DIR, constants.WEB_DIR)
  324. def install_bun():
  325. """Install bun onto the user's system.
  326. Raises:
  327. FileNotFoundError: If the required packages are not installed.
  328. """
  329. # Bun is not supported on Windows.
  330. if platform.system() == "Windows":
  331. console.log("Skipping bun installation on Windows.")
  332. return
  333. # Only install if bun is not already installed.
  334. if not os.path.exists(get_package_manager()):
  335. console.log("Installing bun...")
  336. # Check if curl is installed
  337. curl_path = which("curl")
  338. if curl_path is None:
  339. raise FileNotFoundError("Pynecone requires curl to be installed.")
  340. # Check if unzip is installed
  341. unzip_path = which("unzip")
  342. if unzip_path is None:
  343. raise FileNotFoundError("Pynecone requires unzip to be installed.")
  344. os.system(constants.INSTALL_BUN)
  345. def install_frontend_packages():
  346. """Install the frontend packages."""
  347. # Install the base packages.
  348. subprocess.run(
  349. [get_package_manager(), "install"], cwd=constants.WEB_DIR, stdout=PIPE
  350. )
  351. # Install the app packages.
  352. packages = get_config().frontend_packages
  353. if len(packages) > 0:
  354. subprocess.run(
  355. [get_package_manager(), "add", *packages],
  356. cwd=constants.WEB_DIR,
  357. stdout=PIPE,
  358. )
  359. def is_initialized() -> bool:
  360. """Check whether the app is initialized.
  361. Returns:
  362. Whether the app is initialized in the current directory.
  363. """
  364. return os.path.exists(constants.CONFIG_FILE) and os.path.exists(constants.WEB_DIR)
  365. def is_latest_template() -> bool:
  366. """Whether the app is using the latest template.
  367. Returns:
  368. Whether the app is using the latest template.
  369. """
  370. with open(constants.PCVERSION_TEMPLATE_FILE) as f: # type: ignore
  371. template_version = json.load(f)["version"]
  372. if not os.path.exists(constants.PCVERSION_APP_FILE):
  373. return False
  374. with open(constants.PCVERSION_APP_FILE) as f: # type: ignore
  375. app_version = json.load(f)["version"]
  376. return app_version >= template_version
  377. def set_pynecone_project_hash():
  378. """Write the hash of the Pynecone project to a PCVERSION_APP_FILE."""
  379. with open(constants.PCVERSION_APP_FILE) as f: # type: ignore
  380. pynecone_json = json.load(f)
  381. pynecone_json["project_hash"] = random.getrandbits(128)
  382. with open(constants.PCVERSION_APP_FILE, "w") as f:
  383. json.dump(pynecone_json, f, ensure_ascii=False)
  384. def generate_sitemap(deploy_url: str):
  385. """Generate the sitemap config file.
  386. Args:
  387. deploy_url: The URL of the deployed app.
  388. """
  389. # Import here to avoid circular imports.
  390. from pynecone.compiler import templates
  391. config = json.dumps(
  392. {
  393. "siteUrl": deploy_url,
  394. "generateRobotsTxt": True,
  395. }
  396. )
  397. with open(constants.SITEMAP_CONFIG_FILE, "w") as f:
  398. f.write(templates.SITEMAP_CONFIG(config=config))
  399. def export_app(
  400. app: App,
  401. backend: bool = True,
  402. frontend: bool = True,
  403. zip: bool = False,
  404. deploy_url: Optional[str] = None,
  405. ):
  406. """Zip up the app for deployment.
  407. Args:
  408. app: The app.
  409. backend: Whether to zip up the backend app.
  410. frontend: Whether to zip up the frontend app.
  411. zip: Whether to zip the app.
  412. deploy_url: The URL of the deployed app.
  413. """
  414. # Force compile the app.
  415. app.compile(force_compile=True)
  416. # Remove the static folder.
  417. rm(constants.WEB_STATIC_DIR)
  418. # Generate the sitemap file.
  419. if deploy_url is not None:
  420. generate_sitemap(deploy_url)
  421. # Export the Next app.
  422. subprocess.run([get_package_manager(), "run", "export"], cwd=constants.WEB_DIR)
  423. # Zip up the app.
  424. if zip:
  425. if os.name == "posix":
  426. posix_export(backend, frontend)
  427. if os.name == "nt":
  428. nt_export(backend, frontend)
  429. def nt_export(backend: bool = True, frontend: bool = True):
  430. """Export for nt (Windows) systems.
  431. Args:
  432. backend: Whether to zip up the backend app.
  433. frontend: Whether to zip up the frontend app.
  434. """
  435. cmd = r""
  436. if frontend:
  437. cmd = r'''powershell -Command "Set-Location .web/_static; Compress-Archive -Path .\* -DestinationPath ..\..\frontend.zip -Force"'''
  438. os.system(cmd)
  439. if backend:
  440. cmd = r'''powershell -Command "Get-ChildItem -File | Where-Object { $_.Name -notin @('.web', 'assets', 'frontend.zip', 'backend.zip') } | Compress-Archive -DestinationPath backend.zip -Update"'''
  441. os.system(cmd)
  442. def posix_export(backend: bool = True, frontend: bool = True):
  443. """Export for posix (Linux, OSX) systems.
  444. Args:
  445. backend: Whether to zip up the backend app.
  446. frontend: Whether to zip up the frontend app.
  447. """
  448. cmd = r""
  449. if frontend:
  450. cmd = r"cd .web/_static && zip -r ../../frontend.zip ./*"
  451. os.system(cmd)
  452. if backend:
  453. cmd = r"zip -r backend.zip ./* -x .web/\* ./assets\* ./frontend.zip\* ./backend.zip\*"
  454. os.system(cmd)
  455. def setup_frontend(root: Path):
  456. """Set up the frontend.
  457. Args:
  458. root: root path of the project.
  459. """
  460. # Initialize the web directory if it doesn't exist.
  461. cp(constants.WEB_TEMPLATE_DIR, str(root / constants.WEB_DIR), overwrite=False)
  462. # Install the frontend packages.
  463. console.rule("[bold]Installing frontend packages")
  464. install_frontend_packages()
  465. # copy asset files to public folder
  466. mkdir(str(root / constants.WEB_ASSETS_DIR))
  467. cp(
  468. src=str(root / constants.APP_ASSETS_DIR),
  469. dest=str(root / constants.WEB_ASSETS_DIR),
  470. )
  471. def run_frontend(app: App, root: Path, port: str):
  472. """Run the frontend.
  473. Args:
  474. app: The app.
  475. root: root path of the project.
  476. port: port of the app.
  477. """
  478. # Set up the frontend.
  479. setup_frontend(root)
  480. # Compile the frontend.
  481. app.compile(force_compile=True)
  482. # Run the frontend in development mode.
  483. console.rule("[bold green]App Running")
  484. os.environ["PORT"] = get_config().port if port is None else port
  485. subprocess.Popen(
  486. [get_package_manager(), "run", "next", "telemetry", "disable"],
  487. cwd=constants.WEB_DIR,
  488. env=os.environ,
  489. stdout=DEVNULL,
  490. stderr=STDOUT,
  491. )
  492. subprocess.Popen(
  493. [get_package_manager(), "run", "dev"], cwd=constants.WEB_DIR, env=os.environ
  494. )
  495. def run_frontend_prod(app: App, root: Path, port: str):
  496. """Run the frontend.
  497. Args:
  498. app: The app.
  499. root: root path of the project.
  500. port: port of the app.
  501. """
  502. # Set up the frontend.
  503. setup_frontend(root)
  504. # Export the app.
  505. export_app(app)
  506. os.environ["PORT"] = get_config().port if port is None else port
  507. # Run the frontend in production mode.
  508. subprocess.Popen(
  509. [get_package_manager(), "run", "prod"], cwd=constants.WEB_DIR, env=os.environ
  510. )
  511. def get_num_workers() -> int:
  512. """Get the number of backend worker processes.
  513. Returns:
  514. The number of backend worker processes.
  515. """
  516. return 1 if get_redis() is None else (os.cpu_count() or 1) * 2 + 1
  517. def get_api_port() -> int:
  518. """Get the API port.
  519. Returns:
  520. The API port.
  521. """
  522. port = urlparse(get_config().api_url).port
  523. if port is None:
  524. port = urlparse(constants.API_URL).port
  525. assert port is not None
  526. return port
  527. def get_process_on_port(port) -> Optional[psutil.Process]:
  528. """Get the process on the given port.
  529. Args:
  530. port: The port.
  531. Returns:
  532. The process on the given port.
  533. """
  534. for proc in psutil.process_iter(["pid", "name", "cmdline"]):
  535. try:
  536. for conns in proc.connections(kind="inet"):
  537. if conns.laddr.port == int(port):
  538. return proc
  539. except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
  540. pass
  541. return None
  542. def is_process_on_port(port) -> bool:
  543. """Check if a process is running on the given port.
  544. Args:
  545. port: The port.
  546. Returns:
  547. Whether a process is running on the given port.
  548. """
  549. return get_process_on_port(port) is not None
  550. def kill_process_on_port(port):
  551. """Kill the process on the given port.
  552. Args:
  553. port: The port.
  554. """
  555. if get_process_on_port(port) is not None:
  556. with contextlib.suppress(psutil.AccessDenied):
  557. get_process_on_port(port).kill() # type: ignore
  558. def change_or_terminate_port(port, _type) -> str:
  559. """Terminate or change the port.
  560. Args:
  561. port: The port.
  562. _type: The type of the port.
  563. Returns:
  564. The new port or the current one.
  565. """
  566. console.print(
  567. f"Something is already running on port [bold underline]{port}[/bold underline]. This is the port the {_type} runs on."
  568. )
  569. frontend_action = Prompt.ask("Kill or change it?", choices=["k", "c", "n"])
  570. if frontend_action == "k":
  571. kill_process_on_port(port)
  572. return port
  573. elif frontend_action == "c":
  574. new_port = Prompt.ask("Specify the new port")
  575. # Check if also the new port is used
  576. if is_process_on_port(new_port):
  577. return change_or_terminate_port(new_port, _type)
  578. else:
  579. console.print(
  580. f"The {_type} will run on port [bold underline]{new_port}[/bold underline]."
  581. )
  582. return new_port
  583. else:
  584. console.print("Exiting...")
  585. sys.exit()
  586. def setup_backend():
  587. """Set up backend.
  588. Specifically ensures backend database is updated when running --no-frontend.
  589. """
  590. # Import here to avoid circular imports.
  591. from pynecone.model import Model
  592. config = get_config()
  593. if config.db_url is not None:
  594. Model.create_all()
  595. def run_backend(
  596. app_name: str, port: int, loglevel: constants.LogLevel = constants.LogLevel.ERROR
  597. ):
  598. """Run the backend.
  599. Args:
  600. app_name: The app name.
  601. port: The app port
  602. loglevel: The log level.
  603. """
  604. setup_backend()
  605. uvicorn.run(
  606. f"{app_name}:{constants.APP_VAR}.{constants.API_VAR}",
  607. host=constants.BACKEND_HOST,
  608. port=port,
  609. log_level=loglevel,
  610. reload=True,
  611. )
  612. def run_backend_prod(
  613. app_name: str, port: int, loglevel: constants.LogLevel = constants.LogLevel.ERROR
  614. ):
  615. """Run the backend.
  616. Args:
  617. app_name: The app name.
  618. port: The app port
  619. loglevel: The log level.
  620. """
  621. setup_backend()
  622. num_workers = get_num_workers()
  623. command = (
  624. [
  625. *constants.RUN_BACKEND_PROD_WINDOWS,
  626. "--host",
  627. "0.0.0.0",
  628. "--port",
  629. str(port),
  630. f"{app_name}:{constants.APP_VAR}",
  631. ]
  632. if platform.system() == "Windows"
  633. else [
  634. *constants.RUN_BACKEND_PROD,
  635. "--bind",
  636. f"0.0.0.0:{port}",
  637. "--threads",
  638. str(num_workers),
  639. f"{app_name}:{constants.APP_VAR}()",
  640. ]
  641. )
  642. command += [
  643. "--log-level",
  644. loglevel.value,
  645. "--workers",
  646. str(num_workers),
  647. ]
  648. subprocess.run(command)
  649. def get_production_backend_url() -> str:
  650. """Get the production backend URL.
  651. Returns:
  652. The production backend URL.
  653. """
  654. config = get_config()
  655. return constants.PRODUCTION_BACKEND_URL.format(
  656. username=config.username,
  657. app_name=config.app_name,
  658. )
  659. def to_snake_case(text: str) -> str:
  660. """Convert a string to snake case.
  661. The words in the text are converted to lowercase and
  662. separated by underscores.
  663. Args:
  664. text: The string to convert.
  665. Returns:
  666. The snake case string.
  667. """
  668. s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", text)
  669. return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
  670. def to_camel_case(text: str) -> str:
  671. """Convert a string to camel case.
  672. The first word in the text is converted to lowercase and
  673. the rest of the words are converted to title case, removing underscores.
  674. Args:
  675. text: The string to convert.
  676. Returns:
  677. The camel case string.
  678. """
  679. if "_" not in text:
  680. return text
  681. camel = "".join(
  682. word.capitalize() if i > 0 else word.lower()
  683. for i, word in enumerate(text.lstrip("_").split("_"))
  684. )
  685. prefix = "_" if text.startswith("_") else ""
  686. return prefix + camel
  687. def to_title_case(text: str) -> str:
  688. """Convert a string from snake case to title case.
  689. Args:
  690. text: The string to convert.
  691. Returns:
  692. The title case string.
  693. """
  694. return "".join(word.capitalize() for word in text.split("_"))
  695. WRAP_MAP = {
  696. "{": "}",
  697. "(": ")",
  698. "[": "]",
  699. "<": ">",
  700. '"': '"',
  701. "'": "'",
  702. "`": "`",
  703. }
  704. def get_close_char(open: str, close: Optional[str] = None) -> str:
  705. """Check if the given character is a valid brace.
  706. Args:
  707. open: The open character.
  708. close: The close character if provided.
  709. Returns:
  710. The close character.
  711. Raises:
  712. ValueError: If the open character is not a valid brace.
  713. """
  714. if close is not None:
  715. return close
  716. if open not in WRAP_MAP:
  717. raise ValueError(f"Invalid wrap open: {open}, must be one of {WRAP_MAP.keys()}")
  718. return WRAP_MAP[open]
  719. def is_wrapped(text: str, open: str, close: Optional[str] = None) -> bool:
  720. """Check if the given text is wrapped in the given open and close characters.
  721. Args:
  722. text: The text to check.
  723. open: The open character.
  724. close: The close character.
  725. Returns:
  726. Whether the text is wrapped.
  727. """
  728. close = get_close_char(open, close)
  729. return text.startswith(open) and text.endswith(close)
  730. def wrap(
  731. text: str,
  732. open: str,
  733. close: Optional[str] = None,
  734. check_first: bool = True,
  735. num: int = 1,
  736. ) -> str:
  737. """Wrap the given text in the given open and close characters.
  738. Args:
  739. text: The text to wrap.
  740. open: The open character.
  741. close: The close character.
  742. check_first: Whether to check if the text is already wrapped.
  743. num: The number of times to wrap the text.
  744. Returns:
  745. The wrapped text.
  746. """
  747. close = get_close_char(open, close)
  748. # If desired, check if the text is already wrapped in braces.
  749. if check_first and is_wrapped(text=text, open=open, close=close):
  750. return text
  751. # Wrap the text in braces.
  752. return f"{open * num}{text}{close * num}"
  753. def indent(text: str, indent_level: int = 2) -> str:
  754. """Indent the given text by the given indent level.
  755. Args:
  756. text: The text to indent.
  757. indent_level: The indent level.
  758. Returns:
  759. The indented text.
  760. """
  761. lines = text.splitlines()
  762. if len(lines) < 2:
  763. return text
  764. return os.linesep.join(f"{' ' * indent_level}{line}" for line in lines) + os.linesep
  765. def verify_route_validity(route: str) -> None:
  766. """Verify if the route is valid, and throw an error if not.
  767. Args:
  768. route: The route that need to be checked
  769. Raises:
  770. ValueError: If the route is invalid.
  771. """
  772. pattern = catchall_in_route(route)
  773. if pattern and not route.endswith(pattern):
  774. raise ValueError(f"Catch-all must be the last part of the URL: {route}")
  775. def get_route_args(route: str) -> Dict[str, str]:
  776. """Get the dynamic arguments for the given route.
  777. Args:
  778. route: The route to get the arguments for.
  779. Returns:
  780. The route arguments.
  781. """
  782. args = {}
  783. def add_route_arg(match: re.Match[str], type_: str):
  784. """Add arg from regex search result.
  785. Args:
  786. match: Result of a regex search
  787. type_: The assigned type for this arg
  788. Raises:
  789. ValueError: If the route is invalid.
  790. """
  791. arg_name = match.groups()[0]
  792. if arg_name in args:
  793. raise ValueError(
  794. f"Arg name [{arg_name}] is used more than once in this URL"
  795. )
  796. args[arg_name] = type_
  797. # Regex to check for route args.
  798. check = constants.RouteRegex.ARG
  799. check_strict_catchall = constants.RouteRegex.STRICT_CATCHALL
  800. check_opt_catchall = constants.RouteRegex.OPT_CATCHALL
  801. # Iterate over the route parts and check for route args.
  802. for part in route.split("/"):
  803. match_opt = check_opt_catchall.match(part)
  804. if match_opt:
  805. add_route_arg(match_opt, constants.RouteArgType.LIST)
  806. break
  807. match_strict = check_strict_catchall.match(part)
  808. if match_strict:
  809. add_route_arg(match_strict, constants.RouteArgType.LIST)
  810. break
  811. match = check.match(part)
  812. if match:
  813. # Add the route arg to the list.
  814. add_route_arg(match, constants.RouteArgType.SINGLE)
  815. return args
  816. def catchall_in_route(route: str) -> str:
  817. """Extract the catchall part from a route.
  818. Args:
  819. route: the route from which to extract
  820. Returns:
  821. str: the catchall part of the URI
  822. """
  823. match_ = constants.RouteRegex.CATCHALL.search(route)
  824. return match_.group() if match_ else ""
  825. def catchall_prefix(route: str) -> str:
  826. """Extract the prefix part from a route that contains a catchall.
  827. Args:
  828. route: the route from which to extract
  829. Returns:
  830. str: the prefix part of the URI
  831. """
  832. pattern = catchall_in_route(route)
  833. return route.replace(pattern, "") if pattern else ""
  834. def format_route(route: str) -> str:
  835. """Format the given route.
  836. Args:
  837. route: The route to format.
  838. Returns:
  839. The formatted route.
  840. """
  841. # Strip the route.
  842. route = route.strip("/")
  843. route = to_snake_case(route).replace("_", "-")
  844. # If the route is empty, return the index route.
  845. if route == "":
  846. return constants.INDEX_ROUTE
  847. return route
  848. def format_cond(
  849. cond: str,
  850. true_value: str,
  851. false_value: str = '""',
  852. is_prop=False,
  853. ) -> str:
  854. """Format a conditional expression.
  855. Args:
  856. cond: The cond.
  857. true_value: The value to return if the cond is true.
  858. false_value: The value to return if the cond is false.
  859. is_prop: Whether the cond is a prop
  860. Returns:
  861. The formatted conditional expression.
  862. """
  863. # Import here to avoid circular imports.
  864. from pynecone.var import Var
  865. # Format prop conds.
  866. if is_prop:
  867. prop1 = Var.create(true_value, is_string=type(true_value) == str)
  868. prop2 = Var.create(false_value, is_string=type(false_value) == str)
  869. assert prop1 is not None and prop2 is not None, "Invalid prop values"
  870. return f"{cond} ? {prop1} : {prop2}".replace("{", "").replace("}", "")
  871. # Format component conds.
  872. return wrap(f"{cond} ? {true_value} : {false_value}", "{")
  873. def get_event_handler_parts(handler: EventHandler) -> Tuple[str, str]:
  874. """Get the state and function name of an event handler.
  875. Args:
  876. handler: The event handler to get the parts of.
  877. Returns:
  878. The state and function name.
  879. """
  880. # Get the class that defines the event handler.
  881. parts = handler.fn.__qualname__.split(".")
  882. # If there's no enclosing class, just return the function name.
  883. if len(parts) == 1:
  884. return ("", parts[-1])
  885. # Get the state and the function name.
  886. state_name, name = parts[-2:]
  887. # Construct the full event handler name.
  888. try:
  889. # Try to get the state from the module.
  890. state = vars(sys.modules[handler.fn.__module__])[state_name]
  891. except Exception:
  892. # If the state isn't in the module, just return the function name.
  893. return ("", handler.fn.__qualname__)
  894. return (state.get_full_name(), name)
  895. def format_event_handler(handler: EventHandler) -> str:
  896. """Format an event handler.
  897. Args:
  898. handler: The event handler to format.
  899. Returns:
  900. The formatted function.
  901. """
  902. state, name = get_event_handler_parts(handler)
  903. if state == "":
  904. return name
  905. return f"{state}.{name}"
  906. def format_event(event_spec: EventSpec) -> str:
  907. """Format an event.
  908. Args:
  909. event_spec: The event to format.
  910. Returns:
  911. The compiled event.
  912. """
  913. args = ",".join([":".join((name, val)) for name, val in event_spec.args])
  914. return f"E(\"{format_event_handler(event_spec.handler)}\", {wrap(args, '{')})"
  915. def format_upload_event(event_spec: EventSpec) -> str:
  916. """Format an upload event.
  917. Args:
  918. event_spec: The event to format.
  919. Returns:
  920. The compiled event.
  921. """
  922. from pynecone.compiler import templates
  923. state, name = get_event_handler_parts(event_spec.handler)
  924. return f'uploadFiles({state}, {templates.RESULT}, {templates.SET_RESULT}, {state}.files, "{name}", UPLOAD)'
  925. def format_query_params(router_data: Dict[str, Any]) -> Dict[str, str]:
  926. """Convert back query params name to python-friendly case.
  927. Args:
  928. router_data: the router_data dict containing the query params
  929. Returns:
  930. The reformatted query params
  931. """
  932. params = router_data[constants.RouteVar.QUERY]
  933. return {k.replace("-", "_"): v for k, v in params.items()}
  934. # Set of unique variable names.
  935. USED_VARIABLES = set()
  936. def get_unique_variable_name() -> str:
  937. """Get a unique variable name.
  938. Returns:
  939. The unique variable name.
  940. """
  941. name = "".join([random.choice(string.ascii_lowercase) for _ in range(8)])
  942. if name not in USED_VARIABLES:
  943. USED_VARIABLES.add(name)
  944. return name
  945. return get_unique_variable_name()
  946. def get_default_app_name() -> str:
  947. """Get the default app name.
  948. The default app name is the name of the current directory.
  949. Returns:
  950. The default app name.
  951. """
  952. return os.getcwd().split(os.path.sep)[-1].replace("-", "_")
  953. def is_dataframe(value: Type) -> bool:
  954. """Check if the given value is a dataframe.
  955. Args:
  956. value: The value to check.
  957. Returns:
  958. Whether the value is a dataframe.
  959. """
  960. return value.__name__ == "DataFrame"
  961. def is_figure(value: Type) -> bool:
  962. """Check if the given value is a figure.
  963. Args:
  964. value: The value to check.
  965. Returns:
  966. Whether the value is a figure.
  967. """
  968. return value.__name__ == "Figure"
  969. def is_valid_var_type(var: Type) -> bool:
  970. """Check if the given value is a valid prop type.
  971. Args:
  972. var: The value to check.
  973. Returns:
  974. Whether the value is a valid prop type.
  975. """
  976. return _issubclass(var, StateVar) or is_dataframe(var) or is_figure(var)
  977. def format_dataframe_values(value: Type) -> List[Any]:
  978. """Format dataframe values.
  979. Args:
  980. value: The value to format.
  981. Returns:
  982. Format data
  983. """
  984. if not is_dataframe(type(value)):
  985. return value
  986. format_data = []
  987. for data in list(value.values.tolist()):
  988. element = []
  989. for d in data:
  990. element.append(str(d) if isinstance(d, (list, tuple)) else d)
  991. format_data.append(element)
  992. return format_data
  993. def format_state(value: Any) -> Dict:
  994. """Recursively format values in the given state.
  995. Args:
  996. value: The state to format.
  997. Returns:
  998. The formatted state.
  999. Raises:
  1000. TypeError: If the given value is not a valid state.
  1001. """
  1002. # Handle dicts.
  1003. if isinstance(value, dict):
  1004. return {k: format_state(v) for k, v in value.items()}
  1005. # Return state vars as is.
  1006. if isinstance(value, StateBases):
  1007. return value
  1008. # Convert plotly figures to JSON.
  1009. if isinstance(value, go.Figure):
  1010. return json.loads(to_json(value))["data"] # type: ignore
  1011. # Convert pandas dataframes to JSON.
  1012. if is_dataframe(type(value)):
  1013. return {
  1014. "columns": value.columns.tolist(),
  1015. "data": format_dataframe_values(value),
  1016. }
  1017. raise TypeError(
  1018. "State vars must be primitive Python types, "
  1019. "or subclasses of pc.Base. "
  1020. f"Got var of type {type(value)}."
  1021. )
  1022. def get_event(state, event):
  1023. """Get the event from the given state.
  1024. Args:
  1025. state: The state.
  1026. event: The event.
  1027. Returns:
  1028. The event.
  1029. """
  1030. return f"{state.get_name()}.{event}"
  1031. def format_string(string: str) -> str:
  1032. """Format the given string as a JS string literal..
  1033. Args:
  1034. string: The string to format.
  1035. Returns:
  1036. The formatted string.
  1037. """
  1038. # Escape backticks.
  1039. string = string.replace(r"\`", "`")
  1040. string = string.replace("`", r"\`")
  1041. # Wrap the string so it looks like {`string`}.
  1042. string = wrap(string, "`")
  1043. string = wrap(string, "{")
  1044. return string
  1045. def call_event_handler(event_handler: EventHandler, arg: Var) -> EventSpec:
  1046. """Call an event handler to get the event spec.
  1047. This function will inspect the function signature of the event handler.
  1048. If it takes in an arg, the arg will be passed to the event handler.
  1049. Otherwise, the event handler will be called with no args.
  1050. Args:
  1051. event_handler: The event handler.
  1052. arg: The argument to pass to the event handler.
  1053. Returns:
  1054. The event spec from calling the event handler.
  1055. """
  1056. args = inspect.getfullargspec(event_handler.fn).args
  1057. if len(args) == 1:
  1058. return event_handler()
  1059. assert (
  1060. len(args) == 2
  1061. ), f"Event handler {event_handler.fn} must have 1 or 2 arguments."
  1062. return event_handler(arg)
  1063. def call_event_fn(fn: Callable, arg: Var) -> List[EventSpec]:
  1064. """Call a function to a list of event specs.
  1065. The function should return either a single EventSpec or a list of EventSpecs.
  1066. If the function takes in an arg, the arg will be passed to the function.
  1067. Otherwise, the function will be called with no args.
  1068. Args:
  1069. fn: The function to call.
  1070. arg: The argument to pass to the function.
  1071. Returns:
  1072. The event specs from calling the function.
  1073. Raises:
  1074. ValueError: If the lambda has an invalid signature.
  1075. """
  1076. # Import here to avoid circular imports.
  1077. from pynecone.event import EventHandler, EventSpec
  1078. # Get the args of the lambda.
  1079. args = inspect.getfullargspec(fn).args
  1080. # Call the lambda.
  1081. if len(args) == 0:
  1082. out = fn()
  1083. elif len(args) == 1:
  1084. out = fn(arg)
  1085. else:
  1086. raise ValueError(f"Lambda {fn} must have 0 or 1 arguments.")
  1087. # Convert the output to a list.
  1088. if not isinstance(out, List):
  1089. out = [out]
  1090. # Convert any event specs to event specs.
  1091. events = []
  1092. for e in out:
  1093. # Convert handlers to event specs.
  1094. if isinstance(e, EventHandler):
  1095. if len(args) == 0:
  1096. e = e()
  1097. elif len(args) == 1:
  1098. e = e(arg)
  1099. # Make sure the event spec is valid.
  1100. if not isinstance(e, EventSpec):
  1101. raise ValueError(f"Lambda {fn} returned an invalid event spec: {e}.")
  1102. # Add the event spec to the chain.
  1103. events.append(e)
  1104. # Return the events.
  1105. return events
  1106. def get_handler_args(event_spec: EventSpec, arg: Var) -> Tuple[Tuple[str, str], ...]:
  1107. """Get the handler args for the given event spec.
  1108. Args:
  1109. event_spec: The event spec.
  1110. arg: The controlled event argument.
  1111. Returns:
  1112. The handler args.
  1113. Raises:
  1114. ValueError: If the event handler has an invalid signature.
  1115. """
  1116. args = inspect.getfullargspec(event_spec.handler.fn).args
  1117. if len(args) < 2:
  1118. raise ValueError(
  1119. f"Event handler has an invalid signature, needed a method with a parameter, got {event_spec.handler}."
  1120. )
  1121. return event_spec.args if len(args) > 2 else ((args[1], arg.name),)
  1122. def fix_events(
  1123. events: Optional[List[Union[EventHandler, EventSpec]]], token: str
  1124. ) -> List[Event]:
  1125. """Fix a list of events returned by an event handler.
  1126. Args:
  1127. events: The events to fix.
  1128. token: The user token.
  1129. Returns:
  1130. The fixed events.
  1131. """
  1132. from pynecone.event import Event, EventHandler, EventSpec
  1133. # If the event handler returns nothing, return an empty list.
  1134. if events is None:
  1135. return []
  1136. # If the handler returns a single event, wrap it in a list.
  1137. if not isinstance(events, List):
  1138. events = [events]
  1139. # Fix the events created by the handler.
  1140. out = []
  1141. for e in events:
  1142. # Otherwise, create an event from the event spec.
  1143. if isinstance(e, EventHandler):
  1144. e = e()
  1145. assert isinstance(e, EventSpec), f"Unexpected event type, {type(e)}."
  1146. name = format_event_handler(e.handler)
  1147. payload = dict(e.args)
  1148. # Create an event and append it to the list.
  1149. out.append(
  1150. Event(
  1151. token=token,
  1152. name=name,
  1153. payload=payload,
  1154. )
  1155. )
  1156. return out
  1157. def merge_imports(*imports) -> ImportDict:
  1158. """Merge two import dicts together.
  1159. Args:
  1160. *imports: The list of import dicts to merge.
  1161. Returns:
  1162. The merged import dicts.
  1163. """
  1164. all_imports = defaultdict(set)
  1165. for import_dict in imports:
  1166. for lib, fields in import_dict.items():
  1167. for field in fields:
  1168. all_imports[lib].add(field)
  1169. return all_imports
  1170. def get_hydrate_event(state) -> str:
  1171. """Get the name of the hydrate event for the state.
  1172. Args:
  1173. state: The state.
  1174. Returns:
  1175. The name of the hydrate event.
  1176. """
  1177. return get_event(state, constants.HYDRATE)
  1178. def get_redis() -> Optional[Redis]:
  1179. """Get the redis client.
  1180. Returns:
  1181. The redis client.
  1182. """
  1183. config = get_config()
  1184. if config.redis_url is None:
  1185. return None
  1186. redis_url, redis_port = config.redis_url.split(":")
  1187. print("Using redis at", config.redis_url)
  1188. return Redis(host=redis_url, port=int(redis_port), db=0)
  1189. def is_backend_variable(name: str) -> bool:
  1190. """Check if this variable name correspond to a backend variable.
  1191. Args:
  1192. name: The name of the variable to check
  1193. Returns:
  1194. bool: The result of the check
  1195. """
  1196. return name.startswith("_") and not name.startswith("__")
  1197. def json_dumps(obj: Any):
  1198. """Serialize ``obj`` to a JSON formatted ``str``, ensure_ascii=False.
  1199. Args:
  1200. obj: The obj to be fromatted
  1201. Returns:
  1202. str: The result of the json dumps
  1203. """
  1204. return json.dumps(obj, ensure_ascii=False)
  1205. # Store this here for performance.
  1206. StateBases = get_base_class(StateVar)