Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 9 additions & 12 deletions src/fastapi_cloud_cli/commands/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import tarfile
import tempfile
import time
import uuid
from enum import Enum
from itertools import cycle
from pathlib import Path
Expand Down Expand Up @@ -46,19 +45,14 @@ def _should_exclude_entry(path: Path) -> bool:
return False


def archive(path: Path) -> Path:
def archive(path: Path, tar_path: Path) -> Path:
logger.debug("Starting archive creation for path: %s", path)
files = rignore.walk(
path,
should_exclude_entry=_should_exclude_entry,
additional_ignore_paths=[".fastapicloudignore"],
)

temp_dir = tempfile.mkdtemp()
logger.debug("Created temp directory: %s", temp_dir)

name = f"fastapi-cloud-deploy-{uuid.uuid4()}"
tar_path = Path(temp_dir) / f"{name}.tar"
logger.debug("Archive will be created at: %s", tar_path)

file_count = 0
Expand Down Expand Up @@ -586,11 +580,14 @@ def deploy(
)
raise typer.Exit(1)

logger.debug("Creating archive for deployment")
archive_path = archive(path or Path.cwd()) # noqa: F841
with tempfile.TemporaryDirectory() as temp_dir:
logger.debug("Creating archive for deployment")
archive_path = Path(temp_dir) / "archive.tar"
archive(path or Path.cwd(), archive_path)

with toolkit.progress(title="Creating deployment") as progress:
with handle_http_errors(progress):
with toolkit.progress(
title="Creating deployment"
) as progress, handle_http_errors(progress):
logger.debug("Creating deployment for app: %s", app.id)
deployment = _create_deployment(app.id)

