Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix the npm issue when running a fullstack Python app #471

Merged
merged 4 commits into from
Dec 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/little-kangaroos-exercise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"create-llama": patch
---

Fix the npm issue on the full-stack Python template
93 changes: 65 additions & 28 deletions templates/types/streaming/fastapi/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,33 @@


FRONTEND_DIR = Path(os.getenv("FRONTEND_DIR", ".frontend"))
DEFAULT_FRONTEND_PORT = 3000
APP_HOST = os.getenv("APP_HOST", "localhost")
APP_PORT = int(
os.getenv("APP_PORT", 8000)
) # Allocated to backend but also for access to the app, please change it in .env
DEFAULT_FRONTEND_PORT = (
3000 # Not for access directly, but for proxying to the backend in development
)
STATIC_DIR = Path(os.getenv("STATIC_DIR", "static"))


class NodePackageManager(str):
def __new__(cls, value: str) -> "NodePackageManager":
return super().__new__(cls, value)

@property
def name(self) -> str:
return Path(self).stem
leehuwuj marked this conversation as resolved.
Show resolved Hide resolved

@property
def is_pnpm(self) -> bool:
return self.name == "pnpm"

@property
def is_npm(self) -> bool:
return self.name == "npm"


def build():
"""
Build the frontend and copy the static files to the backend.
Expand Down Expand Up @@ -146,22 +169,20 @@ async def _run_frontend(
package_manager,
"run",
"dev",
"--" if package_manager.is_npm else "",
"-p",
str(port),
cwd=FRONTEND_DIR,
)
rich.print(
f"\n[bold]Waiting for frontend to start, port: {port}, process id: {frontend_process.pid}[/bold]"
)
rich.print("\n[bold]Waiting for frontend to start...")
# Block until the frontend is accessible
for _ in range(timeout):
await asyncio.sleep(1)
# Check if the frontend is accessible (port is open) or frontend_process is running
if frontend_process.returncode is not None:
raise RuntimeError("Could not start frontend dev server")
if not _is_bindable_port(port):
if _is_server_running(port):
rich.print(
f"\n[bold green]Frontend dev server is running on port {port}[/bold green]"
"\n[bold]Frontend dev server is running. Please wait a while for the app to be ready...[/bold]"
)
return frontend_process, port
raise TimeoutError(f"Frontend dev server failed to start within {timeout} seconds")
Expand All @@ -173,33 +194,50 @@ async def _run_backend(
"""
Start the backend development server.

Args:
frontend_port: The port number the frontend is running on
Returns:
Process: The backend process
"""
# Merge environment variables
envs = {**os.environ, **(envs or {})}
rich.print("\n[bold]Starting backend FastAPI server...[/bold]")
# Check if the port is free
if not _is_port_available(APP_PORT):
raise SystemError(
f"Port {APP_PORT} is not available! Please change the port in .env file or kill the process running on this port."
)
rich.print(f"\n[bold]Starting app on port {APP_PORT}...[/bold]")
poetry_executable = _get_poetry_executable()
return await asyncio.create_subprocess_exec(
process = await asyncio.create_subprocess_exec(
poetry_executable,
"run",
"python",
"main.py",
env=envs,
)
# Wait for port is started
timeout = 30
for _ in range(timeout):
await asyncio.sleep(1)
if process.returncode is not None:
raise RuntimeError("Could not start backend dev server")
if _is_server_running(APP_PORT):
rich.print(
f"\n[bold green]App is running. You now can access it at http://{APP_HOST}:{APP_PORT}[/bold green]"
)
return process
# Timeout, kill the process
process.terminate()
raise TimeoutError(f"Backend dev server failed to start within {timeout} seconds")


def _install_frontend_dependencies():
package_manager = _get_node_package_manager()
rich.print(
f"\n[bold]Installing frontend dependencies using {Path(package_manager).name}. It might take a while...[/bold]"
f"\n[bold]Installing frontend dependencies using {package_manager.name}. It might take a while...[/bold]"
)
run([package_manager, "install"], cwd=".frontend", check=True)


def _get_node_package_manager() -> str:
def _get_node_package_manager() -> NodePackageManager:
"""
Check for available package managers and return the preferred one.
Returns 'pnpm' if installed, falls back to 'npm'.
Expand All @@ -215,12 +253,12 @@ def _get_node_package_manager() -> str:
for cmd in pnpm_cmds:
cmd_path = which(cmd)
if cmd_path is not None:
return cmd_path
return NodePackageManager(cmd_path)

for cmd in npm_cmds:
cmd_path = which(cmd)
if cmd_path is not None:
return cmd_path
return NodePackageManager(cmd_path)

raise SystemError(
"Neither pnpm nor npm is installed. Please install Node.js and a package manager first."
Expand All @@ -244,28 +282,27 @@ def _get_poetry_executable() -> str:
raise SystemError("Poetry is not installed. Please install Poetry first.")


def _is_bindable_port(port: int) -> bool:
"""Check if a port is available by attempting to connect to it."""
def _is_port_available(port: int) -> bool:
"""Check if a port is available for binding."""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
try:
# Try to connect to the port
s.connect(("localhost", port))
# If we can connect, port is in use
return False
return False # Port is in use, so not available
except ConnectionRefusedError:
# Connection refused means port is available
return True
return True # Port is available
except socket.error:
# Other socket errors also likely mean port is available
return True
return True # Other socket errors likely mean port is available


def _is_server_running(port: int) -> bool:
"""Check if a server is running on the specified port."""
return not _is_port_available(port)


def _find_free_port(start_port: int) -> int:
"""
Find a free port starting from the given port number.
"""
"""Find a free port starting from the given port number."""
for port in range(start_port, 65535):
if _is_bindable_port(port):
if _is_port_available(port):
return port
raise SystemError("No free port found")

Expand Down
Loading