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
19 changes: 16 additions & 3 deletions copier.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,23 @@ author_email:
type: str
help: Provide the author's email address

docker:
project_type:
type: str
help: Select the type of project
choices:
Empty: empty
Docker: dockerized
AWS Lambda: lambda

is_docker_project:
type: bool
default: "{{ project_type == 'dockerized' }}"
when: false

is_lambda_project:
type: bool
default: true
help: Enable Docker support
default: "{{ project_type == 'lambda' }}"
when: false

tasks:
# Make entrypoint executable if it exists
Expand Down
5 changes: 5 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ TEST_CATALOG := "test_catalog"
DEFAULT_OUTPUT := TEST_CATALOG + "/generated_project"
DOCKER_OUTPUT := TEST_CATALOG + "/dockerized_project"
PLAIN_OUTPUT := TEST_CATALOG + "/plain_project"
LAMBDA_OUTPUT := TEST_CATALOG + "/lambda_project"

TEST_ANSWERS := "test_answers"
DOCKER_ANSWERS := TEST_ANSWERS + "/docker.yml"
PLAIN_ANSWERS := TEST_ANSWERS + "/plain.yml"
LAMBDA_ANSWERS := TEST_ANSWERS + "/lambda.yml"

PATHS_TO_LINT := "tests"

Expand Down Expand Up @@ -42,10 +44,12 @@ output_all_ff output=DEFAULT_OUTPUT:
[group("development")]
[doc("Run all checks and tests (lints, mypy, tests...)")]
all: lint_full test generation_check generation_check_docker
just generation_check {{ LAMBDA_ANSWERS }} {{ LAMBDA_OUTPUT }}

[group("development")]
[doc("Run all checks and tests, but fail on first that returns error (lints, mypy, tests...)")]
all_ff: lint_full_ff test generation_check generation_check_docker
just generation_check {{ LAMBDA_ANSWERS }} {{ LAMBDA_OUTPUT }}

[group("development")]
[doc("Runs template tests.")]
Expand Down Expand Up @@ -105,3 +109,4 @@ generation_check data_file=PLAIN_ANSWERS output=PLAIN_OUTPUT:
copy_all:
just copy {{ DOCKER_ANSWERS }} {{ DOCKER_OUTPUT }}
just copy {{ PLAIN_ANSWERS }} {{ PLAIN_OUTPUT }}
just copy {{ LAMBDA_ANSWERS }} {{ LAMBDA_OUTPUT }}
2 changes: 1 addition & 1 deletion skeleton/.github/workflows/ci.yml.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:

- name: "Install just"
uses: extractions/setup-just@v3
{%- if docker %}
{%- if is_docker_project %}

- name: "Run linters and tests"
run: just dc all
Expand Down
33 changes: 29 additions & 4 deletions skeleton/README.md.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

{{ description }}

{%- if docker %}
{%- if is_docker_project %}
## Development with Docker

1. Build docker image:
```
docker compose build {{ project_name_snake_case }}
Expand All @@ -16,9 +18,32 @@
```
just dc bash
```
{%- endif %}

3. Run tests:
```
just test
### Running Tests

```bash
just test
```

### Code Style

The project uses Ruff for linting and formatting, all linters can be run with `just`:

```bash
just lint_full
just lint_full_ff # (fast-fail mode)
just all # (lint + tests)
just all_ff # (lint + tests in fast-fail mode)
```

{%- if is_lambda_project %}
## Deployment to AWS Lambda
1. Package the application:
```bash
just build
```

2. You will see in `/build` directory a zip file ready for deployment to AWS Lambda.
{%- endif %}

61 changes: 41 additions & 20 deletions skeleton/justfile.jinja
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
set dotenv-load

{% if docker -%}
CONTAINER_NAME := "{{ project_name_snake_case }}"
{% endif -%}
PYTHONPATH := "./{{ project_name_snake_case }}"
{%- if docker %}
WORKDIR := "/code"
{%- endif %}
PATHS_TO_LINT := "{{ project_name_snake_case }} tests"
TEST_PATH := "tests"
ANSWERS_FILE := ".copier/.copier-answers.copier-python-project.yml"

