From 7e853fdab36dfcb46f9979838ae67111eadd0da3 Mon Sep 17 00:00:00 2001 From: Cristi Miloiu Date: Fri, 31 Jan 2025 14:26:37 +0200 Subject: [PATCH 1/5] feat: add streamlit support --- src/commands/analyze/command.ts | 33 ++++ src/commands/analyze/constants.ts | 2 + src/commands/analyze/frameworks.ts | 8 + src/commands/analyze/utils.ts | 2 + src/commands/deploy/command.ts | 20 +++ src/commands/deploy/streamlit/deploy.ts | 211 ++++++++++++++++++++++++ src/commands/deploy/utils.ts | 1 + src/models/projectOptions.ts | 11 +- src/projectConfiguration/yaml/v2.ts | 3 + 9 files changed, 289 insertions(+), 2 deletions(-) create mode 100644 src/commands/deploy/streamlit/deploy.ts diff --git a/src/commands/analyze/command.ts b/src/commands/analyze/command.ts index da9217c0e..4f564fef8 100644 --- a/src/commands/analyze/command.ts +++ b/src/commands/analyze/command.ts @@ -25,6 +25,7 @@ import { isNestjsComponent, isRemixComponent, isEmberComponent, + isStreamlitComponent, } from "./frameworks.js"; import { generateDatabaseName, readOrAskConfig } from "../deploy/utils.js"; import { @@ -55,6 +56,7 @@ import { FLASK_PATTERN, PYTHON_LAMBDA_PATTERN, SERVERLESS_HTTP_PATTERN, + STREAMLIT_PATTERN, } from "./constants.js"; import { analyzeEnvironmentVariableExampleFile, ProjectEnvironment } from "./agent.js"; @@ -282,6 +284,37 @@ export async function analyzeCommand(options: GenezioAnalyzeOptions) { continue; } + if (await isStreamlitComponent(contents)) { + const packageManagerType = + genezioConfig.streamlit?.packageManager || PYTHON_DEFAULT_PACKAGE_MANAGER; + const entryFile = await findEntryFile( + componentPath, + contents, + STREAMLIT_PATTERN, + PYTHON_DEFAULT_ENTRY_FILE, + ); + debugLogger.debug("Streamlit entry file found:", entryFile); + await addSSRComponentToConfig( + options.config, + { + path: componentPath, + packageManager: packageManagerType, + environment: mapEnvironmentVariableToConfig( + resultEnvironmentAnalysis.get(componentPath)?.environmentVariables, + ), + runtime: DEFAULT_PYTHON_RUNTIME, + entryFile: entryFile, + }, + SSRFrameworkComponentType.streamlit, + ); + frameworksDetected.ssr = frameworksDetected.ssr || []; + frameworksDetected.ssr.push({ + component: "streamlit", + environment: resultEnvironmentAnalysis.get(componentPath)?.environmentVariables, + }); + continue; + } + if (await isNestjsComponent(contents)) { const packageManagerType = genezioConfig.nestjs?.packageManager || NODE_DEFAULT_PACKAGE_MANAGER; diff --git a/src/commands/analyze/constants.ts b/src/commands/analyze/constants.ts index 6a997b27e..8a4eeece7 100644 --- a/src/commands/analyze/constants.ts +++ b/src/commands/analyze/constants.ts @@ -37,6 +37,8 @@ export const FASTAPI_PATTERN = [ /from\s+fastapi\s+import\s+FastAPI|import\s+fastapi/, ]; +export const STREAMLIT_PATTERN = [/import\s+streamlit(?:\s+as\s+st)?|from\s+streamlit\s+import/]; + // Agent Prompts export const ENVIRONMENT_ANALYZE_PROMPT = `Your task is to analyze the following .env.example file and provide values as best as you can. diff --git a/src/commands/analyze/frameworks.ts b/src/commands/analyze/frameworks.ts index 429bcfe77..3956f8a33 100644 --- a/src/commands/analyze/frameworks.ts +++ b/src/commands/analyze/frameworks.ts @@ -460,3 +460,11 @@ export function isFastAPIComponent(contents: Record): boolean { export function isPythonLambdaFunction(contents: Record): boolean { return contents["requirements.txt"] !== undefined || contents["pyproject.toml"] !== undefined; } + +// Checks if the project is a Streamlit component (presence of 'requirements.txt' and 'streamlit' in 'requirements.txt') +export function isStreamlitComponent(contents: Record): boolean { + return ( + contents["requirements.txt"] !== undefined && + contents["requirements.txt"].includes("streamlit") + ); +} diff --git a/src/commands/analyze/utils.ts b/src/commands/analyze/utils.ts index 83825876e..69f391963 100644 --- a/src/commands/analyze/utils.ts +++ b/src/commands/analyze/utils.ts @@ -112,6 +112,8 @@ export async function addSSRComponentToConfig( ...config[componentType]?.environment, }, scripts: config[componentType]?.scripts || component.scripts, + entryFile: config[componentType]?.entryFile || component.entryFile, + runtime: config[componentType]?.runtime || component.runtime, }; await configIOController.write(config); diff --git a/src/commands/deploy/command.ts b/src/commands/deploy/command.ts index 0e18751e1..a228ae399 100644 --- a/src/commands/deploy/command.ts +++ b/src/commands/deploy/command.ts @@ -12,6 +12,7 @@ import { YamlConfigurationIOController } from "../../projectConfiguration/yaml/v import { nestJsDeploy } from "./nestjs/deploy.js"; import { zipDeploy } from "./zip/deploy.js"; import { remixDeploy } from "./remix/deploy.js"; +import { streamlitDeploy } from "./streamlit/deploy.js"; export type SSRFrameworkComponent = { path: string; packageManager: PackageManagerType; @@ -24,6 +25,8 @@ export type SSRFrameworkComponent = { [key: string]: string; }; subdomain?: string; + runtime?: string; + entryFile?: string; }; export async function deployCommand(options: GenezioDeployOptions) { @@ -65,6 +68,10 @@ export async function deployCommand(options: GenezioDeployOptions) { debugLogger.debug("Deploying Remix app"); await remixDeploy(options); break; + case DeployType.Streamlit: + debugLogger.debug("Deploying Streamlit app"); + await streamlitDeploy(options); + break; } } } @@ -78,6 +85,7 @@ export enum DeployType { Nest = "nest", Zip = "zip", Remix = "remix", + Streamlit = "streamlit", } /** @@ -128,6 +136,9 @@ async function decideDeployType(options: GenezioDeployOptions): Promise f.name === "function-streamlit")?.cloudUrl; + + await prepareServicesPostBackendDeployment( + updatedGenezioConfig, + updatedGenezioConfig.name, + options.stage, + ); + + if (functionUrl) { + log.info( + `The app is being deployed at ${colors.cyan(functionUrl)}. It might take a few moments to be available worldwide.`, + ); + + log.info( + `\nApp Dashboard URL: ${colors.cyan(`${DASHBOARD_URL}/project/${result.projectId}/${result.projectEnvId}`)}\n` + + `${colors.dim("Here you can monitor logs, set up a custom domain, and more.")}\n`, + ); + } else { + log.warn("No deployment URL was returned."); + } +} + +async function deployFunction( + config: YamlProjectConfiguration, + options: GenezioDeployOptions, + cwd: string, +) { + const cloudProvider = await getCloudProvider(config.name); + const cloudAdapter = getCloudAdapter(cloudProvider); + + const serverFunction = { + path: ".", + name: "streamlit", + entry: "start.py", + type: FunctionType.httpServer, + }; + + const runtime = (config.streamlit?.runtime as PythonRuntime) || DEFAULT_PYTHON_RUNTIME; + + const deployConfig: YamlProjectConfiguration = { + ...config, + backend: { + path: ".", + language: { + name: Language.python, + runtime: runtime, + architecture: "x86_64", + packageManager: PackageManagerType.pip, + }, + functions: [serverFunction], + }, + }; + + const projectConfiguration = new ProjectConfiguration( + deployConfig, + await getCloudProvider(deployConfig.name), + { + generatorResponses: [], + classesInfo: [], + }, + ); + + const cloudInputs = await Promise.all( + projectConfiguration.functions.map((f) => functionToCloudInput(f, cwd, undefined, runtime)), + ); + + const result = await cloudAdapter.deploy( + cloudInputs, + projectConfiguration, + { stage: options.stage }, + ["streamlit"], + ); + + return result; +} + +function getStartFileContent(entryFile: string) { + const startFileContent = ` +import streamlit.web.bootstrap as bootstrap + +flags = { + 'server_port': 8080, + 'global_developmentMode': False +} + +bootstrap.load_config_options(flag_options=flags) +bootstrap.run('${entryFile}', False, [], flags) +`; + + return startFileContent; +} + +function getFilesContents(dir: string): Record { + const contents: Record = {}; + const files = fs.readdirSync(dir); + + for (const file of files) { + if (file.endsWith(".py")) { + const filePath = path.join(dir, file); + contents[file] = fs.readFileSync(filePath, "utf8"); + } + } + return contents; +} diff --git a/src/commands/deploy/utils.ts b/src/commands/deploy/utils.ts index 6372876b5..fcb73e900 100644 --- a/src/commands/deploy/utils.ts +++ b/src/commands/deploy/utils.ts @@ -985,6 +985,7 @@ export async function uploadEnvVarsFromFile( [SSRFrameworkComponentType.nitro]: configuration.nuxt?.environment, [SSRFrameworkComponentType.nestjs]: configuration.nuxt?.environment, [SSRFrameworkComponentType.remix]: configuration.remix?.environment, + [SSRFrameworkComponentType.streamlit]: configuration.streamlit?.environment, backend: configuration.backend?.environment, }[componentType] ?? configuration.backend?.environment; diff --git a/src/models/projectOptions.ts b/src/models/projectOptions.ts index c239307ee..77e07e981 100644 --- a/src/models/projectOptions.ts +++ b/src/models/projectOptions.ts @@ -1,5 +1,10 @@ export type NodeRuntime = "nodejs20.x"; -export type PythonRuntime = "python3.11.x"; +export type PythonRuntime = + | "python3.9.x" + | "python3.10.x" + | "python3.11.x" + | "python3.12.x" + | "python3.13.x"; export type Architecture = "arm64" | "x86_64"; export const DEFAULT_NODE_RUNTIME: NodeRuntime = "nodejs20.x"; export const DEFAULT_ARCHITECTURE: Architecture = "arm64"; @@ -30,6 +35,7 @@ export enum SSRFrameworkComponentType { nuxt = "nuxt", nestjs = "nestjs", remix = "remix", + streamlit = "streamlit", } // These are the human friendly names for the SSR frameworks @@ -40,6 +46,7 @@ export const SSRFrameworkName: Record = { [SSRFrameworkComponentType.nuxt]: "Nuxt.js", [SSRFrameworkComponentType.nestjs]: "NestJS", [SSRFrameworkComponentType.remix]: "Remix", + [SSRFrameworkComponentType.streamlit]: "Streamlit", }; export enum ContainerComponentType { @@ -55,5 +62,5 @@ export const supportedPythonRuntimes = [ "python3.13.x", ] as const; export const supportedArchitectures = ["arm64", "x86_64"] as const; -export const supportedSSRFrameworks = ["nextjs", "nitro", "nuxt"] as const; +export const supportedSSRFrameworks = ["nextjs", "nitro", "nuxt", "streamlit"] as const; export const supportedPythonDepsInstallVersion = ["3.9", "3.10", "3.11", "3.12", "3.13"] as const; diff --git a/src/projectConfiguration/yaml/v2.ts b/src/projectConfiguration/yaml/v2.ts index 9bdb3735a..8ac22524b 100644 --- a/src/projectConfiguration/yaml/v2.ts +++ b/src/projectConfiguration/yaml/v2.ts @@ -329,6 +329,8 @@ function parseGenezioConfig(config: unknown) { .optional(), environment: environmentSchema.optional(), subdomain: zod.string().optional(), + runtime: zod.string().optional(), + entryFile: zod.string().optional(), }); // Define container schema @@ -375,6 +377,7 @@ function parseGenezioConfig(config: unknown) { nitro: ssrFrameworkSchema.optional(), container: containerSchema.optional(), remix: ssrFrameworkSchema.optional(), + streamlit: ssrFrameworkSchema.optional(), }); const parsedConfig = v2Schema.parse(config); From 20641c2ec7ccad0cf68a00554f7add9bb80930d7 Mon Sep 17 00:00:00 2001 From: Cristi Miloiu Date: Fri, 31 Jan 2025 14:39:39 +0200 Subject: [PATCH 2/5] fix streamlit local --- src/commands/local.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/commands/local.ts b/src/commands/local.ts index 716cc43f8..bd2e07277 100644 --- a/src/commands/local.ts +++ b/src/commands/local.ts @@ -103,6 +103,7 @@ import { getFunctionHandlerProvider } from "../utils/getFunctionHandlerProvider. import { getFunctionEntryFilename } from "../utils/getFunctionEntryFilename.js"; import { SSRFrameworkComponent } from "./deploy/command.js"; import fs from "fs"; +import { detectPythonCommand } from "../utils/detectPythonCommand.js"; type UnitProcess = { process: ChildProcess; @@ -344,7 +345,8 @@ export async function startLocalEnvironment(options: GenezioLocalOptions) { !yamlProjectConfiguration.nuxt && !yamlProjectConfiguration.nestjs && !yamlProjectConfiguration.nitro && - !yamlProjectConfiguration.remix + !yamlProjectConfiguration.remix && + !yamlProjectConfiguration.streamlit ) { throw new UserError( "No backend or frontend components found in the genezio.yaml file. You need at least one component to start the local environment.", @@ -1897,10 +1899,20 @@ async function startSsrFramework( ? ["vite:dev", "--port", ssrPort] : ["dev", "--port", ssrPort]; break; + case SSRFrameworkComponentType.streamlit: + command = "-m"; + args = ["streamlit", "run", ssrConfig.entryFile!, "--server.port", ssrPort]; + break; default: throw new Error(`Unknown SSR framework: ${framework}`); } - const childProcess = spawn("npx", [command, ...args], { + + const spawnCommand = + framework === SSRFrameworkComponentType.streamlit + ? (await detectPythonCommand()) || "python3" + : "npx"; + + const childProcess = spawn(spawnCommand, [command, ...args], { stdio: "pipe", env: { ...process.env, From c19d77be5b7f2d226a611a0b578a964a2d81e42e Mon Sep 17 00:00:00 2001 From: Cristi Miloiu Date: Fri, 31 Jan 2025 15:17:33 +0200 Subject: [PATCH 3/5] fix wrong message --- src/commands/deploy/genezio.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/commands/deploy/genezio.ts b/src/commands/deploy/genezio.ts index 8ff6ca0c8..59b8068b5 100644 --- a/src/commands/deploy/genezio.ts +++ b/src/commands/deploy/genezio.ts @@ -662,10 +662,7 @@ export async function functionToCloudInput( ``, `This might lead to unexpected behavior. To ensure consistency, update your genezio.yaml configuration to match your local version:`, ``, - `language:`, - ` name: python`, - ` packageManager: pip`, - ` runtime: python${localPythonVersion}.x`, + `runtime: python${localPythonVersion}.x`, ``, `This will help prevent potential compatibility issues!`, `For complete yaml configuration, visit: https://genezio.com/docs/project-structure/genezio-configuration-file/`, From affb58eff9e98f9908bb1e2c2823bd4a9bbda0fe Mon Sep 17 00:00:00 2001 From: Cristi Miloiu Date: Fri, 31 Jan 2025 16:26:11 +0200 Subject: [PATCH 4/5] fix order --- src/commands/deploy/command.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/commands/deploy/command.ts b/src/commands/deploy/command.ts index a228ae399..e0ae6f68b 100644 --- a/src/commands/deploy/command.ts +++ b/src/commands/deploy/command.ts @@ -209,14 +209,6 @@ async function decideDeployType(options: GenezioDeployOptions): Promise Date: Sat, 1 Feb 2025 18:22:10 +0200 Subject: [PATCH 5/5] nit: generate unique start file name for streamlit deployments --- src/commands/deploy/streamlit/deploy.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/commands/deploy/streamlit/deploy.ts b/src/commands/deploy/streamlit/deploy.ts index 3298dfe15..19e7ef6d7 100644 --- a/src/commands/deploy/streamlit/deploy.ts +++ b/src/commands/deploy/streamlit/deploy.ts @@ -78,15 +78,16 @@ export async function streamlitDeploy(options: GenezioDeployOptions) { dereference: true, }); - // create start file if not exists - const startFile = path.join(tempCwd, "start.py"); + const randomId = Math.random().toString(36).substring(2, 6); + const startFileName = `start-${randomId}.py`; + const startFile = path.join(tempCwd, startFileName); if (!fs.existsSync(startFile)) { fs.writeFileSync(startFile, getStartFileContent(entryFile)); } const updatedGenezioConfig = await readOrAskConfig(options.config); // Deploy the component - const result = await deployFunction(updatedGenezioConfig, options, tempCwd); + const result = await deployFunction(updatedGenezioConfig, options, tempCwd, startFileName); await uploadEnvVarsFromFile( options.env, @@ -131,6 +132,7 @@ async function deployFunction( config: YamlProjectConfiguration, options: GenezioDeployOptions, cwd: string, + startFileName: string, ) { const cloudProvider = await getCloudProvider(config.name); const cloudAdapter = getCloudAdapter(cloudProvider); @@ -138,7 +140,7 @@ async function deployFunction( const serverFunction = { path: ".", name: "streamlit", - entry: "start.py", + entry: startFileName, type: FunctionType.httpServer, };