Skip to content
This repository was archived by the owner on Nov 16, 2023. It is now read-only.
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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__pycache__/
Original file line number Diff line number Diff line change
@@ -1,24 +1,37 @@
ARG PYTHON_VERSION=3.7.0-alpine3.8
ARG PYTHON_VERSION=3.7

FROM python:${PYTHON_VERSION} as builder
FROM python:${PYTHON_VERSION} AS builder

WORKDIR /usr/src/{{cookiecutter.PROJECT_NAME_SLUG}}
WORKDIR /src

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
RUN pip wheel --no-cache-dir -r requirements.txt -w /deps

COPY requirements-dev.txt .
RUN pip install --no-cache-dir -r requirements-dev.txt

COPY . .

# Run tests, linter
RUN pytest testapp.py \
RUN flake8 *.py \
&& isort --check-only *.py \
&& flake8 *.py
&& pydocstyle *.py \
&& python -m doctest *.py

FROM python:${PYTHON_VERSION}-slim AS runtime

WORKDIR /{{cookiecutter.PROJECT_NAME_SLUG}}/deps
COPY --from=builder /deps .
RUN pip install --no-cache-dir *.whl

WORKDIR /{{cookiecutter.PROJECT_NAME_SLUG}}/src
COPY --from=builder /src .

# Release
FROM python:${PYTHON_VERSION}
ENV HOST=0.0.0.0
ENV PORT=80
ENV WORKERS=4
ENV LOG_LEVEL=info

WORKDIR /{{cookiecutter.PROJECT_NAME_SLUG}}
COPY --from=builder /usr/src/{{cookiecutter.PROJECT_NAME_SLUG}} /{{cookiecutter.PROJECT_NAME_SLUG}}
RUN pip install -r requirements.txt
EXPOSE $PORT

CMD ["python", "main.py"]
CMD gunicorn --log-level="$LOG_LEVEL" --bind="$HOST:$PORT" --workers="$WORKERS" "main:build_app(wsgi=True)"
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
swagger: '2.0'

info:
title: Transformation API
version: '1.0'

basePath: '/'

paths:
'/':
post:
summary: Transform the data.
operationId: main.main
consumes:
- application/json
parameters:
- $ref: '#/parameters/Data'
responses:
200:
description: The transformed data.

parameters:
Data:
name: data
description: The data to transform.
in: body
schema:
$ref: '#/definitions/Data'
required: true

