diff --git a/src/commands/analyze/command.ts b/src/commands/analyze/command.ts index da9217c0..4f564fef 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 6a997b27..8a4eeece 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 429bcfe7..3956f8a3 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 83825876..69f39196 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 0e18751e..e0ae6f68 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, + startFileName: string, +) { + const cloudProvider = await getCloudProvider(config.name); + const cloudAdapter = getCloudAdapter(cloudProvider); + + const serverFunction = { + path: ".", + name: "streamlit", + entry: startFileName, + 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 6372876b..fcb73e90 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/commands/local.ts b/src/commands/local.ts index 716cc43f..bd2e0727 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, diff --git a/src/models/projectOptions.ts b/src/models/projectOptions.ts index c239307e..77e07e98 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 9bdb3735..8ac22524 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);