From d886a8f5fc51f2e60369f6377a2c9492a159c66c Mon Sep 17 00:00:00 2001 From: Jared Lunde Date: Wed, 21 Aug 2024 12:47:00 -0700 Subject: [PATCH] feat: add special case for python fastapi (#24) --- README.md | 9 +- runtime/bun.go | 16 ++- runtime/bun_test.go | 17 ++- runtime/deno.go | 13 ++- runtime/deno_test.go | 10 +- runtime/elixir.go | 12 +- runtime/golang.go | 11 +- runtime/java.go | 14 ++- runtime/main.go | 2 +- runtime/nextjs.go | 22 ++-- runtime/nextjs_test.go | 17 ++- runtime/node.go | 16 ++- runtime/node_test.go | 17 ++- runtime/php.go | 16 ++- runtime/php_test.go | 17 ++- runtime/python.go | 146 ++++++++++++++++++++----- runtime/python_test.go | 17 ++- runtime/ruby.go | 16 ++- runtime/ruby_test.go | 17 ++- runtime/rust.go | 12 +- runtime/static.go | 12 +- testdata/python-fastapi/.tool-versions | 1 + testdata/python-fastapi/Pipfile | 10 ++ testdata/python-fastapi/Pipfile.lock | 59 ++++++++++ testdata/python-fastapi/main.py | 0 25 files changed, 407 insertions(+), 92 deletions(-) create mode 100644 testdata/python-fastapi/.tool-versions create mode 100644 testdata/python-fastapi/Pipfile create mode 100644 testdata/python-fastapi/Pipfile.lock create mode 100644 testdata/python-fastapi/main.py diff --git a/README.md b/README.md index 31f4add..ca6fbc1 100644 --- a/README.md +++ b/README.md @@ -420,6 +420,7 @@ In order of precedence: #### Detected Files - `requirements.txt` + - `uv.lock` - `poetry.lock` - `Pipefile.lock` - `pyproject.toml` @@ -447,14 +448,16 @@ In order of precedence: - `START_CMD` - The command to start the project (default: detected from source code) #### Install Command -- If Poetry: `poetry install --no-dev --no-interactive --no-ansi` -- If Pipenv: `PIPENV_VENV_IN_PROJECT=1 pipenv install --deploy` -- If PDM: `pdm install --prod` +- If Poetry: `pip install poetry && poetry install --no-dev --no-ansi --no-root` +- If Pipenv: `pipenv install --dev --system --deploy` +- If uv: `pip install uv && uv sync --python-preference=only-system --no-cache --no-dev` +- If PDM: `pip install pdm && pdm install --prod` - If `pyproject.toml` exists: `pip install --upgrade build setuptools && pip install .` - If `requirements.txt` exists: `pip install -r requirements.txt` #### Start Command - If Django is detected: `python manage.py runserver 0.0.0.0:${PORT}` +- If FastAPI is detected: `fastapi run [main.py, app.py, application.py, app/main.py, app/application.py, app/__init__.py] --port ${PORT}` - If `pyproject.toml` exists: `python -m ${projectName}` - Otherwise: `python [main.py, app.py, application.py, app/main.py, app/application.py, app/__init__.py]` diff --git a/runtime/bun.go b/runtime/bun.go index 388003b..2f18b4a 100644 --- a/runtime/bun.go +++ b/runtime/bun.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "log/slog" + "maps" "os" "path/filepath" "strings" @@ -37,7 +38,7 @@ func (d *Bun) Match(path string) bool { return false } -func (d *Bun) GenerateDockerfile(path string) ([]byte, error) { +func (d *Bun) GenerateDockerfile(path string, data ...map[string]string) ([]byte, error) { tmpl, err := template.New("Dockerfile").Parse(bunTemplate) if err != nil { return nil, fmt.Errorf("Failed to parse template") @@ -125,11 +126,15 @@ func (d *Bun) GenerateDockerfile(path string) ([]byte, error) { } var buf bytes.Buffer - if err := tmpl.Option("missingkey=zero").Execute(&buf, map[string]string{ + templateData := map[string]string{ "Version": *version, "BuildCMD": buildCMD, "StartCMD": startCMD, - }); err != nil { + } + if len(data) > 0 { + maps.Copy(templateData, data[0]) + } + if err := tmpl.Option("missingkey=zero").Execute(&buf, templateData); err != nil { return nil, fmt.Errorf("Failed to execute template") } @@ -145,7 +150,7 @@ FROM base AS deps WORKDIR /app COPY package.json bun.lockb ./ ARG INSTALL_CMD="bun install" -RUN if [ ! -z "${INSTALL_CMD}" ]; then sh -c "$INSTALL_CMD"; fi +RUN {{.InstallMounts}}if [ ! -z "${INSTALL_CMD}" ]; then sh -c "$INSTALL_CMD"; fi FROM base AS builder WORKDIR /app @@ -153,7 +158,7 @@ COPY --from=deps /app/node_modules* ./node_modules COPY . . ENV NODE_ENV=production ARG BUILD_CMD={{.BuildCMD}} -RUN if [ ! -z "${BUILD_CMD}" ]; then sh -c "$BUILD_CMD"; fi +RUN {{.BuildMounts}}if [ ! -z "${BUILD_CMD}" ]; then sh -c "$BUILD_CMD"; fi FROM ${BUILDER}:${VERSION}-slim AS runtime WORKDIR /app @@ -168,6 +173,7 @@ COPY --chown=nonroot:nonroot --from=builder /app . USER nonroot:nonroot ENV PORT=8080 +EXPOSE ${PORT} ENV NODE_ENV=production ARG START_CMD={{.StartCMD}} ENV START_CMD=${START_CMD} diff --git a/runtime/bun_test.go b/runtime/bun_test.go index 76cbd5e..144dab0 100644 --- a/runtime/bun_test.go +++ b/runtime/bun_test.go @@ -45,6 +45,7 @@ func TestBunGenerateDockerfile(t *testing.T) { tests := []struct { name string path string + data map[string]string expected []any }{ { @@ -57,6 +58,20 @@ func TestBunGenerateDockerfile(t *testing.T) { path: "../testdata/bun-bunfig", expected: []any{`ARG VERSION=1.1.4`, `ARG INSTALL_CMD="bun install"`, `ARG BUILD_CMD="bun run build:prod"`, `ARG START_CMD="bun run start:production"`}, }, + { + name: "Bun project with build mounts", + path: "../testdata/bun-bunfig", + data: map[string]string{"BuildMounts": `--mount=type=secret,id=_env,target=/app/.env \ + `}, + expected: []any{regexp.MustCompile(`^RUN --mount=type=secret,id=_env,target=/app/.env \\$`)}, + }, + { + name: "Bun project with install mounts", + path: "../testdata/bun-bunfig", + data: map[string]string{"InstallMounts": `--mount=type=secret,id=_env,target=/app/.env \ + `}, + expected: []any{regexp.MustCompile(`^RUN --mount=type=secret,id=_env,target=/app/.env \\$`)}, + }, { name: "Not a Bun project", path: "../testdata/deno", @@ -67,7 +82,7 @@ func TestBunGenerateDockerfile(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { bun := &runtime.Bun{Log: logger} - dockerfile, err := bun.GenerateDockerfile(test.path) + dockerfile, err := bun.GenerateDockerfile(test.path, test.data) if err != nil { t.Errorf("unexpected error: %v", err) } diff --git a/runtime/deno.go b/runtime/deno.go index 816884d..0e77944 100644 --- a/runtime/deno.go +++ b/runtime/deno.go @@ -7,6 +7,7 @@ import ( "fmt" "io/fs" "log/slog" + "maps" "os" "path/filepath" "strings" @@ -70,7 +71,7 @@ func (d *Deno) Match(path string) bool { return detected } -func (d *Deno) GenerateDockerfile(path string) ([]byte, error) { +func (d *Deno) GenerateDockerfile(path string, data ...map[string]string) ([]byte, error) { tmpl, err := template.New("Dockerfile").Parse(denoTemplate) if err != nil { return nil, fmt.Errorf("Failed to parse template") @@ -156,11 +157,15 @@ func (d *Deno) GenerateDockerfile(path string) ([]byte, error) { } var buf bytes.Buffer - if err := tmpl.Option("missingkey=zero").Execute(&buf, map[string]string{ + templateData := map[string]string{ "Version": *version, "InstallCMD": installCMD, "StartCMD": startCMD, - }); err != nil { + } + if len(data) > 0 { + maps.Copy(templateData, data[0]) + } + if err := tmpl.Option("missingkey=zero").Execute(&buf, templateData); err != nil { return nil, fmt.Errorf("Failed to execute template") } @@ -192,7 +197,7 @@ USER nonroot:nonroot ENV PORT=8080 EXPOSE ${PORT} ARG INSTALL_CMD={{.InstallCMD}} -RUN if [ ! -z "${INSTALL_CMD}" ]; then sh -c "$INSTALL_CMD"; fi +RUN {{.InstallMounts}}if [ ! -z "${INSTALL_CMD}" ]; then sh -c "$INSTALL_CMD"; fi ARG START_CMD={{.StartCMD}} ENV START_CMD=${START_CMD} diff --git a/runtime/deno_test.go b/runtime/deno_test.go index fa48552..d4c6f8f 100644 --- a/runtime/deno_test.go +++ b/runtime/deno_test.go @@ -45,6 +45,7 @@ func TestDenoGenerateDockerfile(t *testing.T) { tests := []struct { name string path string + data map[string]string expected []any }{ { @@ -57,6 +58,13 @@ func TestDenoGenerateDockerfile(t *testing.T) { path: "../testdata/deno-jsonc", expected: []any{`ARG VERSION=1.43.3`, `ARG INSTALL_CMD="deno task cache"`, `ARG START_CMD="deno task start"`}, }, + { + name: "Deno project with install mounts", + path: "../testdata/deno-jsonc", + data: map[string]string{"InstallMounts": `--mount=type=secret,id=_env,target=/app/.env \ + `}, + expected: []any{regexp.MustCompile(`^RUN --mount=type=secret,id=_env,target=/app/.env \\$`)}, + }, { name: "Not a Deno project", path: "../testdata/ruby", @@ -67,7 +75,7 @@ func TestDenoGenerateDockerfile(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { deno := &runtime.Deno{Log: logger} - dockerfile, err := deno.GenerateDockerfile(test.path) + dockerfile, err := deno.GenerateDockerfile(test.path, test.data) if err != nil { t.Errorf("unexpected error: %v", err) } diff --git a/runtime/elixir.go b/runtime/elixir.go index 90f3c31..3fe6aba 100644 --- a/runtime/elixir.go +++ b/runtime/elixir.go @@ -5,6 +5,7 @@ import ( "bytes" "fmt" "log/slog" + "maps" "os" "path/filepath" "strings" @@ -35,7 +36,7 @@ func (d *Elixir) Match(path string) bool { return false } -func (d *Elixir) GenerateDockerfile(path string) ([]byte, error) { +func (d *Elixir) GenerateDockerfile(path string, data ...map[string]string) ([]byte, error) { tmpl, err := template.New("Dockerfile").Parse(elixirTemplate) if err != nil { return nil, fmt.Errorf("Failed to parse template") @@ -68,11 +69,15 @@ func (d *Elixir) GenerateDockerfile(path string) ([]byte, error) { ) var buf bytes.Buffer - if err := tmpl.Option("missingkey=zero").Execute(&buf, map[string]string{ + templateData := map[string]string{ "ElixirVersion": *elixirVersion, "OTPVersion": strings.Split(*otpVersion, ".")[0], "BinName": binName, - }); err != nil { + } + if len(data) > 0 { + maps.Copy(templateData, data[0]) + } + if err := tmpl.Option("missingkey=zero").Execute(&buf, templateData); err != nil { return nil, fmt.Errorf("Failed to execute template") } @@ -129,6 +134,7 @@ COPY --from=build --chown=nonroot:nonroot /app/_build/${MIX_ENV}/rel/${BIN_NAME} RUN cp /app/bin/${BIN_NAME} /app/bin/server ENV PORT=8080 +EXPOSE ${PORT} USER nonroot:nonroot CMD ["/app/bin/server", "start"] diff --git a/runtime/golang.go b/runtime/golang.go index b813571..46dd03c 100644 --- a/runtime/golang.go +++ b/runtime/golang.go @@ -5,6 +5,7 @@ import ( "bytes" "fmt" "log/slog" + "maps" "os" "path/filepath" "strings" @@ -36,7 +37,7 @@ func (d *Golang) Match(path string) bool { return false } -func (d *Golang) GenerateDockerfile(path string) ([]byte, error) { +func (d *Golang) GenerateDockerfile(path string, data ...map[string]string) ([]byte, error) { tmpl, err := template.New("Dockerfile").Parse(golangTemplate) if err != nil { return nil, fmt.Errorf("Failed to parse template") @@ -84,10 +85,14 @@ func (d *Golang) GenerateDockerfile(path string) ([]byte, error) { d.Log.Info("Using package: " + pkg) var buf bytes.Buffer - if err := tmpl.Option("missingkey=zero").Execute(&buf, map[string]string{ + templateData := map[string]string{ "Version": *version, "Package": pkg, - }); err != nil { + } + if len(data) > 0 { + maps.Copy(templateData, data[0]) + } + if err := tmpl.Option("missingkey=zero").Execute(&buf, templateData); err != nil { return nil, fmt.Errorf("Failed to execute template") } diff --git a/runtime/java.go b/runtime/java.go index 40f8b4b..5cf9d06 100644 --- a/runtime/java.go +++ b/runtime/java.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "log/slog" + "maps" "os" "path/filepath" "regexp" @@ -46,7 +47,7 @@ func (d *Java) Match(path string) bool { return false } -func (d *Java) GenerateDockerfile(path string) ([]byte, error) { +func (d *Java) GenerateDockerfile(path string, data ...map[string]string) ([]byte, error) { version, err := findJDKVersion(path, d.Log) if err != nil { return nil, err @@ -124,14 +125,17 @@ func (d *Java) GenerateDockerfile(path string) ([]byte, error) { startCMDJSON, _ := json.Marshal(startCMD) startCMD = string(startCMDJSON) } - - if err := tmpl.Option("missingkey=zero").Execute(&buf, map[string]string{ + templateData := map[string]string{ "Version": *version, "GradleVersion": gradleVersion, "MavenVersion": mavenVersion, "BuildCMD": buildCMD, "StartCMD": startCMD, - }); err != nil { + } + if len(data) > 0 { + maps.Copy(templateData, data[0]) + } + if err := tmpl.Option("missingkey=zero").Execute(&buf, templateData); err != nil { return nil, fmt.Errorf("Failed to execute template") } @@ -165,6 +169,7 @@ RUN chown -R nonroot:nonroot /app COPY --from=build --chown=nonroot:nonroot /app/target/*.jar /app/target/ ENV PORT=8080 +EXPOSE ${PORT} USER nonroot:nonroot ARG JAVA_OPTS= @@ -200,6 +205,7 @@ RUN chown -R nonroot:nonroot /app COPY --from=build --chown=nonroot:nonroot /app/build/libs/*.jar /app/build/libs/ ENV PORT=8080 +EXPOSE ${PORT} USER nonroot:nonroot ARG JAVA_OPTS= diff --git a/runtime/main.go b/runtime/main.go index b98e23f..040a8b7 100644 --- a/runtime/main.go +++ b/runtime/main.go @@ -7,7 +7,7 @@ type Runtime interface { // Returns true if the runtime can be used for the given path. Match(path string) bool // Generates a Dockerfile for the given path. - GenerateDockerfile(path string) ([]byte, error) + GenerateDockerfile(path string, data ...map[string]string) ([]byte, error) } type RuntimeName string diff --git a/runtime/nextjs.go b/runtime/nextjs.go index ec5b3ff..ab272e0 100644 --- a/runtime/nextjs.go +++ b/runtime/nextjs.go @@ -5,6 +5,7 @@ import ( "bytes" "fmt" "log/slog" + "maps" "os" "path/filepath" "strings" @@ -42,7 +43,7 @@ func (d *NextJS) Match(path string) bool { return false } -func (d *NextJS) GenerateDockerfile(path string) ([]byte, error) { +func (d *NextJS) GenerateDockerfile(path string, data ...map[string]string) ([]byte, error) { nextJSTemplate := nextJSServerTemplate nextConfigFiles := []string{ "next.config.js", @@ -92,9 +93,13 @@ func (d *NextJS) GenerateDockerfile(path string) ([]byte, error) { } var buf bytes.Buffer - if err := tmpl.Option("missingkey=zero").Execute(&buf, map[string]string{ + templateData := map[string]string{ "Version": *version, - }); err != nil { + } + if len(data) > 0 { + maps.Copy(templateData, data[0]) + } + if err := tmpl.Option("missingkey=zero").Execute(&buf, templateData); err != nil { return nil, fmt.Errorf("Failed to execute template") } @@ -112,7 +117,7 @@ WORKDIR /app # Install dependencies based on the preferred package manager COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* bun.lockb* ./ -RUN if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ +RUN {{.InstallMounts}}if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ elif [ -f package-lock.json ]; then npm ci; \ elif [ -f bun.lockb ]; then npm i -g bun && bun install; \ elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \ @@ -130,7 +135,7 @@ COPY . . # Uncomment the following line in case you want to disable telemetry during the build. ENV NEXT_TELEMETRY_DISABLED 1 -RUN if [ -f yarn.lock ]; then yarn run build; \ +RUN {{.BuildMounts}}if [ -f yarn.lock ]; then yarn run build; \ elif [ -f package-lock.json ]; then npm run build; \ elif [ -f bun.lockb ]; then npm i -g bun && bun run build; \ elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \ @@ -163,8 +168,8 @@ COPY --from=builder --chown=nonroot:nonroot /app/.next/static ./.next/static USER nonroot -EXPOSE 3000 ENV PORT=3000 +EXPOSE ${PORT} # server.js is created by next build from the standalone output # https://nextjs.org/docs/pages/api-reference/next-config-js/output @@ -180,7 +185,7 @@ FROM ${BUILDER}:${VERSION}-slim AS base FROM base AS deps WORKDIR /app COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* bun.lockb* ./ -RUN if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ +RUN {{.InstallMounts}}if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ elif [ -f package-lock.json ]; then npm ci; \ elif [ -f bun.lockb ]; then npm i -g bun && bun install; \ elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \ @@ -193,7 +198,7 @@ ENV NODE_ENV=production WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . -RUN if [ -f yarn.lock ]; then yarn run build; \ +RUN {{.BuildMounts}}if [ -f yarn.lock ]; then yarn run build; \ elif [ -f package-lock.json ]; then npm run build; \ elif [ -f bun.lockb ]; then npm i -g bun && bun run build; \ elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \ @@ -219,5 +224,6 @@ USER nonroot ENV NODE_ENV=production ENV NEXT_TELEMETRY_DISABLED 1 ENV PORT=8080 +EXPOSE ${PORT} CMD ["node_modules/.bin/next", "start", "-H", "0.0.0.0"] `) diff --git a/runtime/nextjs_test.go b/runtime/nextjs_test.go index 3c1cdfd..ba74f63 100644 --- a/runtime/nextjs_test.go +++ b/runtime/nextjs_test.go @@ -45,6 +45,7 @@ func TestNextJSGenerateDockerfile(t *testing.T) { tests := []struct { name string path string + data map[string]string expected []any }{ { @@ -57,6 +58,20 @@ func TestNextJSGenerateDockerfile(t *testing.T) { path: "../testdata/nextjs-standalone", expected: []any{`ARG VERSION=16.0.0`, `CMD HOSTNAME="0.0.0.0" node server.js`}, }, + { + name: "NextJS project with build mounts", + path: "../testdata/nextjs-standalone", + data: map[string]string{"BuildMounts": `--mount=type=secret,id=_env,target=/app/.env \ + `}, + expected: []any{regexp.MustCompile(`^RUN --mount=type=secret,id=_env,target=/app/.env \\$`)}, + }, + { + name: "NextJS project with install mounts", + path: "../testdata/nextjs-standalone", + data: map[string]string{"InstallMounts": `--mount=type=secret,id=_env,target=/app/.env \ + `}, + expected: []any{regexp.MustCompile(`^RUN --mount=type=secret,id=_env,target=/app/.env \\$`)}, + }, { name: "Not a NextJS project", path: "../testdata/deno", @@ -67,7 +82,7 @@ func TestNextJSGenerateDockerfile(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { nextjs := &runtime.NextJS{Log: logger} - dockerfile, err := nextjs.GenerateDockerfile(test.path) + dockerfile, err := nextjs.GenerateDockerfile(test.path, test.data) if err != nil { t.Errorf("unexpected error: %v", err) } diff --git a/runtime/node.go b/runtime/node.go index f04bddc..9d2606a 100644 --- a/runtime/node.go +++ b/runtime/node.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "log/slog" + "maps" "os" "path/filepath" "regexp" @@ -40,7 +41,7 @@ func (d *Node) Match(path string) bool { return false } -func (d *Node) GenerateDockerfile(path string) ([]byte, error) { +func (d *Node) GenerateDockerfile(path string, data ...map[string]string) ([]byte, error) { tmpl, err := template.New("Dockerfile").Parse(nodeTemplate) if err != nil { return nil, fmt.Errorf("Failed to parse template") @@ -141,12 +142,16 @@ func (d *Node) GenerateDockerfile(path string) ([]byte, error) { ) var buf bytes.Buffer - if err := tmpl.Option("missingkey=zero").Execute(&buf, map[string]string{ + templateData := map[string]string{ "Version": *version, "InstallCMD": safeCommand(installCMD), "BuildCMD": safeCommand(buildCMD), "StartCMD": safeCommand(startCMD), - }); err != nil { + } + if len(data) > 0 { + maps.Copy(templateData, data[0]) + } + if err := tmpl.Option("missingkey=zero").Execute(&buf, templateData); err != nil { return nil, errors.New("Failed to execute template") } @@ -176,7 +181,7 @@ COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* bun.lockb* ./ ARG INSTALL_CMD={{.InstallCMD}} ARG NPM_MIRROR= RUN if [ ! -z "${NPM_MIRROR}" ]; then npm config set registry ${NPM_MIRROR}; fi -RUN if [ ! -z "${INSTALL_CMD}" ]; then echo "${INSTALL_CMD}" > dep.sh; sh dep.sh; fi +RUN {{.InstallMounts}}if [ ! -z "${INSTALL_CMD}" ]; then echo "${INSTALL_CMD}" > dep.sh; sh dep.sh; fi FROM base AS builder WORKDIR /app @@ -184,7 +189,7 @@ COPY --from=deps /app/node_modules* ./node_modules COPY . . ENV NODE_ENV=production ARG BUILD_CMD={{.BuildCMD}} -RUN if [ ! -z "${BUILD_CMD}" ]; then sh -c "$BUILD_CMD"; fi +RUN {{.BuildMounts}}if [ ! -z "${BUILD_CMD}" ]; then sh -c "$BUILD_CMD"; fi FROM base AS runtime WORKDIR /app @@ -201,6 +206,7 @@ COPY --chown=nonroot:nonroot --from=builder /app . USER nonroot:nonroot ENV PORT=8080 +EXPOSE ${PORT} ENV NODE_ENV=production ARG START_CMD={{.StartCMD}} ENV START_CMD=${START_CMD} diff --git a/runtime/node_test.go b/runtime/node_test.go index db83356..ff8e7e4 100644 --- a/runtime/node_test.go +++ b/runtime/node_test.go @@ -50,6 +50,7 @@ func TestNodeGenerateDockerfile(t *testing.T) { tests := []struct { name string path string + data map[string]string expected []any }{ { @@ -67,6 +68,20 @@ func TestNodeGenerateDockerfile(t *testing.T) { path: "../testdata/node-yarn", expected: []any{`ARG VERSION=16.0.0`, `ARG INSTALL_CMD="yarn --frozen-lockfile"`, `ARG BUILD_CMD="yarn run build:prod"`, `ARG START_CMD="yarn run start-it"`}, }, + { + name: "Node project with build mounts", + path: "../testdata/node-yarn", + data: map[string]string{"BuildMounts": `--mount=type=secret,id=_env,target=/app/.env \ + `}, + expected: []any{regexp.MustCompile(`^RUN --mount=type=secret,id=_env,target=/app/.env \\$`)}, + }, + { + name: "Node project with install mounts", + path: "../testdata/node-yarn", + data: map[string]string{"InstallMounts": `--mount=type=secret,id=_env,target=/app/.env \ + `}, + expected: []any{regexp.MustCompile(`^RUN --mount=type=secret,id=_env,target=/app/.env \\$`)}, + }, { name: "Not a Node project", path: "../testdata/deno", @@ -77,7 +92,7 @@ func TestNodeGenerateDockerfile(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { node := &runtime.Node{Log: logger} - dockerfile, err := node.GenerateDockerfile(test.path) + dockerfile, err := node.GenerateDockerfile(test.path, test.data) if err != nil { t.Errorf("unexpected error: %v", err) } diff --git a/runtime/php.go b/runtime/php.go index ebffc8c..6c64fd9 100644 --- a/runtime/php.go +++ b/runtime/php.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "log/slog" + "maps" "os" "path/filepath" "regexp" @@ -38,7 +39,7 @@ func (d *PHP) Match(path string) bool { return false } -func (d *PHP) GenerateDockerfile(path string) ([]byte, error) { +func (d *PHP) GenerateDockerfile(path string, data ...map[string]string) ([]byte, error) { tmpl, err := template.New("Dockerfile").Parse(phpTemplate) if err != nil { return nil, fmt.Errorf("Failed to parse template") @@ -128,12 +129,16 @@ func (d *PHP) GenerateDockerfile(path string) ([]byte, error) { ) var buf bytes.Buffer - if err := tmpl.Option("missingkey=zero").Execute(&buf, map[string]string{ + templateData := map[string]string{ "Version": *version, "InstallCMD": safeCommand(installCMD), "BuildCMD": safeCommand(buildCMD), "StartCMD": safeCommand(startCMD), - }); err != nil { + } + if len(data) > 0 { + maps.Copy(templateData, data[0]) + } + if err := tmpl.Option("missingkey=zero").Execute(&buf, templateData); err != nil { return nil, fmt.Errorf("Failed to execute template") } @@ -150,8 +155,8 @@ COPY . . ARG INSTALL_CMD={{.InstallCMD}} ARG BUILD_CMD={{.BuildCMD}} -RUN if [ ! -z "${INSTALL_CMD}" ]; then sh -c "$INSTALL_CMD"; fi -RUN if [ ! -z "${BUILD_CMD}" ]; then sh -c "$BUILD_CMD"; fi +RUN {{.InstallMounts}}if [ ! -z "${INSTALL_CMD}" ]; then sh -c "$INSTALL_CMD"; fi +RUN {{.BuildMounts}}if [ ! -z "${BUILD_CMD}" ]; then sh -c "$BUILD_CMD"; fi FROM php:${VERSION}-apache AS runtime @@ -160,6 +165,7 @@ RUN update-ca-certificates 2>/dev/null || true RUN addgroup --system nonroot && adduser --system --ingroup nonroot nonroot ENV PORT=8080 +EXPOSE ${PORT} RUN sed -i "s/80/${PORT}/g" /etc/apache2/sites-available/000-default.conf /etc/apache2/ports.conf COPY --from=build --chown=nonroot:nonroot /app /var/www/html diff --git a/runtime/php_test.go b/runtime/php_test.go index bcac585..c1407f2 100644 --- a/runtime/php_test.go +++ b/runtime/php_test.go @@ -50,6 +50,7 @@ func TestPHPGenerateDockerfile(t *testing.T) { tests := []struct { name string path string + data map[string]string expected []any }{ { @@ -67,6 +68,20 @@ func TestPHPGenerateDockerfile(t *testing.T) { path: "../testdata/php-npm", expected: []any{`ARG VERSION=8.2.0`, `ARG INSTALL_CMD="yarn --frozen-lockfile"`, `ARG BUILD_CMD="yarn run build"`, `ARG START_CMD="apache2-foreground`}, }, + { + name: "PHP project with build mounts", + path: "../testdata/php-npm", + data: map[string]string{"BuildMounts": `--mount=type=secret,id=_env,target=/app/.env \ + `}, + expected: []any{regexp.MustCompile(`^RUN --mount=type=secret,id=_env,target=/app/.env \\$`)}, + }, + { + name: "PHP project with install mounts", + path: "../testdata/php-npm", + data: map[string]string{"InstallMounts": `--mount=type=secret,id=_env,target=/app/.env \ + `}, + expected: []any{regexp.MustCompile(`^RUN --mount=type=secret,id=_env,target=/app/.env \\$`)}, + }, { name: "Not a PHP project", path: "../testdata/deno", @@ -77,7 +92,7 @@ func TestPHPGenerateDockerfile(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { php := &runtime.PHP{Log: logger} - dockerfile, err := php.GenerateDockerfile(test.path) + dockerfile, err := php.GenerateDockerfile(test.path, test.data) if err != nil { t.Errorf("unexpected error: %v", err) } diff --git a/runtime/python.go b/runtime/python.go index 2639b87..6ec547f 100644 --- a/runtime/python.go +++ b/runtime/python.go @@ -5,6 +5,7 @@ import ( "bytes" "fmt" "log/slog" + "maps" "os" "path/filepath" "strings" @@ -25,6 +26,7 @@ func (d *Python) Match(path string) bool { checkPaths := []string{ filepath.Join(path, "requirements.txt"), filepath.Join(path, "poetry.lock"), + filepath.Join(path, "uv.lock"), filepath.Join(path, "Pipfile.lock"), filepath.Join(path, "pyproject.toml"), filepath.Join(path, "pdm.lock"), @@ -49,7 +51,7 @@ func (d *Python) Match(path string) bool { return false } -func (d *Python) GenerateDockerfile(path string) ([]byte, error) { +func (d *Python) GenerateDockerfile(path string, data ...map[string]string) ([]byte, error) { tmpl, err := template.New("Dockerfile").Parse(pythonTemplate) if err != nil { return nil, fmt.Errorf("Failed to parse template") @@ -62,50 +64,61 @@ func (d *Python) GenerateDockerfile(path string) ([]byte, error) { } installCMD := "" + packageManager := PythonPackageManagerPip if _, err := os.Stat(filepath.Join(path, "requirements.txt")); err == nil { d.Log.Info("Detected requirements.txt file") - installCMD = "pip install -r requirements.txt" + installCMD = "pip install --no-cache -r requirements.txt" + } else if _, err := os.Stat(filepath.Join(path, "uv.lock")); err == nil { + d.Log.Info("Detected a uv project") + installCMD = "pip install uv && uv sync --python-preference=only-system --no-cache --no-dev" + packageManager = PythonPackageManagerUv } else if _, err := os.Stat(filepath.Join(path, "poetry.lock")); err == nil { d.Log.Info("Detected a poetry project") - installCMD = "poetry install --no-dev --no-interactive --no-ansi" + installCMD = "pip install poetry && poetry install --no-dev --no-ansi --no-root" + packageManager = PythonPackageManagerPoetry } else if _, err := os.Stat(filepath.Join(path, "Pipfile.lock")); err == nil { d.Log.Info("Detected a pipenv project") - installCMD = "PIPENV_VENV_IN_PROJECT=1 pipenv install --deploy" + installCMD = "pip install pipenv && pipenv install --dev --system --deploy" + packageManager = PythonPackageManagerPipenv } else if _, err := os.Stat(filepath.Join(path, "pdm.lock")); err == nil { d.Log.Info("Detected a pdm project") - installCMD = "pdm install --prod" + installCMD = "pip install pdm && pdm install --prod" + packageManager = PythonPackageManagerPdm } else if _, err := os.Stat(filepath.Join(path, "pyproject.toml")); err == nil { d.Log.Info("Detected a pyproject.toml file") installCMD = "pip install --upgrade build setuptools && pip install ." } managePy := isDjangoProject(path) + isFastAPI := isFastAPIProject(path) startCMD := "" projectName := filepath.Base(path) if managePy != nil { d.Log.Info("Detected Django project") startCMD = fmt.Sprintf(`python ` + *managePy + ` runserver 0.0.0.0:${PORT}`) - } else if _, err := os.Stat(filepath.Join(path, "pyproject.toml")); err == nil { - f, err := os.Open(filepath.Join(path, "pyproject.toml")) - if err == nil { - var pyprojectTOML map[string]interface{} - err := toml.NewDecoder(f).Decode(&pyprojectTOML) + } else if !isFastAPI { + if _, err := os.Stat(filepath.Join(path, "pyproject.toml")); err == nil { + f, err := os.Open(filepath.Join(path, "pyproject.toml")) if err == nil { - if project, ok := pyprojectTOML["project"].(map[string]interface{}); ok { - if name, ok := project["name"].(string); ok { - projectName = name - } - } else if project, ok := pyprojectTOML["tool.poetry"].(map[string]interface{}); ok { - if name, ok := project["name"].(string); ok { - projectName = name + var pyprojectTOML map[string]interface{} + err := toml.NewDecoder(f).Decode(&pyprojectTOML) + if err == nil { + if project, ok := pyprojectTOML["project"].(map[string]interface{}); ok { + if name, ok := project["name"].(string); ok { + projectName = name + } + } else if project, ok := pyprojectTOML["tool.poetry"].(map[string]interface{}); ok { + if name, ok := project["name"].(string); ok { + projectName = name + } } } - } - if projectName != "" { - startCMD = fmt.Sprintf(`python -m %s`, projectName) - d.Log.Info("Detected start command via pyproject.toml") + if projectName != "" { + startCMD = fmt.Sprintf(`python -m %s`, projectName) + d.Log.Info("Detected start command via pyproject.toml") + } } } } @@ -129,12 +142,24 @@ func (d *Python) GenerateDockerfile(path string) ([]byte, error) { continue } - startCMD = fmt.Sprintf(`python %s`, fn) - d.Log.Info("Detected start command via main file: " + startCMD) + if isFastAPI { + startCMD = fmt.Sprintf(`fastapi run %s --port ${PORT}`, fn) + } else { + startCMD = fmt.Sprintf(`python %s`, fn) + d.Log.Info("Detected start command via main file: " + startCMD) + } break } } + packagerInstructions := "" + switch packageManager { + case PythonPackageManagerPoetry: + packagerInstructions = poetryInstructions + case PythonPackageManagerUv: + packagerInstructions = uvInstructions + } + d.Log.Info( fmt.Sprintf(`Detected defaults Python version : %s @@ -146,11 +171,16 @@ func (d *Python) GenerateDockerfile(path string) ([]byte, error) { ) var buf bytes.Buffer - if err := tmpl.Option("missingkey=zero").Execute(&buf, map[string]string{ - "Version": *version, - "InstallCMD": safeCommand(installCMD), - "StartCMD": safeCommand(startCMD), - }); err != nil { + templateData := map[string]string{ + "Version": *version, + "InstallCMD": safeCommand(installCMD), + "StartCMD": safeCommand(startCMD), + "PackagerInstructions": packagerInstructions, + } + if len(data) > 0 { + maps.Copy(templateData, data[0]) + } + if err := tmpl.Option("missingkey=zero").Execute(&buf, templateData); err != nil { return nil, fmt.Errorf("Failed to execute template") } @@ -166,14 +196,19 @@ RUN apt-get update && apt-get install -y --no-install-recommends wget ca-certifi RUN update-ca-certificates 2>/dev/null || true RUN addgroup --system nonroot && adduser --system --ingroup nonroot nonroot RUN chown -R nonroot:nonroot /app +RUN mkdir -p /var/cache +RUN chown -R nonroot:nonroot /var/cache + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 +{{ .PackagerInstructions }} COPY --chown=nonroot:nonroot . . ARG INSTALL_CMD={{.InstallCMD}} RUN if [ ! -z "${INSTALL_CMD}" ]; then sh -c "$INSTALL_CMD"; fi ENV PORT=8080 -ENV PYTHONDONTWRITEBYTECODE=1 -ENV PYTHONUNBUFFERED=1 +EXPOSE ${PORT} USER nonroot:nonroot ARG START_CMD={{.StartCMD}} @@ -182,6 +217,19 @@ RUN if [ -z "${START_CMD}" ]; then echo "Unable to detect a container start comm CMD ${START_CMD} `) +var poetryInstructions = ` +ENV POETRY_NO_INTERACTION=1 +ENV POETRY_VIRTUALENVS_CREATE=false +ENV POETRY_CACHE_DIR='/var/cache/pypoetry' +ENV POETRY_HOME='/usr/local'` + +var uvInstructions = ` +# Set the UV_CACHE_DIR environment variable to a directory where uv will store its cache +ENV UV_CACHE_DIR='/var/cache/uv' +# Use the virtual environment automatically +ENV VIRTUAL_ENV=/app/.venv +ENV PATH="/app/.venv/bin:$PATH"` + func findPythonVersion(path string, log *slog.Logger) (*string, error) { version := "" versionFiles := []string{ @@ -305,3 +353,41 @@ func isDjangoProject(path string) *string { return nil } + +func isFastAPIProject(path string) bool { + packagerFiles := []string{"requirements.txt", "pyproject.toml", "Pipfile"} + + for _, file := range packagerFiles { + _, err := os.Stat(filepath.Join(path, file)) + if err == nil { + f, err := os.Open(filepath.Join(path, file)) + if err != nil { + return false + } + + defer f.Close() + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := scanner.Text() + if strings.Contains(strings.ToLower(line), "fastapi") { + return true + } + } + + f.Close() + } + } + + return false +} + +type PythonPackageManager string + +const ( + PythonPackageManagerPip PythonPackageManager = "pip" + PythonPackageManagerPoetry PythonPackageManager = "poetry" + PythonPackageManagerUv PythonPackageManager = "uv" + PythonPackageManagerPipenv PythonPackageManager = "pipenv" + PythonPackageManagerPdm PythonPackageManager = "pdm" +) diff --git a/runtime/python_test.go b/runtime/python_test.go index 4e82e4e..1c7e9f4 100644 --- a/runtime/python_test.go +++ b/runtime/python_test.go @@ -67,7 +67,7 @@ func TestPythonGenerateDockerfile(t *testing.T) { path: "../testdata/python", expected: []any{ `ARG VERSION=3.12`, - `ARG INSTALL_CMD="pip install -r requirements.txt"`, + `ARG INSTALL_CMD="pip install --no-cache -r requirements.txt"`, `ARG START_CMD="python main.py"`, }, }, @@ -76,7 +76,7 @@ func TestPythonGenerateDockerfile(t *testing.T) { path: "../testdata/python-django", expected: []any{ `ARG VERSION=3.6.0`, - `ARG INSTALL_CMD="PIPENV_VENV_IN_PROJECT=1 pipenv install --deploy"`, + `ARG INSTALL_CMD="pip install pipenv && pipenv install --dev --system --deploy"`, `ARG START_CMD="python manage.py runserver 0.0.0.0:${PORT}"`, }, }, @@ -85,7 +85,7 @@ func TestPythonGenerateDockerfile(t *testing.T) { path: "../testdata/python-pdm", expected: []any{ `ARG VERSION=3.4.1`, - `ARG INSTALL_CMD="pdm install --prod"`, + `ARG INSTALL_CMD="pip install pdm && pdm install --prod"`, `ARG START_CMD="python app.py"`, }, }, @@ -94,7 +94,7 @@ func TestPythonGenerateDockerfile(t *testing.T) { path: "../testdata/python-poetry", expected: []any{ `ARG VERSION=3.8.5`, - `ARG INSTALL_CMD="poetry install --no-dev --no-interactive --no-ansi"`, + `ARG INSTALL_CMD="pip install poetry && poetry install --no-dev --no-ansi --no-root"`, `ARG START_CMD="python app/main.py"`, }, }, @@ -107,6 +107,15 @@ func TestPythonGenerateDockerfile(t *testing.T) { `ARG START_CMD="python -m pyproject"`, }, }, + { + name: "Python project with FastAPI", + path: "../testdata/python-fastapi", + expected: []any{ + `ARG VERSION=3.6.0`, + `ARG INSTALL_CMD="pip install pipenv && pipenv install --dev --system --deploy"`, + `ARG START_CMD="fastapi run main.py --port ${PORT}"`, + }, + }, { name: "Not a Python project", path: "../testdata/deno", diff --git a/runtime/ruby.go b/runtime/ruby.go index 90ef2b5..a4ab902 100644 --- a/runtime/ruby.go +++ b/runtime/ruby.go @@ -5,6 +5,7 @@ import ( "bytes" "fmt" "log/slog" + "maps" "os" "path/filepath" "strings" @@ -39,7 +40,7 @@ func (d *Ruby) Match(path string) bool { return false } -func (d *Ruby) GenerateDockerfile(path string) ([]byte, error) { +func (d *Ruby) GenerateDockerfile(path string, data ...map[string]string) ([]byte, error) { tmpl, err := template.New("Dockerfile").Parse(rubyTemplate) if err != nil { return nil, fmt.Errorf("Failed to parse template") @@ -117,12 +118,16 @@ func (d *Ruby) GenerateDockerfile(path string) ([]byte, error) { ) var buf bytes.Buffer - if err := tmpl.Option("missingkey=zero").Execute(&buf, map[string]string{ + templateData := map[string]string{ "Version": *version, "InstallCMD": safeCommand(installCMD), "BuildCMD": safeCommand(buildCMD), "StartCMD": safeCommand(startCMD), - }); err != nil { + } + if len(data) > 0 { + maps.Copy(templateData, data[0]) + } + if err := tmpl.Option("missingkey=zero").Execute(&buf, templateData); err != nil { return nil, fmt.Errorf("Failed to execute template") } @@ -145,10 +150,11 @@ ENV NODE_ENV=production RUN chown -R nonroot:nonroot /app COPY --chown=nonroot:nonroot . . -RUN if [ ! -z "${INSTALL_CMD}" ]; then sh -c "$INSTALL_CMD"; fi -RUN if [ ! -z "${BUILD_CMD}" ]; then sh -c "$BUILD_CMD"; fi +RUN {{.InstallMounts}}if [ ! -z "${INSTALL_CMD}" ]; then sh -c "$INSTALL_CMD"; fi +RUN {{.BuildMounts}}if [ ! -z "${BUILD_CMD}" ]; then sh -c "$BUILD_CMD"; fi ENV PORT=8080 +EXPOSE ${PORT} USER nonroot:nonroot ARG START_CMD={{.StartCMD}} diff --git a/runtime/ruby_test.go b/runtime/ruby_test.go index aca0db9..60ec8ae 100644 --- a/runtime/ruby_test.go +++ b/runtime/ruby_test.go @@ -55,6 +55,7 @@ func TestRubyGenerateDockerfile(t *testing.T) { tests := []struct { name string path string + data map[string]string expected []any }{ { @@ -87,6 +88,20 @@ func TestRubyGenerateDockerfile(t *testing.T) { `ARG START_CMD="bundle exec rackup config.ru -p ${PORT}"`, }, }, + { + name: "Ruby project with build mounts", + path: "../testdata/ruby-config-ru", + data: map[string]string{"BuildMounts": `--mount=type=secret,id=_env,target=/app/.env \ + `}, + expected: []any{regexp.MustCompile(`^RUN --mount=type=secret,id=_env,target=/app/.env \\$`)}, + }, + { + name: "Ruby project with install mounts", + path: "../testdata/ruby-config-ru", + data: map[string]string{"InstallMounts": `--mount=type=secret,id=_env,target=/app/.env \ + `}, + expected: []any{regexp.MustCompile(`^RUN --mount=type=secret,id=_env,target=/app/.env \\$`)}, + }, { name: "Ruby project with rails", path: "../testdata/ruby-rails", @@ -107,7 +122,7 @@ func TestRubyGenerateDockerfile(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { ruby := &runtime.Ruby{Log: logger} - dockerfile, err := ruby.GenerateDockerfile(test.path) + dockerfile, err := ruby.GenerateDockerfile(test.path, test.data) if err != nil { t.Errorf("unexpected error: %v", err) } diff --git a/runtime/rust.go b/runtime/rust.go index d99e816..6f88271 100644 --- a/runtime/rust.go +++ b/runtime/rust.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "log/slog" + "maps" "os" "path/filepath" "strings" @@ -36,7 +37,7 @@ func (d *Rust) Match(path string) bool { return false } -func (d *Rust) GenerateDockerfile(path string) ([]byte, error) { +func (d *Rust) GenerateDockerfile(path string, data ...map[string]string) ([]byte, error) { tmpl, err := template.New("Dockerfile").Parse(rustlangTemplate) if err != nil { return nil, fmt.Errorf("Failed to parse template") @@ -87,9 +88,13 @@ func (d *Rust) GenerateDockerfile(path string) ([]byte, error) { } var buf bytes.Buffer - if err := tmpl.Option("missingkey=zero").Execute(&buf, map[string]string{ + templateData := map[string]string{ "BinName": binName, - }); err != nil { + } + if len(data) > 0 { + maps.Copy(templateData, data[0]) + } + if err := tmpl.Option("missingkey=zero").Execute(&buf, templateData); err != nil { return nil, fmt.Errorf("Failed to execute template") } @@ -123,5 +128,6 @@ COPY --chown=nonroot:nonroot --from=build /app/target/*/release/${BIN_NAME} ./ap USER nonroot:nonroot ENV PORT=8080 +EXPOSE ${PORT} CMD ["/app/app"] `) diff --git a/runtime/static.go b/runtime/static.go index 77b21e9..aa5ee5a 100644 --- a/runtime/static.go +++ b/runtime/static.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "log/slog" + "maps" "os" "path/filepath" "strings" @@ -37,7 +38,7 @@ func (d *Static) Match(path string) bool { return false } -func (d *Static) GenerateDockerfile(path string) ([]byte, error) { +func (d *Static) GenerateDockerfile(path string, data ...map[string]string) ([]byte, error) { tmpl, err := template.New("Dockerfile").Parse(staticTemplate) if err != nil { return nil, fmt.Errorf("Failed to parse template") @@ -56,9 +57,13 @@ func (d *Static) GenerateDockerfile(path string) ([]byte, error) { d.Log.Info("Detected root directory: " + serverRoot) var buf bytes.Buffer - if err := tmpl.Option("missingkey=zero").Execute(&buf, map[string]string{ + templateData := map[string]string{ "ServerRoot": serverRoot, - }); err != nil { + } + if len(data) > 0 { + maps.Copy(templateData, data[0]) + } + if err := tmpl.Option("missingkey=zero").Execute(&buf, templateData); err != nil { return nil, fmt.Errorf("Failed to execute template") } @@ -73,6 +78,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends wget && apt-get COPY . . ENV PORT=8080 +EXPOSE ${PORT} ENV SERVER_PORT=${PORT} ARG SERVER_ROOT={{.ServerRoot}} ENV SERVER_ROOT=${SERVER_ROOT} diff --git a/testdata/python-fastapi/.tool-versions b/testdata/python-fastapi/.tool-versions new file mode 100644 index 0000000..62e4ea0 --- /dev/null +++ b/testdata/python-fastapi/.tool-versions @@ -0,0 +1 @@ +python 3.6.0 \ No newline at end of file diff --git a/testdata/python-fastapi/Pipfile b/testdata/python-fastapi/Pipfile new file mode 100644 index 0000000..b4b2c9b --- /dev/null +++ b/testdata/python-fastapi/Pipfile @@ -0,0 +1,10 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +requests = "*" +fastapi = "*" + +[dev-packages] diff --git a/testdata/python-fastapi/Pipfile.lock b/testdata/python-fastapi/Pipfile.lock new file mode 100644 index 0000000..8555936 --- /dev/null +++ b/testdata/python-fastapi/Pipfile.lock @@ -0,0 +1,59 @@ +{ + "_meta": { + "hash": { + "sha256": "bb57e0d7853b45999e47c163c46b95bc2fde31c527d8d7b5b5539dc979444a6d" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "fastapi": { + "hashes": [ + "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3", + "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18" + ], + "index": "pypi", + "version": "==2022.12.7" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:2393a695cd12afedd0dcb26fe5d50d0cf248e5a66f75dbd89a3d4eb333a61af4", + "sha256:a637e5fae88995b256e3409dc4d52c2e2e0ba32c42a6365fee8bbd2238de3cfb" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' and python_version < '4'", + "version": "==1.24.3" + } + }, + "develop": {} +} \ No newline at end of file diff --git a/testdata/python-fastapi/main.py b/testdata/python-fastapi/main.py new file mode 100644 index 0000000..e69de29