{% raw -%}
{%- if is_docker_project %}
CONTAINER_NAME := "{{ project_name_snake_case }}"
WORKDIR := "/code"
{%- endif %}
{%- if is_lambda_project %}
BUILD_PATH := "build"
CURRENT_DATETIME := `date +'%Y%m%d_%H%M%S'`
PYTHON_VERSION := "3.13"
{%- endif %}
{%- raw %}
[doc("Command run when 'just' is called without any arguments")]
default: help

Expand All @@ -27,9 +29,14 @@ all: lint_full test
[doc("Run all checks and tests, but fail on first that returns error (lints, mypy, tests...)")]
all_ff: lint_full_ff test
{%- endraw -%}
{%- if docker -%}
{%- if is_docker_project -%}
{%- raw %}

[group("development")]
[doc("Run any command inside docker (e.g. just dc bash)")]
dc command *args:
docker compose run --rm -w {{WORKDIR}} {{CONTAINER_NAME}} just {{command}} {{args}}

[group("development")]
[doc("Open bash console (useful when prefixed with dc, as it opens bash inside docker)")]
@bash:
Expand All @@ -49,17 +56,6 @@ ruff:
copier_update answers=ANSWERS_FILE skip-answered="true":
uv run copier update --answers-file {{answers}} \
{{ if skip-answered == "true" { "--skip-answered" } else { "" } }}
{%- endraw -%}
{%- if docker -%}
{%- raw %}

[group("development")]
[doc("Run any command inside docker (e.g. just dc bash)")]
dc command *args:
docker compose run --rm -w {{WORKDIR}} {{CONTAINER_NAME}} just {{command}} {{args}}
{%- endraw -%}
{%- endif -%}
{%- raw %}

[group("lint")]
[doc("Run fawltydeps lint check (deopendency issues)")]
Expand Down Expand Up @@ -112,3 +108,28 @@ _set_pythonpath path=PYTHONPATH:
test file=TEST_PATH: _set_pythonpath
uv run pytest {{file}} --durations=10
{% endraw -%}
{%- if is_lambda_project -%}
{%- raw %}
[group("lambda_build")]
build: build_create_env
cd {{ BUILD_PATH }}/pkg && \
zip -r ../{% endraw %}{{ project_name_snake_case }}{% raw %}_{{ CURRENT_DATETIME }}.zip .

[group("lambda_build")]
build_create_env: build_generate_requirements build_install

[group("lambda_build")]
build_generate_requirements:
uv export --frozen --no-dev --no-editable -o {{ BUILD_PATH }}/requirements.txt

[group("lambda_build")]
build_install:
uv pip install \
--no-installer-metadata \
--no-compile-bytecode \
--python-platform x86_64-manylinux2014 \
--python {{ PYTHON_VERSION }} \
--target {{ BUILD_PATH }}/pkg \
-r {{ BUILD_PATH }}/requirements.txt
{% endraw -%}
{%- endif -%}
41 changes: 22 additions & 19 deletions skeleton/pyproject.toml.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,36 @@ description = "{{ description }}"
authors = [{ name = "{{ author_name }}", email = "{{ author_email }}" }]
requires-python = ">=3.13"
readme = "README.md"
dependencies = []
dependencies = [
{%- if is_lambda_project %}
"aws-lambda-powertools>=3.16.0,<4",
{%- endif %}
]

[dependency-groups]
dev = [
"copier>=9.11.3,<10",
{%- if docker %}
"dotenv-linter>=0.7.0,<1",
{%- endif %}
"fawltydeps>=0.20.0,<1.0.0",
"ipython>=9.9.0,<10",
"mypy>=1.19.1,<2",
"pytest-asyncio>=1.3.0,<2",
"pytest-cov>=7.0.0,<8",
{%- if docker %}
"pytest-env>=1.2.0,<2",
{%- endif %}
"pytest-mock>=3.15.1,<4",
"pytest>=9.0.2,<10",
{%- if docker %}
"ruff>=0.14.14,<0.15.0",
{%- if is_docker_project %}
"dotenv-linter>=0.7.0,<1",
"pytest-env>=1.2.0,<2",
"python-dotenv>=1.2.1,<2",
{%- endif %}
"ruff>=0.14.14,<0.15.0",
]

