4
\$\begingroup\$

I'm still new to python, but my history is in the C/C++ world with plenty of Makefiles, though I've rarely written from scratch such elaborate ones as this. The Hitchhiker's Guide to Python gave me permission to use a Makefile to organize my Python project, so that's what I did.

I'm looking for suggestions on how to make this code more idiomatic, from the perspectives of both Makefile alone and also common usage of Makefile in Python projects. Any and all other feedback also welcome and appreciated.

Consider also reviewing the Python that this Makefile deploys.

all: deployment_test form.html # TODO: delete line when d'test depends on f'html

deployment_test: deploy deployment.url web_response.ics.masked.expected
    wget $(shell cat deployment.url)?place=flint%20hill%20va\&lookaheaddays=1 \
        --output-document=web_response.ics
    sed -e 's/DATE-TIME:.*/DATE-TIME:.../' \
        -e 's/Sextant/*/' \
        -e 's/Trine/*/' \
        web_response.ics > web_response.ics.masked
    diff -b web_response.ics.masked \
         web_response.ics.masked.expected
    echo Deployment test\(s\) passed. $(TIMESTAMP_FILE_CONTENTS) \
        > deployment_test

form.html: form.html_blank_action deployment.url
    sed -e 's!form action=\"\"!form action=\"$(shell cat deployment.url)\"!' \
        form.html_blank_action > form.html

AWS_NAME=aspectus
AWS_DESCRIPTION="aspect+prospectus. Generates iCalendars with astrological aspect events."
AWS_APIGATEWAY_DEPLOYMENT_STAGE_NAME=prod

deployment.url: configure_aws_environment
    echo https://$(shell cat aws_apigateway_api_id).execute-api.$(shell aws configure get region).amazonaws.com/$(AWS_APIGATEWAY_DEPLOYMENT_STAGE_NAME)/$(AWS_NAME)>deployment.url

deploy: deployment.zip configure_aws_environment
    aws s3 cp deployment.zip s3://$(AWS_NAME)/
    aws lambda update-function-code \
        --function-name $(AWS_NAME) \
        --s3-bucket $(AWS_NAME) \
        --s3-key deployment.zip \
        > deploy
    echo Deployment successful. $(TIMESTAMP_FILE_CONTENTS)>deploy

unit_test: *.py tests/*.py
    python -m unittest discover -s tests
    @echo Unit test\(s\) passed. $(TIMESTAMP_FILE_CONTENTS) > unit_test

deployment.zip: unit_test deployment_dependency_libraries
    rm -rf deployment
    mkdir deployment
    cp *.py deployment
    cp -R deployment_dependency_libraries/* deployment
    cd deployment ; \
        zip -qr ../deployment.zip *
    rm -rf deployment

configure_aws_environment: aws_lambda_arn \
                           aws_apigateway_api_id \
                           aws_apigateway_resource_id \
                           aws_apigateway_method \
                           aws_apigateway_api_arn
    aws apigateway put-integration \
        --rest-api-id $(shell cat aws_apigateway_api_id) \
        --resource-id $(shell cat aws_apigateway_resource_id) \
        --http-method $(shell cat aws_apigateway_method) \
        --type AWS_PROXY \
        --integration-http-method POST \
        --uri arn:aws:apigateway:$(shell aws configure get region):lambda:path/2015-03-31/functions/$(shell cat aws_lambda_arn)/invocations #\
        #--request-parameters 'method.request.queryString.place=string,method.request.queryString.lookaheaddays=string'
    aws apigateway put-method-response \
        --rest-api-id $(shell cat aws_apigateway_api_id) \
        --resource-id $(shell cat aws_apigateway_resource_id) \
        --http-method $(shell cat aws_apigateway_method) \
        --status-code 200 \
        --response-models "{}"
    aws apigateway put-integration-response \
        --rest-api-id $(shell cat aws_apigateway_api_id) \
        --resource-id $(shell cat aws_apigateway_resource_id) \
        --http-method $(shell cat aws_apigateway_method) \
        --status-code 200 \
        --selection-pattern ".*"
    aws apigateway create-deployment \
        --rest-api-id $(shell cat aws_apigateway_api_id) \
        --description $(AWS_DESCRIPTION) \
        --stage-name $(AWS_APIGATEWAY_DEPLOYMENT_STAGE_NAME)
    aws lambda add-permission \
        --function-name $(AWS_NAME) \
        --statement-id $(AWS_NAME)-all-stages \
        --action lambda:InvokeFunction \
        --principal apigateway.amazonaws.com \
        --source-arn "$(shell cat aws_apigateway_api_arn)/*/$(shell cat aws_apigateway_method)/$(AWS_NAME)"
    aws lambda add-permission \
        --function-name $(AWS_NAME) \
        --statement-id \
            apigateway-$(AWS_NAME)-stage-$(AWS_APIGATEWAY_DEPLOYMENT_STAGE_NAME) \
        --action lambda:InvokeFunction \
        --principal apigateway.amazonaws.com \
        --source-arn "$(shell cat aws_apigateway_api_arn)/$(AWS_APIGATEWAY_DEPLOYMENT_STAGE_NAME)/$(shell cat aws_apigateway_method)/$(AWS_NAME)"
    aws s3 mb s3://$(AWS_NAME)
    echo AWS environment configured. $(TIMESTAMP_FILE_CONTENTS) \
        > configure_aws_environment

