Skip to content

Commit b19969e

Browse files
authored
Merge pull request #918 from sanders41/docker
Add Dockerfile for Poetry FastAPI projects
2 parents a1c7518 + 9f31ebc commit b19969e

File tree

7 files changed

+268
-18
lines changed

7 files changed

+268
-18
lines changed

.github/workflows/testing_fastapi.yml

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,90 @@ jobs:
109109
working-directory: ${{ env.WORKING_DIR }}
110110
if: matrix.project_type == 'application'
111111
run: uv run pytest
112+
test-poetry-fastapi-project:
113+
name: test-fastapi-poetry-setup-fastapi
114+
strategy:
115+
fail-fast: false
116+
runs-on: ubuntu-latest
117+
steps:
118+
- uses: actions/checkout@v5
119+
- name: install Rust
120+
uses: dtolnay/rust-toolchain@stable
121+
- name: Cache dependencies
122+
uses: Swatinem/[email protected]
123+
- name: Install sqlx-cli
124+
run: cargo install sqlx-cli --no-default-features -F native-tls -F postgres
125+
- name: Install Poetry
126+
run: pipx install poetry
127+
- name: Configure poetry
128+
run: |
129+
poetry config virtualenvs.create true
130+
poetry config virtualenvs.in-project true
131+
- name: Set up Python
132+
uses: actions/setup-python@v6
133+
with:
134+
python-version: ${{ env.MIN_PYTHON_VERSION }}
135+
- name: Build package
136+
run: cargo build --release -F fastapi
137+
- name: Run creation
138+
run: ./scripts/ci_run_fastapi.sh "fastapi" 2
139+
shell: bash
140+
- name: Install Dependencies
141+
working-directory: ${{ env.WORKING_DIR }}
142+
run: poetry install
143+
- name: Pre-commit check
144+
working-directory: ${{ env.WORKING_DIR }}
145+
run: |
146+
poetry run pre-commit install
147+
git add .
148+
poetry run pre-commit run --all-files
149+
- name: make .env
150+
working-directory: ${{ env.WORKING_DIR }}
151+
run: touch .env
152+
- name: Build and start Docker containers
153+
working-directory: ${{ env.WORKING_DIR }}
154+
run: docker compose up -d
155+
- name: Test with pytest
156+
working-directory: ${{ env.WORKING_DIR }}
157+
run: poetry run pytest -n auto
158+
test-poetry-non-fastapi-project:
159+
name: test-fastapi-poetry-setup-non-fastapi
160+
strategy:
161+
fail-fast: false
162+
matrix:
163+
project_type: ["application", "lib"]
164+
runs-on: ubuntu-latest
165+
steps:
166+
- uses: actions/checkout@v5
167+
- name: install Rust
168+
uses: dtolnay/rust-toolchain@stable
169+
- name: Cache dependencies
170+
uses: Swatinem/[email protected]
171+
- name: Install Poetry
172+
run: pipx install poetry
173+
- name: Configure poetry
174+
run: |
175+
poetry config virtualenvs.create true
176+
poetry config virtualenvs.in-project true
177+
- name: Set up Python
178+
uses: actions/setup-python@v6
179+
with:
180+
python-version: ${{ env.MIN_PYTHON_VERSION }}
181+
- name: Build package
182+
run: cargo build --release -F fastapi
183+
- name: Run creation
184+
run: ./scripts/ci_run_fastapi.sh ${{ matrix.project_type }} 2
185+
shell: bash
186+
- name: Install Dependencies
187+
working-directory: ${{ env.WORKING_DIR }}
188+
run: poetry install
189+
- name: Pre-commit check
190+
working-directory: ${{ env.WORKING_DIR }}
191+
run: |
192+
poetry run pre-commit install
193+
git add .
194+
poetry run pre-commit run --all-files
195+
- name: Test with pytest
196+
working-directory: ${{ env.WORKING_DIR }}
197+
if: matrix.project_type == 'application'
198+
run: poetry run pytest

src/fastapi/docker_files.rs

