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
45 changes: 37 additions & 8 deletions Dockerfile
Copy link
Contributor Author

@ghukill ghukill Aug 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unsure at this time if this will be a pattern we want to adopt elsewhere? But it seems to work well here. It uses a two-part build process:

  1. FROM python:3.13-slim AS builder establishes the first build layer
  2. We generate a requirements.txt file from the project's pyproject.toml and uv.lock files
  3. We start a new layer FROM python:3.13-slim (the final layer)
  4. We use COPY --from=builder /requirements.txt /requirements.txt to get that important requirements.txt file, but that's the only asset from the previous layer
  5. We use the global python environment and install dependencies
  6. We launch the click CLI in a pretty boring way, no uv scripts involved

Original file line number Diff line number Diff line change
@@ -1,12 +1,41 @@
FROM python:3.12-slim as build
WORKDIR /app
COPY . .
#========================================
# builder layer
#========================================
FROM python:3.13-slim AS builder

# install uv and use system python
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
ENV UV_SYSTEM_PYTHON=1

COPY pyproject.toml pyproject.toml
COPY uv.lock uv.lock

RUN uv export --no-dev --format requirements.txt -o /requirements.txt

#========================================
# final image
#========================================
FROM python:3.13-slim

RUN pip install --no-cache-dir --upgrade pip pipenv
# install git
RUN apt-get update && \
apt-get install -y --no-install-recommends git ca-certificates

RUN apt-get update && apt-get upgrade -y && apt-get install -y git
# install uv and use system python
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
ENV UV_SYSTEM_PYTHON=1

# get pylock.toml from builder layer
COPY --from=builder /requirements.txt /requirements.txt

# install dependencies to global python
RUN uv pip install -r /requirements.txt

# copy application
WORKDIR /app
COPY ./launcher /app/launcher

COPY Pipfile* /
RUN pipenv install
EXPOSE 2718

ENTRYPOINT ["pipenv", "run", "my_app"]
ENTRYPOINT ["python", "-m", "launcher.cli"]
CMD []
11 changes: 10 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ update: # Update Python dependencies
######################

test: # Run tests and print a coverage report
uv run coverage run --source=my_app -m pytest -vv
uv run coverage run --source=launcher -m pytest -vv
uv run coverage report -m

coveralls: test # Write coverage data to an LCOV report
Expand Down Expand Up @@ -65,3 +65,12 @@ black-apply: # Apply changes with 'black'

ruff-apply: # Resolve 'fixable errors' with 'ruff'
uv run ruff check --fix .

####################################
# Docker
####################################
build: # Build local image for testing
docker build -t marimo-launcher:latest .

shell: # Shell into local container for testing
docker run -it --entrypoint='bash' marimo-launcher:latest
34 changes: 3 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,4 @@
# python-cli-template

A template repository for creating Python CLI applications.

## App Setup (delete this section and above after initial application setup)

1. Rename "my_app" to the desired app name across the repo. (May be helpful to do a project-wide find-and-replace).
2. Update Python version if needed.
3. Install all dependencies with `make install` to create initial Pipfile.lock with latest dependency versions.
4. Add initial app description to README and update initial required ENV variable documentation as needed.
5. Update license if needed (check app-specific dependencies for licensing terms).
6. Check Github repository settings:
- Confirm repo branch protection settings are correct (see [dev docs](https://mitlibraries.github.io/guides/basics/github.html) for details)
- Confirm that all of the following are enabled in the repo's code security and analysis settings:
- Dependabot alerts
- Dependabot security updates
- Secret scanning
7. Create a Sentry project for the app if needed (we want this for most apps):
- Send initial exceptions to Sentry project for dev, stage, and prod environments to create them.
- Create an alert for the prod environment only, with notifications sent to the appropriate team(s).
- If *not* using Sentry, delete Sentry configuration from config.py and test_config.py, and remove sentry_sdk from project dependencies.

# my_app

Description of the app
# marimo-launcher

## Development

Expand All @@ -31,7 +7,7 @@ Description of the app
- To update dependencies: `make update`
- To run unit tests: `make test`
- To lint the repo: `make lint`
- To run the app: `uv run my_app --help`
- To run the app: `uv run launcher --help`

## Environment Variables

Expand All @@ -44,11 +20,7 @@ WORKSPACE=### Set to `dev` for local development, this will be set to `stage` an

### Optional

_Delete this section if it isn't applicable to the PR._

```shell
<OPTIONAL_ENV>=### Description for optional environment variable
```
None yet...



Expand Down
1 change: 1 addition & 0 deletions launcher/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""launcher package."""
7 changes: 6 additions & 1 deletion my_app/cli.py → launcher/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import click

from my_app.config import configure_logger, configure_sentry
from launcher.config import configure_logger, configure_sentry

logger = logging.getLogger(__name__)

Expand All @@ -26,3 +26,8 @@ def main(*, verbose: bool) -> None:
logger.info(
"Total time to complete process: %s", str(timedelta(seconds=elapsed_time))
)


if __name__ == "__main__":
logger = logging.getLogger("launcher.cli")
main()
2 changes: 1 addition & 1 deletion my_app/config.py → launcher/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def configure_logger(logger: logging.Logger, *, verbose: bool) -> str:
)
logger.setLevel(logging.DEBUG)
for handler in logging.root.handlers:
handler.addFilter(logging.Filter("my_app"))
handler.addFilter(logging.Filter("launcher"))
else:
logging.basicConfig(
format="%(asctime)s %(levelname)s %(name)s.%(funcName)s(): %(message)s"
Expand Down
1 change: 0 additions & 1 deletion my_app/__init__.py

This file was deleted.

5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
# https://mitlibraries.atlassian.net/wiki/spaces/IN/pages/3432415247/Python+Project+Linters#Template-for-pyproject.toml

[project]
name = "python-lambda-template"
name = "marimo-launcher"
version = "2.0.0"
requires-python = ">=3.13"

dependencies = [
"click>=8.2.1",
"marimo>=0.14.17",
"sentry-sdk>=2.34.1",
]

Expand Down Expand Up @@ -82,4 +83,4 @@ fixture-parentheses = false
max-doc-length = 90

[tool.ruff.lint.pydocstyle]
convention = "google"
convention = "google"
2 changes: 1 addition & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from my_app.cli import main
from launcher.cli import main


def test_cli_no_options(caplog, runner):
Expand Down
2 changes: 1 addition & 1 deletion tests/test_config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import logging

from my_app.config import configure_logger, configure_sentry
from launcher.config import configure_logger, configure_sentry


def test_configure_logger_not_verbose():
Expand Down
Loading