aws_apigateway_api_id:
    aws apigateway create-rest-api \
        --name $(AWS_NAME) \
        --description $(AWS_DESCRIPTION) \
        --output text \
        --query "id" \
        > aws_apigateway_api_id \
        || (rm -f aws_apigateway_api_id ; false)

aws_apigateway_resource_id: aws_apigateway_parent_resource_id
    aws apigateway create-resource \
        --rest-api-id $(shell cat aws_apigateway_api_id) \
        --parent-id $(shell cat aws_apigateway_parent_resource_id) \
        --path-part $(AWS_NAME) \
        --query 'id' \
        --output text \
        > aws_apigateway_resource_id \
        || (rm -f aws_apigateway_resource_id ; false)

aws_apigateway_parent_resource_id:
    aws apigateway get-resources \
        --rest-api-id $(shell cat aws_apigateway_api_id) \
        --query "items[?path=='/'].id" \
        --output text \
        > aws_apigateway_parent_resource_id \
        || (rm -f aws_apigateway_parent_resource_id ; false)

aws_apigateway_method: aws_apigateway_api_id aws_apigateway_resource_id
    aws apigateway put-method \
        --rest-api-id $(shell cat aws_apigateway_api_id) \
        --resource-id $(shell cat aws_apigateway_resource_id) \
        --http-method GET \
        --authorization-type NONE \
        --query 'httpMethod' \
        --output text \
        > aws_apigateway_method \
        || (rm -f aws_apigateway_method ; false)

aws_apigateway_api_arn: aws_lambda_arn aws_apigateway_api_id
    sed -e 's/lambda/execute-api/' \
        -e "s/function:$(AWS_NAME)/$(shell cat aws_apigateway_api_id)/" \
        aws_lambda_arn > aws_apigateway_api_arn

aws_lambda_arn: aws_iam_role_arn 
    zip empty.zip aws_iam_role_arn
    aws lambda create-function \
        --function-name $(AWS_NAME) \
        --description $(AWS_DESCRIPTION) \
        --runtime python2.7 \
        --role $(shell cat aws_iam_role_arn) \
        --handler $(AWS_NAME).lambda_handler \
        --zip-file fileb://empty.zip \
        --query 'FunctionArn' \
        --output text \
        > aws_lambda_arn \
        || (rm -f empty.zip aws_lambda_arn ; false)
    rm -f empty.zip

aws_iam_role_arn: aws_iam_assume_role_policy.json
ifeq "$(wildcard aws_iam_role_arn)" ""
    $(info file aws_iam_role_arn does not exist. creating.)
    aws iam create-role \
        --role-name $(AWS_NAME) \
        --assume-role-policy-document file://aws_iam_assume_role_policy.json \
        --path /service-role/ \
        --output text \
        --query "Role.Arn" \
        > aws_iam_role_arn \
        || (rm -f aws_iam_role_arn ; false)
    aws iam attach-role-policy \
        --role-name $(AWS_NAME) \
        --policy-arn \
            arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole \
        || (rm aws_iam_role_arn ; false)
    sleep 10 # to let it sink in. w/out this, lambda create-function complains