definitions:
{{DATA_SCHEMA}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Data:
properties:
key:
description: The key of the input.
type: string
intValue:
description: The value of the input.
type: number
required:
- key
- intValue

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""
Implements the business logic for {{cookiecutter.PROJECT_NAME_SLUG}}.

Overwrite the sample implementation in this file with your own requirements.
The framework expects a function named `transform` to be present in this
module but helper functions may be added at your convenience.
"""


def transform(data: dict) -> dict:
"""
Apply the required transformations to the input data.

The input data for this function is guaranteed to follow the specification
provided to the framework. Consult `data.spec.yaml` for an example.

Args:
data: input object to transform

>>> transform({'intValue': 100})
{'intValue': 200}
"""
data['intValue'] += 100
return data
Original file line number Diff line number Diff line change
@@ -1,94 +1,77 @@
""" Entrypoint for customer application. Listens for HTTP requests from
the input reader, and sends the transformed message to the output writer. """
import json
import logging
import os
from http.server import BaseHTTPRequestHandler
from http.server import HTTPServer

import requests

import datahelper

# HOST & PORT are the values used to run the current application
HOST = os.getenv("HOST")
PORT = os.getenv("PORT")

# OUTPUT_URL is the url which receives all the output messages after they are processed by the app
OUTPUT_URL = os.getenv("OUTPUT_URL")

# Filepath for the JSON schema which represents
# the schema for the expected input messages to the app
SCHEMA_FILEPATH = os.getenv("SCHEMA_FILEPATH")


class Socket(BaseHTTPRequestHandler):
"""Handles HTTP requests that come to the server."""

def _set_headers(self):
"""Sets common headers when returning an OK HTTPStatus. """
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()

def do_POST(self):
"""Handles a POST request to the server.
Sends 400 error if there is an issue, otherwise sends a success message.

Raises:
ValidationError: Returns when data given is not valid against schema
HTTPError: Returns when there is an error sending message to output url

"""
content_length = int(self.headers['Content-Length'])
data = self.rfile.read(content_length)
data = data.decode("utf-8")

try:
datahelper.validate_schema(data, SCHEMA_FILEPATH)
except BaseException:
self.send_error(
400, 'Incorrect data format. Please check JSON schema.')
logging.error('Incorrect data format. Please check JSON schema.')
raise

try:
transformed_data = datahelper.transform(data)
output_message(transformed_data)
self._set_headers()
self.wfile.write(bytes("Data successfully consumed", 'utf8'))
except BaseException:
self.send_error(400, 'Error when sending output message')
logging.error('Error when sending output message')
raise


def output_message(data: object):
"""Outputs the transformed payload to the specified HTTP endpoint

Args:
data: transformed json object to send to output writer
"""
request = requests.post(OUTPUT_URL, data=json.dumps(data))
if request.status_code != 200:

logging.error("Error with a request %s and message not sent was %s",
request.status_code, data)
else:
logging.info("%s Response received from output writer",
request.status_code)
"""
Entrypoint for customer application.

Listens for HTTP(S) requests from the input reader, transforms the message
and sends the transformed message to the output writer.

You can view a specification for the API at /swagger.json as well as a testing
console at /ui/.

All business logic for {{cookiecutter.PROJECT_NAME_SLUG}} should be included
in `logic.py` and it is unlikely that changes to this `main.py` file will be
required.
"""
from logging import getLogger

from requests import post

import settings
from logic import transform

LOG = getLogger(__name__)


def main(data: dict):
"""Transform the input and forward the transformed payload via HTTP(S)."""
data = transform(data)

if not settings.OUTPUT_URL:
LOG.warning('Not OUTPUT_URL specified, not forwarding data.')
return data, 500

response = post(settings.OUTPUT_URL, json=data)
if not response.ok:
LOG.error('Error %d with post to url %s and body %s.',
response.status_code, settings.OUTPUT_URL, data)
return data, response.status_code

LOG.info('Response %d received from output writer.', response.status_code)
return data, 200

def run(server_class=HTTPServer, handler_class=Socket):
"""Run the server on specified host and port, using our
custom Socket class to receive and process requests.
"""

server_address = (HOST, int(PORT))
httpd = server_class(server_address, handler_class)
logging.info('Running server on host ${HOST} and port ${PORT}')
httpd.serve_forever()
def build_app(wsgi=False):
"""Create the web server."""
from connexion import App

with open(settings.DATA_SCHEMA, encoding='utf-8') as fobj:
data_schema = '\n'.join(' {}'.format(line.rstrip('\r\n'))
for line in fobj)

LOG.setLevel(settings.LOG_LEVEL)

app = App(__name__)
app.add_api('api.spec.yaml', arguments={'DATA_SCHEMA': data_schema})

return app.app if wsgi else app


def cli():
"""Command line interface for the web server."""
from argparse import ArgumentParser

from yaml import YAMLError

parser = ArgumentParser(description=__doc__)
parser.parse_args()

try:
app = build_app()
except YAMLError as ex:
parser.error('Unable to parse {} as a Swagger spec.\n\n{}'
.format(settings.DATA_SCHEMA, ex))
else:
app.run(port=settings.PORT, host=settings.HOST)


if __name__ == "__main__":
run()
cli()
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
hypothesis
python-dotenv
jsonschema
pytest
flake8
isort
requests
pydocstyle
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
python-dotenv
jsonschema
connexion[swagger-ui]
environs
gunicorn
requests

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""
Module to hold all application configuration.

All environment variable access hould happen in this module only.
"""
from environs import Env

env = Env()
env.read_env()

PORT = env.int('PORT', 8888)
HOST = env('HOST', '127.0.0.1')
DATA_SCHEMA = env('SCHEMA_FILEPATH', 'data.spec.yaml')
OUTPUT_URL = env('OUTPUT_URL', '')
LOG_LEVEL = env('LOG_LEVEL', 'INFO').upper()
Loading