{%- if is_lambda_project %}
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
{%- endif %}

[tool.uv]
package = false

Expand Down Expand Up @@ -63,6 +69,7 @@ mark-parentheses = true

[tool.ruff.lint.isort]
order-by-type = false
known-first-party = ["{{ project_name_snake_case }}"]

[tool.coverage.report]
exclude_lines = [
Expand All @@ -76,23 +83,19 @@ deps = ["pyproject.toml"]
ignore_unused = [
# Dev dependencies. This list should contain ALL of them!
"copier",
{%- if docker %}
"dotenv-linter",
{%- endif %}
"fawltydeps",
"ipython",
"mypy",
"pytest-asyncio",
"pytest-cov",
{%- if docker %}
"pytest-env",
{%- endif %}
"pytest-mock",
{%- if docker %}
"python-dotenv",
{%- endif %}
"pytest",
"ruff",
{%- if is_docker_project %}
"dotenv-linter",
"pytest-env",
"python-dotenv",
{%- endif %}
]

[tool.mypy]
Expand All @@ -105,7 +108,7 @@ disable_error_code = "misc"
python_files = ["tests.py", "test_*.py", "*_tests.py"]
addopts = "--strict-markers -p no:warnings --cov=. --cov-fail-under=90 --cov-config=.coveragerc"
asyncio_mode = "auto"
{%- if docker %}
{%- if is_docker_project %}

[tool.pytest_env]
FLAVOR = "test"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from dataclasses import dataclass
from typing import Any

import pytest


@dataclass
class LambdaContext:
function_name: str = "test"
memory_limit_in_mb: int = 128
invoked_function_arn: str = "arn:aws:lambda:eu-west-1:809313241:function:test"
aws_request_id: str = "52fdfc07-2182-154f-163f-5f0f9a621d72"


@pytest.fixture()
def lambda_context() -> LambdaContext:
return LambdaContext()


@pytest.fixture()
def api_gw_event() -> dict[str, Any]:
return {
"headers": {},
"body": "",
"requestContext": {
"http": {
"method": "GET",
"path": "/some_id/some_path",
"protocol": "HTTP/1.1",
"sourceIp": "192.168.0.1/32",
"userAgent": "agent",
},
"stage": "$default",
},
"resource": "/{some_id}/some_path",
"rawPath": "/some_id/some_path",
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from typing import Any

from aws_lambda_powertools.utilities.typing import LambdaContext

from {{ project_name_snake_case }}.lambda_function import lambda_handler


def test_lambda_handler(
api_gw_event: dict[str, Any],
lambda_context: LambdaContext,
) -> None:
response = lambda_handler(event=api_gw_event, context=lambda_context)
assert response["statusCode"] == 200
assert response["body"] == "hello world"
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from typing import Any

from aws_lambda_powertools.event_handler import APIGatewayHttpResolver, Response
from aws_lambda_powertools.utilities.typing import LambdaContext

app = APIGatewayHttpResolver()


@app.get("/<some_id>/some_path")
def main(some_id: str) -> Response[str]:
return Response(
status_code=200,
content_type="text/plain; charset=utf-8",
body="hello world",
)


def lambda_handler(event: dict[str, Any], context: LambdaContext) -> dict:
"""
Parameters
----------
event: dict, required
API Gateway Lambda Proxy Input Format

Event doc:
https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format

context: LambdaContext, required
Lambda Context runtime methods and attributes

Context doc: https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html

Returns
------
API Gateway Lambda Proxy Output Format: dict

Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html
"""
return app.resolve(event, context)
2 changes: 1 addition & 1 deletion test_answers/docker.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
author_email: author@email.test
author_name: test-author
description: Service description.
docker: true
project_name: test-service
project_type: dockerized
Loading