else
    $(info file aws_iam_role_arn already exists. updating existing arn.)
    aws iam update-assume-role-policy \
        --role-name $(AWS_NAME) \
        --policy-document file://aws_iam_assume_role_policy.json
endif

deployment_dependency_libraries: requirements.txt
    $(info may fail if pip compiles binaries for non-x86_64 Linux)
    rm -rf ./deployment_dependency_libraries
    pip install \
        --requirement requirements.txt \
        --target ./deployment_dependency_libraries \
        --compile

clean: clean_aws_environment
    rm -rf *.pyc \
           web_response.ics \
           web_response.ics.masked \
           unit_test
    rm -rf deployment_test \
           form.html \
           deployment.url \
           deploy
           deployment.zip \
           deployment_dependency_libraries

clean_aws_environment:
ifneq "$(wildcard aws_apigateway_api_id)" ""
    aws apigateway delete-rest-api \
       --rest-api-id $(shell cat aws_apigateway_api_id)
    rm -f aws_apigateway_api_id
endif
    $(info if there are multiple apigateways to delete, or if the created API ID does not get captured in the file aws_apigateway_api_id, this can take forever...)
    $(info without these crazy sleeps, we will get TooManyRequestsException)
    for i in `aws apigateway get-rest-apis \
                  --output text \
                  --query "items[?name=='$(AWS_NAME)'].id"` ; do \
        aws apigateway delete-rest-api --rest-api-id $$i ; \
        sleep 30 ; done
    rm -f aws_apigateway_api_arn \
          aws_apigateway_method \
          aws_apigateway_parent_resource_id \
          aws_apigateway_resource_id \
          configure_aws_environment \
          aws_iam_role_arn \
          aws_lambda_arn
    if aws iam list-roles | grep -q $(AWS_NAME) ; then \
        aws iam detach-role-policy \
            --role-name $(AWS_NAME) \
            --policy-arn \
                arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole \
            || false ; \
        aws iam delete-role --role-name $(AWS_NAME) || false ; fi
    if aws lambda list-functions | grep -q $(AWS_NAME) ; then \
        aws lambda delete-function --function-name $(AWS_NAME) || false ; fi
    if aws s3 ls | grep -q $(AWS_NAME) ; then \
        aws s3 rm --recursive s3://$(AWS_NAME)/ --include \* || false ; \
        aws s3 rb s3://$(AWS_NAME) || false ; fi

TIMESTAMP_FILE_CONTENTS=This file is a placeholder whose only significance is \
in its timestamp. Its purpose is to indicate which Makefile targets need to \
be executed. Changing the timestamp of this file \(such as by \
updating/writing to it\) will likely break Makefile funcionality.

# Backlog of improvements to this file:
#
# As a maintainer, I want a Selenium WebDriver deployment test, which uses (and
# whose target target depends) on both the Selenium script and form.html, in
# order to integrate the form with the delivery & test pipeline.  (Then remove
# the all: target, and make the deployment_test: target be first in this file.)
#
# As a maintainer, I want Travis CI to do continuous unit testing triggered by
# every commit to GitHub, with a status badge in REAME.md, so that the
# fundamental functionality of the code is always known to be in a usable state.
#
# As a maintainer, I want my AWS credentials securely conveyed to Travis CI, so
# that every commit can also enable continuous deployment in addition to
# continuous unit testing.  (Likely define encrypted variables in .travis.yml.
# See:
# * https://docs.travis-ci.com/user/environment-variables/#Defining-encrypted-variables-in-.travis.yml
# * http://docs.aws.amazon.com/cli/latest/topic/config-vars.html)
#
# As a maintainer, I want the Selenium deployment scripts to be automatically
# run with each commit to GitHub, in order to enable continuous deployment
# testing.  (Use Sauce Labs as in
# https://docs.travis-ci.com/user/gui-and-headless-browsers/.)
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.