Skip to content

Commit

Permalink
Merge pull request #1691 from Genez-io/support-streamlit
Browse files Browse the repository at this point in the history
feat: add streamlit support
  • Loading branch information
cristim67 authored Feb 4, 2025
2 parents d0bb6b1 + d693eda commit 15e5fae
Show file tree
Hide file tree
Showing 11 changed files with 306 additions and 8 deletions.
33 changes: 33 additions & 0 deletions src/commands/analyze/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
isNestjsComponent,
isRemixComponent,
isEmberComponent,
isStreamlitComponent,
} from "./frameworks.js";
import { generateDatabaseName, readOrAskConfig } from "../deploy/utils.js";
import {
Expand Down Expand Up @@ -55,6 +56,7 @@ import {
FLASK_PATTERN,
PYTHON_LAMBDA_PATTERN,
SERVERLESS_HTTP_PATTERN,
STREAMLIT_PATTERN,
} from "./constants.js";
import { analyzeEnvironmentVariableExampleFile, ProjectEnvironment } from "./agent.js";

Expand Down Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions src/commands/analyze/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 8 additions & 0 deletions src/commands/analyze/frameworks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -460,3 +460,11 @@ export function isFastAPIComponent(contents: Record<string, string>): boolean {
export function isPythonLambdaFunction(contents: Record<string, string>): 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<string, string>): boolean {
return (
contents["requirements.txt"] !== undefined &&
contents["requirements.txt"].includes("streamlit")
);
}
2 changes: 2 additions & 0 deletions src/commands/analyze/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
20 changes: 20 additions & 0 deletions src/commands/deploy/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -24,6 +25,8 @@ export type SSRFrameworkComponent = {
[key: string]: string;
};
subdomain?: string;
runtime?: string;
entryFile?: string;
};

export async function deployCommand(options: GenezioDeployOptions) {
Expand Down Expand Up @@ -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;
}
}
}
Expand All @@ -78,6 +85,7 @@ export enum DeployType {
Nest = "nest",
Zip = "zip",
Remix = "remix",
Streamlit = "streamlit",
}

/**
Expand Down Expand Up @@ -128,6 +136,9 @@ async function decideDeployType(options: GenezioDeployOptions): Promise<DeployTy
if (config.remix) {
deployableComponents.push(DeployType.Remix);
}
if (config.streamlit) {
deployableComponents.push(DeployType.Streamlit);
}

// This ensures backwards compatibility for next/nuxt projects
// that have a simple (only name and region) genezio.yaml
Expand Down Expand Up @@ -198,6 +209,15 @@ async function decideDeployType(options: GenezioDeployOptions): Promise<DeployTy
}
}

// Check if requirements.txt exists
if (fs.existsSync(path.join(cwd, "requirements.txt"))) {
// Check if streamlit is in the requirements.txt
const requirementsTxt = fs.readFileSync(path.join(cwd, "requirements.txt"), "utf-8");
if (requirementsTxt.includes("streamlit")) {
return [DeployType.Streamlit];
}
}

// Check if a Dockerfile exists in non-genezio project
if (
!fs.existsSync(path.join(cwd, "genezio.yaml")) &&
Expand Down
5 changes: 1 addition & 4 deletions src/commands/deploy/genezio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/`,
Expand Down
213 changes: 213 additions & 0 deletions src/commands/deploy/streamlit/deploy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import { YamlProjectConfiguration } from "../../../projectConfiguration/yaml/v2.js";
import { GenezioDeployOptions } from "../../../models/commandOptions.js";
import {
DEFAULT_PYTHON_RUNTIME,
PythonRuntime,
SSRFrameworkComponentType,
} from "../../../models/projectOptions.js";
import {
PackageManagerType,
PYTHON_DEFAULT_PACKAGE_MANAGER,
} from "../../../packageManagers/packageManager.js";
import { PYTHON_DEFAULT_ENTRY_FILE } from "../../analyze/command.js";
import { STREAMLIT_PATTERN } from "../../analyze/constants.js";
import { findEntryFile } from "../../analyze/frameworks.js";
import { addSSRComponentToConfig } from "../../analyze/utils.js";
import {
prepareServicesPostBackendDeployment,
prepareServicesPreBackendDeployment,
readOrAskConfig,
uploadEnvVarsFromFile,
uploadUserCode,
} from "../utils.js";
import path from "path";
import fs from "fs";
import { debugLogger, log } from "../../../utils/logging.js";
import colors from "colors";
import { Language } from "../../../projectConfiguration/yaml/models.js";
import { DASHBOARD_URL } from "../../../constants.js";
import { getCloudProvider } from "../../../requests/getCloudProvider.js";
import { functionToCloudInput, getCloudAdapter } from "../genezio.js";
import { FunctionType } from "../../../projectConfiguration/yaml/models.js";
import { ProjectConfiguration } from "../../../models/projectConfiguration.js";
import { createTemporaryFolder } from "../../../utils/file.js";

export async function streamlitDeploy(options: GenezioDeployOptions) {
const genezioConfig = await readOrAskConfig(options.config);
const packageManagerType =
genezioConfig.streamlit?.packageManager || PYTHON_DEFAULT_PACKAGE_MANAGER;

const cwd = process.cwd();
const componentPath = genezioConfig.streamlit?.path
? path.resolve(cwd, genezioConfig.streamlit.path)
: cwd;

// Prepare services before deploying (database, authentication, etc)
await prepareServicesPreBackendDeployment(
genezioConfig,
genezioConfig.name,
options.stage,
options.env,
);

const entryFile = await findEntryFile(
componentPath,
getFilesContents(componentPath),
STREAMLIT_PATTERN,
PYTHON_DEFAULT_ENTRY_FILE,
);

// Add streamlit component to config
await addSSRComponentToConfig(
options.config,
{
path: componentPath,
packageManager: packageManagerType,
runtime: DEFAULT_PYTHON_RUNTIME,
entryFile: entryFile,
},
SSRFrameworkComponentType.streamlit,
);

// Copy to tmp folder
const tempCwd = await createTemporaryFolder();
debugLogger.debug(`Copying project files to ${tempCwd}`);
await fs.promises.cp(componentPath, tempCwd, {
recursive: true,
force: true,
dereference: true,
});

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, startFileName);

await uploadEnvVarsFromFile(
options.env,
result.projectId,
result.projectEnvId,
tempCwd,
options.stage || "prod",
updatedGenezioConfig,
SSRFrameworkComponentType.streamlit,
);

await uploadUserCode(
updatedGenezioConfig.name,
updatedGenezioConfig.region,
options.stage,
componentPath,
);

const functionUrl = result.functions.find((f) => 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<string, string> {
const contents: Record<string, string> = {};
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;
}
1 change: 1 addition & 0 deletions src/commands/deploy/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Loading

0 comments on commit 15e5fae

Please sign in to comment.