Skip to content
Open
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
4 changes: 4 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,7 @@ logs/
# Temporary files
tmp/
temp/

# Docker
docker/Dockerfile*
docker/docker-compose.yml
2 changes: 2 additions & 0 deletions .env-example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
FLASK_DEBUG=1
CACHE_REDIS_HOST=redis
5 changes: 4 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,8 @@
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter"
},
"python.formatting.provider": "none"
"python.formatting.provider": "none",
"python-envs.defaultEnvManager": "ms-python.python:conda",
"python-envs.defaultPackageManager": "ms-python.python:conda",
"python-envs.pythonProjects": []
}
71 changes: 61 additions & 10 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
install:
.PHONY: help
help: ## Print this message
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-24s\033[0m %s\n", $$1, $$2}'

install: ## Install Python dependencies
pip install -U -e .[dev]

debug:
FLASK_APP=policyengine_household_api.api FLASK_DEBUG=1 flask run --without-threads
debug: ## Run Flask app with FLASK_DEBUG=1
FLASK_APP=policyengine_household_api.api FLASK_DEBUG=1 flask run --without-threads --host=0.0.0.0

test:
test: ## Run unit tests
pytest -vv --timeout=150 -rP tests/to_refactor tests/unit

test-with-auth:
test-with-auth: ## Run integration tests
CONFIG_FILE=config/test_with_auth.yaml pytest -vv --timeout=150 -rP tests/integration_with_auth

debug-test:
debug-test: ## Run tests with FLASK_DEBUG=1
FLASK_DEBUG=1 pytest -vv --durations=0 --timeout=150 -rP tests

format:
format: ## Run black
black . -l 79

deploy:
deploy: ## Deploy to GCP
python gcp/export.py
gcloud config set app/cloud_build_timeout 1800
cp gcp/policyengine_household_api/* .
Expand All @@ -25,9 +29,56 @@ deploy:
rm Dockerfile
rm .gac.json

changelog:
changelog: ## Build changelog
build-changelog changelog.yaml --output changelog.yaml --update-last-date --start-from 0.1.0 --append-file changelog_entry.yaml
build-changelog changelog.yaml --org PolicyEngine --repo policyengine-household-api --output CHANGELOG.md --template .github/changelog_template.md
bump-version changelog.yaml setup.py policyengine_household_api/constants.py
rm changelog_entry.yaml || true
touch changelog_entry.yaml
touch changelog_entry.yaml

COMPOSE_FILE ?= docker/docker-compose.yml
COMPOSE_EXTERNAL_FILE ?= docker/docker-compose.external.yml
DOCKER_IMG ?= policyengine:policyengine-household-api
DOCKER_NAME ?= policyengine-household-api
ifeq (, $(shell which docker))
DOCKER_CONTAINER_ID := docker-is-not-installed
else
DOCKER_CONTAINER_ID := $(shell docker ps --filter ancestor=$(DOCKER_IMG) --format "{{.ID}}")
endif
DOCKER_NETWORK ?= policyengine-api_default
DOCKER_CONSOLE ?= policyengine-api-console

.PHONY: docker-build
docker-build: ## Build the docker image
docker compose --file $(COMPOSE_FILE) build --force-rm

.PHONY: docker-run
docker-run: ## Run the app as docker container with supporting services
docker compose --file $(COMPOSE_FILE) up

.PHONY: docker-run-external
docker-run-external: ## Run with external network (for multi-service setups)
docker compose --file $(COMPOSE_FILE) --file $(COMPOSE_EXTERNAL_FILE) up

.PHONY: services-start
services-start: ## Run the docker containers for supporting services (e.g. Redis)
docker compose --file $(COMPOSE_FILE) up -d redis

.PHONY: services-start-external
services-start-external: ## Start services with external network
docker compose --file $(COMPOSE_FILE) --file $(COMPOSE_EXTERNAL_FILE) up -d redis

.PHONY: services-stop
services-stop: ## Stop the docker containers for supporting services
docker compose --file $(COMPOSE_FILE) down

.PHONY: docker-network-create
docker-network-create: ## Create the external Docker network (for multi-service setups)
docker network create $(DOCKER_NETWORK) || true

.PHONY: docker-console
docker-console: ## Open a one-off container bash session
@docker run -p 8080:5000 -v $(PWD):/code \
--network $(DOCKER_NETWORK) \
--rm --name $(DOCKER_CONSOLE) -it \
$(DOCKER_IMG) bash
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,54 @@

A version of the PolicyEngine API that runs the `calculate` endpoint over household object. To debug locally, run `make debug`.

## Local development with Docker Compose

To run this app locally via Docker Compose:

```
% make docker-build
% make docker-run
```

and point your browser at http://localhost:8080 to access the API.

To develop the code locally, you will want to instead start only the Redis docker container and a one-off
API container, with your local filesystem mounted into the running docker container.

```
% make services-start
% make docker-console
```

Then inside the container, start the Flask service:

```
policyapi@[your-docker-id]:/code$ make debug
```

and point your browser at http://localhost:8080 to access the API.

### Running with other PolicyEngine services

If you're running this alongside other PolicyEngine services (e.g., the main API) and need
containers to communicate across projects, use the external network mode:

```
% make docker-network-create # Create shared network (once)
% make docker-run-external # Run with external network
```

This connects the household API to a shared `policyengine-api_default` network that other
PolicyEngine docker-compose projects can also join.

For development with external networking:

```
% make docker-network-create
% make services-start-external
% make docker-console
```

## Development rules

1. Every endpoint should return a JSON object with at least a "status" and "message" field.
Expand Down
6 changes: 6 additions & 0 deletions changelog_entry.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
- bump: minor
changes:
added:
- Docker Compose support for local development with Redis
- Makefile targets for building and running Docker containers
- Support for external Docker networks when running alongside other PolicyEngine services
10 changes: 10 additions & 0 deletions config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,16 @@ services:
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
```

Or, alternately, use the `docker/docker-compose.yml` file and create a `.env` file in your root directory
to set all environment variables. Example:

```
% cat .env
CONFIG_FILE=/code/config/custom.yaml
DATABASE__PASSWORD=your-secret-password
ANTHROPIC_API_KEY=your-key-here
```

#### Kubernetes ConfigMap
```yaml
apiVersion: v1
Expand Down
26 changes: 26 additions & 0 deletions docker/Dockerfile.api
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
FROM python:3.12

# use root to install pip libs
USER root

WORKDIR /code

COPY . /code/

ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

# can't use make install because we ignore .github/ via .dockerignore
# and env is set via docker-compose.yml or injection at container run time
RUN pip install -e ".[dev]" --config-settings editable_mode=compat

# switch to app user
RUN groupadd policyapi && \
useradd -g policyapi policyapi && \
apt-get purge -y --auto-remove build-essential && \
apt-get -y install make && \
chown -R policyapi:policyapi /code

RUN chown -R policyapi:policyapi /usr/local/lib/python3.12/site-packages/policyengine*

USER policyapi
17 changes: 17 additions & 0 deletions docker/docker-compose.external.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Override file for connecting to an external Docker network.
# Use this when running alongside other PolicyEngine services.
#
# Usage:
# make docker-run-external
#
# Or manually:
# docker compose -f docker/docker-compose.yml -f docker/docker-compose.external.yml up
#
# Prerequisites:
# The external network must exist. Create it with:
# docker network create policyengine-api_default

networks:
my_network:
name: ${DOCKER_NETWORK:-policyengine-api_default}
external: true
33 changes: 33 additions & 0 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
services:
redis:
image: "redis:alpine"
expose:
- "6379"
volumes:
- redis-data:/data
networks:
- my_network

policyengine:
build:
context: ../
dockerfile: docker/Dockerfile.api
command: ["/bin/bash", "/code/docker/start.sh"]
image: policyengine:policyengine-household-api
depends_on:
- redis
env_file:
- ../.env
expose:
- 8080
ports:
- "8080:8080"
networks:
- my_network

volumes:
redis-data:

networks:
my_network:
name: ${DOCKER_NETWORK:-policyengine-api_default}
13 changes: 13 additions & 0 deletions docker/start.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/bash
# Environment variables
PORT="${PORT:-8080}"
WORKER_COUNT="${WORKER_COUNT:-3}"
REDIS_PORT="${REDIS_PORT:-6379}"

# Start the API
gunicorn -b :"$PORT" policyengine_household_api.api --timeout 300 --workers 5 --preload &

# Keep the script running and handle shutdown gracefully
trap "pkill -P $$; exit 1" INT TERM

wait
Loading