Lines changed: 160 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -423,16 +423,11 @@ RUN sh /uv-installer.sh && rm /uv-installer.sh
423423
424424
ENV PATH="/root/.local/bin:$PATH"
425425
426-
COPY pyproject.toml uv.lock ./
426+
COPY . ./
427427
428428
RUN --mount=type=cache,target=/root/.cache/uv \
429429
uv venv -p {python_version} \
430-
&& uv sync --locked --no-dev --no-install-project --no-editable
431-
432-
COPY . /app
433-
434-
RUN --mount=type=cache,target=/root/.cache/uv \
435-
uv sync --locked --no-dev --no-editable
430+
&& uv sync --locked --no-dev --no-editable
436431
437432
438433
# Build production stage
@@ -462,6 +457,83 @@ USER appuser
462457
463458
ENTRYPOINT ["./entrypoint.sh"]
464459
"#,
460+
),
461+
ProjectManager::Poetry => format!(
462+
r#"# syntax=docker/dockerfile:1
463+
464+
FROM ubuntu:24.04 AS builder
465+
466+
WORKDIR /app
467+
468+
ENV \
469+
PYTHONUNBUFFERED=true \
470+
POETRY_NO_INTERACTION=true \
471+
POETRY_VIRTUALENVS_IN_PROJECT=true \
472+
POETRY_CACHE_DIR=/tmp/poetry_cache
473+
474+
RUN : \
475+
&& apt-get update \
476+
&& apt-get install -y --no-install-recommends \
477+
curl \
478+
ca-certificates \
479+
software-properties-common \
480+
&& add-apt-repository ppa:deadsnakes/ppa \
481+
&& apt-get update \
482+
&& apt-get install -y --no-install-recommends \
483+
python{python_version} \
484+
&& apt-get clean \
485+
&& rm -rf /var/lib/apt/lists/*
486+
487+
# Install Poetry
488+
RUN curl -sSL https://install.python-poetry.org | python{python_version} -
489+
490+
ENV PATH="/root/.local/bin:$PATH"
491+
492+
COPY pyproject.toml poetry.lock ./
493+
494+
COPY . /app
495+
496+
RUN --mount=type=cache,target=$POETRY_CACHE_DIR \
497+
poetry config virtualenvs.in-project true \
498+
&& poetry install --only=main
499+
500+
501+
# Build production stage
502+
FROM ubuntu:24.04 AS prod
503+
504+
RUN useradd appuser
505+
506+
WORKDIR /app
507+
508+
RUN chown appuser:appuser /app
509+
510+
ENV \
511+
PYTHONUNBUFFERED=true \
512+
PATH="/app/.venv/bin:$PATH" \
513+
PORT="8000"
514+
515+
RUN : \
516+
&& apt-get update \
517+
&& apt-get install -y --no-install-recommends\
518+
software-properties-common \
519+
&& add-apt-repository ppa:deadsnakes/ppa \
520+
&& apt-get update \
521+
&& apt-get install -y --no-install-recommends python{python_version} \
522+
&& apt-get clean \
523+
&& rm -rf /var/lib/apt/lists/*
524+
525+
COPY --from=builder /app/.venv /app/.venv
526+
COPY --from=builder /app/my_project /app/my_project
527+
COPY ./scripts/entrypoint.sh /app
528+
529+
RUN chmod +x /app/entrypoint.sh
530+
531+
EXPOSE 8000
532+
533+
USER appuser
534+
535+
ENTRYPOINT ["./entrypoint.sh"]
536+
"#
465537
),
466538
_ => todo!("Implement this"),
467539
}
@@ -540,3 +612,84 @@ pub fn save_entrypoint_script(project_info: &ProjectInfo) -> Result<()> {
540612

541613
Ok(())
542614
}
615+
616+
#[cfg(test)]
617+
mod tests {
618+
use super::*;
619+
use crate::project_info::{DatabaseManager, LicenseType, ProjectInfo, Pyo3PythonManager};
620+
use insta::assert_yaml_snapshot;
621+
use std::fs::create_dir_all;
622+
use tmp_path::tmp_path;
623+
624+
#[tmp_path]
625+
fn project_info_dummy() -> ProjectInfo {
626+
ProjectInfo {
627+
project_name: "My project".to_string(),
628+
project_slug: "my-project".to_string(),
629+
source_dir: "my_project".to_string(),
630+
project_description: "This is a test".to_string(),
631+
creator: "Arthur Dent".to_string(),
632+
creator_email: "[email protected]".to_string(),
633+
license: LicenseType::Mit,
634+
copyright_year: Some("2023".to_string()),
635+
version: "0.1.0".to_string(),
636+
python_version: "3.11".to_string(),
637+
min_python_version: "3.9".to_string(),
638+
project_manager: ProjectManager::Poetry,
639+
pyo3_python_manager: Some(Pyo3PythonManager::Uv),
640+
is_application: true,
641+
is_async_project: false,
642+
github_actions_python_test_versions: vec![
643+
"3.9".to_string(),
644+
"3.10".to_string(),
645+
"3.11".to_string(),
646+
"3.12".to_string(),
647+
],
648+
max_line_length: 100,
649+
use_dependabot: true,
650+
dependabot_schedule: None,
651+
dependabot_day: None,
652+
use_continuous_deployment: true,
653+
use_release_drafter: true,
654+
use_multi_os_ci: true,
655+
include_docs: false,
656+
docs_info: None,
657+
download_latest_packages: false,
658+
project_root_dir: Some(tmp_path),
659+
is_fastapi_project: true,
660+
database_manager: Some(DatabaseManager::AsyncPg),
661+
}
662+
}
663+
664+
#[test]
665+
fn test_save_dockerfile_uv() {
666+
let mut project_info = project_info_dummy();
667+
project_info.project_manager = ProjectManager::Uv;
668+
let base = project_info.base_dir();
669+
create_dir_all(&base).unwrap();
670+
let expected_file = base.join("Dockerfile");
671+
save_dockerfile(&project_info).unwrap();
672+
673+
assert!(expected_file.is_file());
674+
675+
let content = std::fs::read_to_string(expected_file).unwrap();
676+
677+
assert_yaml_snapshot!(content);
678+
}
679+
680+
#[test]
681+
fn test_save_dockerfile_poetry() {
682+
let mut project_info = project_info_dummy();
683+
project_info.project_manager = ProjectManager::Poetry;
684+
let base = project_info.base_dir();
685+
create_dir_all(&base).unwrap();
686+
let expected_file = base.join("Dockerfile");
687+
save_dockerfile(&project_info).unwrap();
688+
689+
assert!(expected_file.is_file());
690+
691+
let content = std::fs::read_to_string(expected_file).unwrap();
692+
693+
assert_yaml_snapshot!(content);
694+
}
695+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
source: src/fastapi/docker_files.rs
3+
expression: content
4+
---
5+
"# syntax=docker/dockerfile:1\n\nFROM ubuntu:24.04 AS builder\n\nWORKDIR /app\n\nENV \\\n PYTHONUNBUFFERED=true \\\n POETRY_NO_INTERACTION=true \\\n POETRY_VIRTUALENVS_IN_PROJECT=true \\\n POETRY_CACHE_DIR=/tmp/poetry_cache\n\nRUN : \\\n && apt-get update \\\n && apt-get install -y --no-install-recommends \\\n curl \\\n ca-certificates \\\n software-properties-common \\\n && add-apt-repository ppa:deadsnakes/ppa \\\n && apt-get update \\\n && apt-get install -y --no-install-recommends \\\n python3.11 \\\n && apt-get clean \\\n && rm -rf /var/lib/apt/lists/*\n\n# Install Poetry\nRUN curl -sSL https://install.python-poetry.org | python3.11 -\n\nENV PATH=\"/root/.local/bin:$PATH\"\n\nCOPY pyproject.toml poetry.lock ./\n\nCOPY . /app\n\nRUN --mount=type=cache,target=$POETRY_CACHE_DIR \\\n poetry config virtualenvs.in-project true \\\n && poetry install --only=main\n\n\n# Build production stage\nFROM ubuntu:24.04 AS prod\n\nRUN useradd appuser\n\nWORKDIR /app\n\nRUN chown appuser:appuser /app\n\nENV \\\n PYTHONUNBUFFERED=true \\\n PATH=\"/app/.venv/bin:$PATH\" \\\n PORT=\"8000\"\n\nRUN : \\\n && apt-get update \\\n && apt-get install -y --no-install-recommends\\\n software-properties-common \\\n && add-apt-repository ppa:deadsnakes/ppa \\\n && apt-get update \\\n && apt-get install -y --no-install-recommends python3.11 \\\n && apt-get clean \\\n && rm -rf /var/lib/apt/lists/*\n\nCOPY --from=builder /app/.venv /app/.venv\nCOPY --from=builder /app/my_project /app/my_project\nCOPY ./scripts/entrypoint.sh /app\n\nRUN chmod +x /app/entrypoint.sh\n\nEXPOSE 8000\n\nUSER appuser\n\nENTRYPOINT [\"./entrypoint.sh\"]\n"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
source: src/fastapi/docker_files.rs
3+
expression: content
4+
---
5+
"# syntax=docker/dockerfile:1\n\nFROM ubuntu:24.04 AS builder\n\nWORKDIR /app\n\nENV \\\n PYTHONUNBUFFERED=true \\\n UV_PYTHON_INSTALL_DIR=/opt/uv/python \\\n UV_LINK_MODE=copy\n\nRUN : \\\n && apt-get update \\\n && apt-get install -y --no-install-recommends \\\n curl \\\n ca-certificates \\\n && apt-get clean \\\n && rm -rf /var/lib/apt/lists/*\n\n# Install uv\nADD https://astral.sh/uv/install.sh /uv-installer.sh\n\nRUN sh /uv-installer.sh && rm /uv-installer.sh\n\nENV PATH=\"/root/.local/bin:$PATH\"\n\nCOPY . ./\n\nRUN --mount=type=cache,target=/root/.cache/uv \\\n uv venv -p 3.11 \\\n && uv sync --locked --no-dev --no-editable\n\n\n# Build production stage\nFROM ubuntu:24.04 AS prod\n\nRUN useradd appuser\n\nWORKDIR /app\n\nRUN chown appuser:appuser /app\n\nENV \\\n PYTHONUNBUFFERED=true \\\n PATH=\"/app/.venv/bin:$PATH\" \\\n PORT=\"8000\"\n\nCOPY --from=builder /app/.venv /app/.venv\nCOPY --from=builder /app/my_project /app/my_project\nCOPY --from=builder /opt/uv/python /opt/uv/python\nCOPY ./scripts/entrypoint.sh /app\n\nRUN chmod +x /app/entrypoint.sh\n\nEXPOSE 8000\n\nUSER appuser\n\nENTRYPOINT [\"./entrypoint.sh\"]\n"

src/project_generator.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use crate::{
1717
project_info::{LicenseType, ProjectInfo, ProjectManager, Pyo3PythonManager},
1818
python_files::generate_python_files,
1919
rust_files::{save_cargo_toml_file, save_lib_file},
20-
utils::is_python_312_or_greater,
20+
utils::is_python_311_or_greater,
2121
};
2222

2323
#[cfg(feature = "fastapi")]
@@ -302,7 +302,7 @@ fn build_latest_dev_dependencies(project_info: &ProjectInfo) -> Result<String> {
302302
packages.push(PythonPackageVersion::new(PythonPackage::PytestCov));
303303
packages.push(PythonPackageVersion::new(PythonPackage::Ruff));
304304

305-
if !is_python_312_or_greater(&project_info.min_python_version)?
305+
if !is_python_311_or_greater(&project_info.min_python_version)?
306306
&& matches!(project_info.project_manager, ProjectManager::Poetry)
307307
{
308308
packages.push(PythonPackageVersion::new(PythonPackage::Tomli));

src/python_files.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use anyhow::{bail, Result};
55
use crate::{
66
file_manager::save_file_with_content,
77
project_info::{ProjectInfo, ProjectManager},
8-
utils::is_python_312_or_greater,
8+
utils::is_python_311_or_greater,
99
};
1010

1111
fn create_dunder_main_file(module: &str, is_async_project: bool) -> String {
@@ -235,7 +235,7 @@ fn create_version_test_file(
235235
};
236236

237237
if let Some(v) = version_test {
238-
if is_python_312_or_greater(min_python_version)? {
238+
if is_python_311_or_greater(min_python_version)? {
239239
Ok(Some(format!(
240240
r#"import tomllib
241241
from pathlib import Path

src/utils.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
use anyhow::{bail, Result};
22

3-
pub fn is_python_312_or_greater(version: &str) -> Result<bool> {
3+
pub fn is_python_311_or_greater(version: &str) -> Result<bool> {
44
let version_parts = split_version(version)?;
55

6-
if version_parts.1 >= 12 {
6+
if version_parts.1 >= 11 {
77
Ok(true)
88
} else {
99
Ok(false)
@@ -39,19 +39,19 @@ mod tests {
3939

4040
#[test]
4141
fn test_python_312() {
42-
let result = is_python_312_or_greater("3.12").unwrap();
42+
let result = is_python_311_or_greater("3.12").unwrap();
4343
assert!(result);
4444
}
4545

4646
#[test]
47-
fn test_python_313() {
48-
let result = is_python_312_or_greater("3.13").unwrap();
47+
fn test_python_311_311() {
48+
let result = is_python_311_or_greater("3.11").unwrap();
4949
assert!(result);
5050
}
5151

5252
#[test]
53-
fn test_python_311() {
54-
let result = is_python_312_or_greater("3.11").unwrap();
53+
fn test_python_311_310() {
54+
let result = is_python_311_or_greater("3.10").unwrap();
5555
assert!(!result);
5656
}
5757

0 commit comments

Comments
 (0)