Expand All @@ -602,7 +599,7 @@ def deploy(

_upload_deployment(deployment.id, archive_path)

progress.log("Deployment uploaded successfully!")
progress.log("Deployment uploaded successfully!")

toolkit.print_line()

Expand Down
103 changes: 59 additions & 44 deletions tests/test_archive.py
Original file line number Diff line number Diff line change
@@ -1,76 +1,89 @@
import tarfile
from pathlib import Path

import pytest

from fastapi_cloud_cli.commands.deploy import archive


def test_archive_creates_tar_file(tmp_path: Path) -> None:
(tmp_path / "main.py").write_text("print('hello')")
(tmp_path / "config.json").write_text('{"key": "value"}')
(tmp_path / "subdir").mkdir()
(tmp_path / "subdir" / "utils.py").write_text("def helper(): pass")
@pytest.fixture
def src_path(tmp_path: Path) -> Path:
path = tmp_path / "source"
path.mkdir()
return path


@pytest.fixture
def tar_path(tmp_path: Path) -> Path:
return tmp_path / "archive.tar"


tar_path = archive(tmp_path)
def test_archive_creates_tar_file(src_path: Path, tar_path: Path) -> None:
(src_path / "main.py").write_text("print('hello')")
(src_path / "config.json").write_text('{"key": "value"}')
(src_path / "subdir").mkdir()
(src_path / "subdir" / "utils.py").write_text("def helper(): pass")

archive(src_path, tar_path)
assert tar_path.exists()
assert tar_path.suffix == ".tar"
assert tar_path.name.startswith("fastapi-cloud-deploy-")


def test_archive_excludes_venv_and_similar_folders(tmp_path: Path) -> None:
def test_archive_excludes_venv_and_similar_folders(
src_path: Path, tar_path: Path
) -> None:
"""Should exclude .venv directory from archive."""
# the only files we want to include
(tmp_path / "main.py").write_text("print('hello')")
(tmp_path / "static").mkdir()
(tmp_path / "static" / "index.html").write_text("<html></html>")
(src_path / "main.py").write_text("print('hello')")
(src_path / "static").mkdir()
(src_path / "static" / "index.html").write_text("<html></html>")
# virtualenv
(tmp_path / ".venv").mkdir()
(tmp_path / ".venv" / "lib").mkdir()
(tmp_path / ".venv" / "lib" / "package.py").write_text("# package")
(src_path / ".venv").mkdir()
(src_path / ".venv" / "lib").mkdir()
(src_path / ".venv" / "lib" / "package.py").write_text("# package")
# pycache
(tmp_path / "__pycache__").mkdir()
(tmp_path / "__pycache__" / "main.cpython-311.pyc").write_text("bytecode")
(src_path / "__pycache__").mkdir()
(src_path / "__pycache__" / "main.cpython-311.pyc").write_text("bytecode")
# pyc files
(tmp_path / "main.pyc").write_text("bytecode")
(src_path / "main.pyc").write_text("bytecode")
# mypy/pytest
(tmp_path / ".mypy_cache").mkdir()
(tmp_path / ".mypy_cache" / "file.json").write_text("{}")
(tmp_path / ".pytest_cache").mkdir()
(tmp_path / ".pytest_cache" / "cache.db").write_text("data")
(src_path / ".mypy_cache").mkdir()
(src_path / ".mypy_cache" / "file.json").write_text("{}")
(src_path / ".pytest_cache").mkdir()
(src_path / ".pytest_cache" / "cache.db").write_text("data")

tar_path = archive(tmp_path)
archive(src_path, tar_path)

with tarfile.open(tar_path, "r") as tar:
names = tar.getnames()
assert set(names) == {"main.py", "static/index.html"}


def test_archive_preserves_relative_paths(tmp_path: Path) -> None:
(tmp_path / "src").mkdir()
(tmp_path / "src" / "app").mkdir()
(tmp_path / "src" / "app" / "main.py").write_text("print('hello')")
def test_archive_preserves_relative_paths(src_path: Path, tar_path: Path) -> None:
(src_path / "src").mkdir()
(src_path / "src" / "app").mkdir()
(src_path / "src" / "app" / "main.py").write_text("print('hello')")

tar_path = archive(tmp_path)
archive(src_path, tar_path)

with tarfile.open(tar_path, "r") as tar:
names = tar.getnames()
assert names == ["src/app/main.py"]


def test_archive_respects_fastapicloudignore(tmp_path: Path) -> None:
def test_archive_respects_fastapicloudignore(src_path: Path, tar_path: Path) -> None:
"""Should exclude files specified in .fastapicloudignore."""
# Create test files
(tmp_path / "main.py").write_text("print('hello')")
(tmp_path / "config.py").write_text("CONFIG = 'value'")
(tmp_path / "secrets.env").write_text("SECRET_KEY=xyz")
(tmp_path / "data").mkdir()
(tmp_path / "data" / "file.txt").write_text("data")
(src_path / "main.py").write_text("print('hello')")
(src_path / "config.py").write_text("CONFIG = 'value'")
(src_path / "secrets.env").write_text("SECRET_KEY=xyz")
(src_path / "data").mkdir()
(src_path / "data" / "file.txt").write_text("data")

# Create .fastapicloudignore file
(tmp_path / ".fastapicloudignore").write_text("secrets.env\ndata/\n")
(src_path / ".fastapicloudignore").write_text("secrets.env\ndata/\n")

# Create archive
tar_path = archive(tmp_path)
archive(src_path, tar_path)

# Verify ignored files are excluded
with tarfile.open(tar_path, "r") as tar:
Expand All @@ -81,21 +94,23 @@ def test_archive_respects_fastapicloudignore(tmp_path: Path) -> None:
}


def test_archive_respects_fastapicloudignore_unignore(tmp_path: Path) -> None:
def test_archive_respects_fastapicloudignore_unignore(
src_path: Path, tar_path: Path
) -> None:
"""Test we can use .fastapicloudignore to unignore files inside .gitignore"""
# Create test files
(tmp_path / "main.py").write_text("print('hello')")
(tmp_path / "static/build").mkdir(exist_ok=True, parents=True)
(tmp_path / "static/build/style.css").write_text("body { background: #bada55 }")
(src_path / "main.py").write_text("print('hello')")
(src_path / "static/build").mkdir(exist_ok=True, parents=True)
(src_path / "static/build/style.css").write_text("body { background: #bada55 }")
# Rignore needs a .git folder to make .gitignore work
(tmp_path / ".git").mkdir(exist_ok=True, parents=True)
(tmp_path / ".gitignore").write_text("build/")
(src_path / ".git").mkdir(exist_ok=True, parents=True)
(src_path / ".gitignore").write_text("build/")

# Create .fastapicloudignore file
(tmp_path / ".fastapicloudignore").write_text("!static/build")
(src_path / ".fastapicloudignore").write_text("!static/build")

# Create archive
tar_path = archive(tmp_path)
archive(src_path, tar_path)

# Verify ignored files are excluded
with tarfile.open(tar_path, "r") as tar:
Expand Down