Skip to content
This repository has been archived by the owner on Feb 9, 2024. It is now read-only.

Bronze #1

Merged
merged 10 commits into from
Nov 18, 2017
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
*.pyc
/.coverage
/config.py
/venv
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ python:
- "3.5"

install:
- cp tk/default_config.py config.py
- ./bin/build-dev

script:
Expand Down
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@

[![Build Status](https://travis-ci.org/bartfeenstra/tk.svg?branch=master)](https://travis-ci.org/bartfeenstra/tk) [![Coverage Status](https://coveralls.io/repos/github/bartfeenstra/tk/badge.svg?branch=master)](https://coveralls.io/github/bartfeenstra/tk?branch=master)

## Usage
Substitute `http://127.0.0.1:5000` for the actual application URL, if
you are not using `./bin/run-dev`.

### Installation
Copy `./tk/default_config.py` to `./config.py`, and override any of the
default configuration as necessary.

### Submitting a document
`curl -X POST --header "Content-Type: application/octet-stream" --data-binary @{file_path} http://127.0.0.1:5000/submit`
where `{file_path}` is file path of the document to process.

### Retrieving a document's profile
`curl -X GET --header "Accept: text/xml" http://127.0.0.1:5000/retrieve/{uuid}`
where `{uuid}` is the process UUID returned by `POST /submit`.

## Development

### Building the code
Expand All @@ -14,7 +30,8 @@ Run `./bin/test`.
Run `./bin/fix` to fix what can be fixed automatically.

### Running the application
Run `./bin/run-dev` to start a development web server.
Run `./bin/run-dev` to start a development web server at
[http://127.0.0.1:5000](http://127.0.0.1:5000).

### Code style
All code follows [PEP 8](https://www.python.org/dev/peps/pep-0008/).
2 changes: 1 addition & 1 deletion bin/run-dev
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

(
cd `dirname "$0"`/.. &&
FLASK_APP=./tk/flask/entry_point.py python -m flask run
FLASK_APP=./tk/flask/entry_point.py TK_CONFIG_FILE=`readlink -f ./config.py` python -m flask run
)


2 changes: 1 addition & 1 deletion bin/test
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
(
cd `dirname "$0"`/..
flake8 --ignore=E501 ./tk
coverage run -m nose2 &&
TK_CONFIG_FILE=`readlink -f ./config.py` coverage run -m nose2 &&
coverage report -m
)
8 changes: 5 additions & 3 deletions requirements-dev-frozen.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,19 @@ click==6.7
coverage==4.4.2
coveralls==1.2.0
docopt==0.6.2
flake8==3.3.0
flake8==3.5.0
Flask==0.12.2
idna==2.6
itsdangerous==0.24
Jinja2==2.10
MarkupSafe==1.0
mccabe==0.6.1
nose2==0.6.5
nose2==0.7.2
pycodestyle==2.3.1
pyflakes==1.5.0
pyflakes==1.6.0
requests==2.18.4
requests-futures==0.9.7
requests-mock==1.3.0
six==1.11.0
urllib3==1.22
Werkzeug==0.12.2
7 changes: 4 additions & 3 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
-r ./requirements.txt
autopep8 ~= 1.3
coverage ~= 4.4.1
coverage ~= 4.4.2
coveralls ~= 1.2.0
flake8 ~= 3.3.0
nose2 ~= 0.6.5
flake8 ~= 3.5.0
nose2 ~= 0.7.2
requests-mock ~= 1.3.0
6 changes: 6 additions & 0 deletions requirements-frozen.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
certifi==2017.11.5
chardet==3.0.4
click==6.7
Flask==0.12.2
idna==2.6
itsdangerous==0.24
Jinja2==2.10
MarkupSafe==1.0
requests==2.18.4
requests-futures==0.9.7
urllib3==1.22
Werkzeug==0.12.2
4 changes: 3 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
flask ~= 0.12.2
flask ~= 0.12.2
requests ~= 2.18.4
requests-futures ~= 0.9.7
4 changes: 4 additions & 0 deletions tk/default_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
SOURCEBOX_URL = 'https://staging.textkernel.nl/sourcebox/extract.do'
SOURCEBOX_ACCOUNT_NAME = None
SOURCEBOX_USER_NAME = None
SOURCEBOX_PASSWORD = None
75 changes: 73 additions & 2 deletions tk/flask/app.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,76 @@
from flask import Flask
from flask import Flask, request, Response
from requests_futures.sessions import FuturesSession
from werkzeug.exceptions import NotAcceptable, UnsupportedMediaType, NotFound, \
BadRequest

from tk.process import Process


def request_content_type(content_type):
"""
Check we can accept the request body.
:param content_type:
:return:
"""

def decorator(route_method):
def checker(*route_method_args, **route_method_kwargs):
if request.mimetype != content_type:
raise UnsupportedMediaType()
return route_method(*route_method_args, **route_method_kwargs)

return checker

return decorator


def response_content_type(content_type):
"""
Check we can deliver the right content type.
:return:
"""

def decorator(route_method):
def checker(*route_method_args, **route_method_kwargs):
negotiated_content_type = request.accept_mimetypes.best_match(
[content_type])
if negotiated_content_type is None:
raise NotAcceptable()
return route_method(*route_method_args, **route_method_kwargs)

return checker

return decorator


class App(Flask):
pass
def __init__(self, *args, **kwargs):
super().__init__('tk', static_folder=None, *args, **kwargs)
self.config.from_object('tk.default_config')
self.config.from_envvar('TK_CONFIG_FILE')
self._register_routes()
self._session = FuturesSession()
self._process = Process(self._session, self.config['SOURCEBOX_URL'],
self.config['SOURCEBOX_ACCOUNT_NAME'],
self.config['SOURCEBOX_USER_NAME'],
self.config['SOURCEBOX_PASSWORD'])

def _register_routes(self):
@self.route('/submit', methods=['POST'], endpoint='submit')
@request_content_type('application/octet-stream')
@response_content_type('text/plain')
def submit():
document = request.get_data()
if not document:
raise BadRequest()
process_id = self._process.submit(document)
return Response(process_id, 200, mimetype='text/plain')

@self.route('/retrieve/<process_id>', endpoint='retrieve')
@request_content_type('')
@response_content_type('text/xml')
def retrieve(process_id):
result = self._process.retrieve(process_id)
if result is None:
raise NotFound()
return Response(result, 200, mimetype='text/plain')
2 changes: 1 addition & 1 deletion tk/flask/entry_point.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from tk.flask.app import App

app = App('tk')
app = App()
52 changes: 52 additions & 0 deletions tk/process.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import uuid
from queue import Queue
from threading import Thread


class Process:
def __init__(self, session, sourcebox_url, sourcebox_account_name, sourcebox_user_name, sourcebox_password):
self._processes = {}
self._process_queue = Queue()
self._session = session
self._sourcebox_url = sourcebox_url
self._sourcebox_account_name = sourcebox_account_name
self._sourcebox_user_name = sourcebox_user_name
self._sourcebox_password = sourcebox_password

worker = Thread(target=self._process_queue_worker,
args=(self._process_queue,))
worker.setDaemon(True)
worker.start()

def _process_queue_worker(self, queue):
while True:
process_id, profile = queue.get()
self._processes[process_id] = profile
queue.task_done()

def submit(self, document):
process_id = str(uuid.uuid4())
self._processes[process_id] = 'PROGRESS'
self._session.post(self._sourcebox_url, data={
'account': self._sourcebox_account_name,
'username': self._sourcebox_user_name,
'password': self._sourcebox_password,
}, files={
'uploaded_file': document,
}, params={
'useHttpErrorCodes': 'true',
'useJsonErrorMsg': 'true',
}, background_callback=self._handle_submit_response(process_id))
return process_id

def _handle_submit_response(self, process_id):
queue = self._process_queue

def _handler(session, response):
queue.put((process_id, response.text))
return _handler

def retrieve(self, process_id):
if process_id not in self._processes:
return None
return self._processes[process_id]
74 changes: 74 additions & 0 deletions tk/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from unittest import TestCase

import requests
import requests_mock

from tk.flask.app import App


def data_provider(data_provider):
"""
Provides a test method with test data.

Applying this decorator to a test method causes that method to be run as
many times as the data provider callable returns dictionary items.
Failed assertions will include information about which data set failed.

:param data_provider: A callable that generates the test data as a
dictionary of tuples containing the test method arguments, keyed by data
set name.
:return:
"""
def decorator(test_method):
"""
The actual decorator.
:param test_method: The test method to decorate.
:return:
"""

def multiplier(self, *test_method_args, **test_method_kwargs):
"""
The replacement (decorated) test method.
:param self:
:param test_method_args: The arguments to the decorated test
method.
:param test_method_kwargs: The keyword arguments to the decorated
test method.
:return:
"""
for fixture_name, test_method_fixture_args in data_provider().items():
try:
test_method(self, *test_method_args,
*test_method_fixture_args,
**test_method_kwargs)
except AssertionError:
raise AssertionError(
'Assertion failed with data set "%s"' % str(fixture_name))
except Exception:
raise AssertionError(
'Unexpected error with data set "%s"' % str(
fixture_name))

return multiplier

return decorator


class IntegrationTestCase(TestCase):
"""
Provides scaffolding for light-weight integration tests.
"""

def setUp(self):
self._flask_app = App()
self._flask_app.config.update(SERVER_NAME='example.com')
self._flask_app_context = self._flask_app.app_context()
self._flask_app_context.push()
self._flask_app_client = self._flask_app.test_client()

session = requests.Session()
adapter = requests_mock.Adapter()
session.mount('mock', adapter)

def tearDown(self):
self._flask_app_context.pop()
Loading