diff --git a/.github/workflows/testing_fastapi.yml b/.github/workflows/testing_fastapi.yml index 7b93fee8..0adce0b7 100644 --- a/.github/workflows/testing_fastapi.yml +++ b/.github/workflows/testing_fastapi.yml @@ -109,3 +109,90 @@ jobs: working-directory: ${{ env.WORKING_DIR }} if: matrix.project_type == 'application' run: uv run pytest + test-poetry-fastapi-project: + name: test-fastapi-poetry-setup-fastapi + strategy: + fail-fast: false + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: install Rust + uses: dtolnay/rust-toolchain@stable + - name: Cache dependencies + uses: Swatinem/rust-cache@v2.8.1 + - name: Install sqlx-cli + run: cargo install sqlx-cli --no-default-features -F native-tls -F postgres + - name: Install Poetry + run: pipx install poetry + - name: Configure poetry + run: | + poetry config virtualenvs.create true + poetry config virtualenvs.in-project true + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: ${{ env.MIN_PYTHON_VERSION }} + - name: Build package + run: cargo build --release -F fastapi + - name: Run creation + run: ./scripts/ci_run_fastapi.sh "fastapi" 2 + shell: bash + - name: Install Dependencies + working-directory: ${{ env.WORKING_DIR }} + run: poetry install + - name: Pre-commit check + working-directory: ${{ env.WORKING_DIR }} + run: | + poetry run pre-commit install + git add . + poetry run pre-commit run --all-files + - name: make .env + working-directory: ${{ env.WORKING_DIR }} + run: touch .env + - name: Build and start Docker containers + working-directory: ${{ env.WORKING_DIR }} + run: docker compose up -d + - name: Test with pytest + working-directory: ${{ env.WORKING_DIR }} + run: poetry run pytest -n auto + test-poetry-non-fastapi-project: + name: test-fastapi-poetry-setup-non-fastapi + strategy: + fail-fast: false + matrix: + project_type: ["application", "lib"] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: install Rust + uses: dtolnay/rust-toolchain@stable + - name: Cache dependencies + uses: Swatinem/rust-cache@v2.8.1 + - name: Install Poetry + run: pipx install poetry + - name: Configure poetry + run: | + poetry config virtualenvs.create true + poetry config virtualenvs.in-project true + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: ${{ env.MIN_PYTHON_VERSION }} + - name: Build package + run: cargo build --release -F fastapi + - name: Run creation + run: ./scripts/ci_run_fastapi.sh ${{ matrix.project_type }} 2 + shell: bash + - name: Install Dependencies + working-directory: ${{ env.WORKING_DIR }} + run: poetry install + - name: Pre-commit check + working-directory: ${{ env.WORKING_DIR }} + run: | + poetry run pre-commit install + git add . + poetry run pre-commit run --all-files + - name: Test with pytest + working-directory: ${{ env.WORKING_DIR }} + if: matrix.project_type == 'application' + run: poetry run pytest diff --git a/src/fastapi/docker_files.rs b/src/fastapi/docker_files.rs index f2d92599..0a226e79 100644 --- a/src/fastapi/docker_files.rs +++ b/src/fastapi/docker_files.rs @@ -423,16 +423,11 @@ RUN sh /uv-installer.sh && rm /uv-installer.sh ENV PATH="/root/.local/bin:$PATH" -COPY pyproject.toml uv.lock ./ +COPY . ./ RUN --mount=type=cache,target=/root/.cache/uv \ uv venv -p {python_version} \ - && uv sync --locked --no-dev --no-install-project --no-editable - -COPY . /app - -RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-dev --no-editable + && uv sync --locked --no-dev --no-editable # Build production stage @@ -462,6 +457,83 @@ USER appuser ENTRYPOINT ["./entrypoint.sh"] "#, + ), + ProjectManager::Poetry => format!( + r#"# syntax=docker/dockerfile:1 + +FROM ubuntu:24.04 AS builder + +WORKDIR /app + +ENV \ + PYTHONUNBUFFERED=true \ + POETRY_NO_INTERACTION=true \ + POETRY_VIRTUALENVS_IN_PROJECT=true \ + POETRY_CACHE_DIR=/tmp/poetry_cache + +RUN : \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + curl \ + ca-certificates \ + software-properties-common \ + && add-apt-repository ppa:deadsnakes/ppa \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + python{python_version} \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Install Poetry +RUN curl -sSL https://install.python-poetry.org | python{python_version} - + +ENV PATH="/root/.local/bin:$PATH" + +COPY pyproject.toml poetry.lock ./ + +COPY . /app + +RUN --mount=type=cache,target=$POETRY_CACHE_DIR \ + poetry config virtualenvs.in-project true \ + && poetry install --only=main + + +# Build production stage +FROM ubuntu:24.04 AS prod + +RUN useradd appuser + +WORKDIR /app + +RUN chown appuser:appuser /app + +ENV \ + PYTHONUNBUFFERED=true \ + PATH="/app/.venv/bin:$PATH" \ + PORT="8000" + +RUN : \ + && apt-get update \ + && apt-get install -y --no-install-recommends\ + software-properties-common \ + && add-apt-repository ppa:deadsnakes/ppa \ + && apt-get update \ + && apt-get install -y --no-install-recommends python{python_version} \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=builder /app/.venv /app/.venv +COPY --from=builder /app/my_project /app/my_project +COPY ./scripts/entrypoint.sh /app + +RUN chmod +x /app/entrypoint.sh + +EXPOSE 8000 + +USER appuser + +ENTRYPOINT ["./entrypoint.sh"] +"# ), _ => todo!("Implement this"), } @@ -540,3 +612,84 @@ pub fn save_entrypoint_script(project_info: &ProjectInfo) -> Result<()> { Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::project_info::{DatabaseManager, LicenseType, ProjectInfo, Pyo3PythonManager}; + use insta::assert_yaml_snapshot; + use std::fs::create_dir_all; + use tmp_path::tmp_path; + + #[tmp_path] + fn project_info_dummy() -> ProjectInfo { + ProjectInfo { + project_name: "My project".to_string(), + project_slug: "my-project".to_string(), + source_dir: "my_project".to_string(), + project_description: "This is a test".to_string(), + creator: "Arthur Dent".to_string(), + creator_email: "authur@heartofgold.com".to_string(), + license: LicenseType::Mit, + copyright_year: Some("2023".to_string()), + version: "0.1.0".to_string(), + python_version: "3.11".to_string(), + min_python_version: "3.9".to_string(), + project_manager: ProjectManager::Poetry, + pyo3_python_manager: Some(Pyo3PythonManager::Uv), + is_application: true, + is_async_project: false, + github_actions_python_test_versions: vec![ + "3.9".to_string(), + "3.10".to_string(), + "3.11".to_string(), + "3.12".to_string(), + ], + max_line_length: 100, + use_dependabot: true, + dependabot_schedule: None, + dependabot_day: None, + use_continuous_deployment: true, + use_release_drafter: true, + use_multi_os_ci: true, + include_docs: false, + docs_info: None, + download_latest_packages: false, + project_root_dir: Some(tmp_path), + is_fastapi_project: true, + database_manager: Some(DatabaseManager::AsyncPg), + } + } + + #[test] + fn test_save_dockerfile_uv() { + let mut project_info = project_info_dummy(); + project_info.project_manager = ProjectManager::Uv; + let base = project_info.base_dir(); + create_dir_all(&base).unwrap(); + let expected_file = base.join("Dockerfile"); + save_dockerfile(&project_info).unwrap(); + + assert!(expected_file.is_file()); + + let content = std::fs::read_to_string(expected_file).unwrap(); + + assert_yaml_snapshot!(content); + } + + #[test] + fn test_save_dockerfile_poetry() { + let mut project_info = project_info_dummy(); + project_info.project_manager = ProjectManager::Poetry; + let base = project_info.base_dir(); + create_dir_all(&base).unwrap(); + let expected_file = base.join("Dockerfile"); + save_dockerfile(&project_info).unwrap(); + + assert!(expected_file.is_file()); + + let content = std::fs::read_to_string(expected_file).unwrap(); + + assert_yaml_snapshot!(content); + } +} diff --git a/src/fastapi/snapshots/python_project__fastapi__docker_files__tests__save_dockerfile_poetry.snap b/src/fastapi/snapshots/python_project__fastapi__docker_files__tests__save_dockerfile_poetry.snap new file mode 100644 index 00000000..576bedd9 --- /dev/null +++ b/src/fastapi/snapshots/python_project__fastapi__docker_files__tests__save_dockerfile_poetry.snap @@ -0,0 +1,5 @@ +--- +source: src/fastapi/docker_files.rs +expression: content +--- +"# 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" diff --git a/src/fastapi/snapshots/python_project__fastapi__docker_files__tests__save_dockerfile_uv.snap b/src/fastapi/snapshots/python_project__fastapi__docker_files__tests__save_dockerfile_uv.snap new file mode 100644 index 00000000..08525d3c --- /dev/null +++ b/src/fastapi/snapshots/python_project__fastapi__docker_files__tests__save_dockerfile_uv.snap @@ -0,0 +1,5 @@ +--- +source: src/fastapi/docker_files.rs +expression: content +--- +"# 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" diff --git a/src/project_generator.rs b/src/project_generator.rs index cbc1bd71..98d1107c 100644 --- a/src/project_generator.rs +++ b/src/project_generator.rs @@ -17,7 +17,7 @@ use crate::{ project_info::{LicenseType, ProjectInfo, ProjectManager, Pyo3PythonManager}, python_files::generate_python_files, rust_files::{save_cargo_toml_file, save_lib_file}, - utils::is_python_312_or_greater, + utils::is_python_311_or_greater, }; #[cfg(feature = "fastapi")] @@ -302,7 +302,7 @@ fn build_latest_dev_dependencies(project_info: &ProjectInfo) -> Result { packages.push(PythonPackageVersion::new(PythonPackage::PytestCov)); packages.push(PythonPackageVersion::new(PythonPackage::Ruff)); - if !is_python_312_or_greater(&project_info.min_python_version)? + if !is_python_311_or_greater(&project_info.min_python_version)? && matches!(project_info.project_manager, ProjectManager::Poetry) { packages.push(PythonPackageVersion::new(PythonPackage::Tomli)); diff --git a/src/python_files.rs b/src/python_files.rs index aebc1cbb..da1b1534 100644 --- a/src/python_files.rs +++ b/src/python_files.rs @@ -5,7 +5,7 @@ use anyhow::{bail, Result}; use crate::{ file_manager::save_file_with_content, project_info::{ProjectInfo, ProjectManager}, - utils::is_python_312_or_greater, + utils::is_python_311_or_greater, }; fn create_dunder_main_file(module: &str, is_async_project: bool) -> String { @@ -235,7 +235,7 @@ fn create_version_test_file( }; if let Some(v) = version_test { - if is_python_312_or_greater(min_python_version)? { + if is_python_311_or_greater(min_python_version)? { Ok(Some(format!( r#"import tomllib from pathlib import Path diff --git a/src/utils.rs b/src/utils.rs index 0dbfbc4f..bb1642b8 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,9 +1,9 @@ use anyhow::{bail, Result}; -pub fn is_python_312_or_greater(version: &str) -> Result { +pub fn is_python_311_or_greater(version: &str) -> Result { let version_parts = split_version(version)?; - if version_parts.1 >= 12 { + if version_parts.1 >= 11 { Ok(true) } else { Ok(false) @@ -39,19 +39,19 @@ mod tests { #[test] fn test_python_312() { - let result = is_python_312_or_greater("3.12").unwrap(); + let result = is_python_311_or_greater("3.12").unwrap(); assert!(result); } #[test] - fn test_python_313() { - let result = is_python_312_or_greater("3.13").unwrap(); + fn test_python_311_311() { + let result = is_python_311_or_greater("3.11").unwrap(); assert!(result); } #[test] - fn test_python_311() { - let result = is_python_312_or_greater("3.11").unwrap(); + fn test_python_311_310() { + let result = is_python_311_or_greater("3.10").unwrap(); assert!(!result); }