Skip to content

Commit

Permalink
Merge pull request #140 from kalaspuff/feature/http-static
Browse files Browse the repository at this point in the history
Static file handler in HTTP transport class. Fixes for param issues in all transports.
  • Loading branch information
kalaspuff authored Aug 5, 2017
2 parents 5340ec7 + b2c7fa0 commit f9e7d74
Show file tree
Hide file tree
Showing 9 changed files with 110 additions and 7 deletions.
1 change: 1 addition & 0 deletions examples/amqp_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class ExampleAmqpService(object):

@amqp('example.route1')
async def route1a(self, data: Any) -> None:
banana = True
self.logger.info('Received data (function: route1a) - "{}"'.format(data))

@amqp('example.route1')
Expand Down
6 changes: 5 additions & 1 deletion examples/http_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from aiohttp import web
from tomodachi.discovery.dummy_registry import DummyRegistry
from tomodachi.protocol.json_base import JsonBase
from tomodachi.transport.http import http, http_error, Response
from tomodachi.transport.http import http, http_error, http_static, Response


@tomodachi.service
Expand Down Expand Up @@ -38,6 +38,10 @@ async def example_with_id(self, request: web.Request, id: str) -> str:
async def response_object(self, request: web.Request) -> Response:
return Response(body='{"data": true}', status=200, content_type='application/json')

@http_static('static/', r'/static/')
async def static_files(self) -> None:
pass

@http_error(status_code=404)
async def error_404(self, request: web.Request) -> str:
return 'error 404'
10 changes: 9 additions & 1 deletion tests/services/http_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import tomodachi
from typing import Any, Dict, Tuple # noqa
from aiohttp import web
from tomodachi.transport.http import http, http_error, Response
from tomodachi.transport.http import http, http_error, http_static, Response
from tomodachi.discovery.dummy_registry import DummyRegistry


Expand Down Expand Up @@ -124,6 +124,14 @@ async def forwarded_for(self, request: web.Request) -> str:
async def authorization(self, request: web.Request) -> str:
return request.auth.login if request.auth else ''

@http_static('../static_files', r'/static/')
async def static_files_filename_append(self) -> None:
pass

@http_static('../static_files', r'/download/(?P<filename>[^/]+?)/image')
async def static_files_filename_existing(self) -> None:
pass

@http_error(status_code=404)
async def test_404(self, request: web.Request) -> str:
return 'test 404'
Expand Down
Binary file added tests/static_files/image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 35 additions & 0 deletions tests/test_http_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import pytest
import os
import logging
import pathlib
import mimetypes
from typing import Any
from multidict import CIMultiDictProxy
from run_test_service_helper import start_service
Expand Down Expand Up @@ -221,6 +223,39 @@ async def _async(loop: Any) -> None:
assert response.status == 200
assert await response.text() == ''

async with aiohttp.ClientSession(loop=loop) as client:
f = pathlib.Path('{}/tests/static_files/image.png'.format(os.path.realpath(os.getcwd()))).open('r')
ct, encoding = mimetypes.guess_type(str(f.name))

response = await client.get('http://127.0.0.1:{}/static/image.png'.format(port))
assert response is not None
assert response.status == 200
assert response.headers.get('Content-Type') == 'image/png'
assert response.headers.get('Content-Type') == ct

with open(str(f.name), 'rb') as fobj:
data = fobj.read(20000)
assert (await response.read()) == data

async with aiohttp.ClientSession(loop=loop) as client:
f = pathlib.Path('{}/tests/static_files/image.png'.format(os.path.realpath(os.getcwd()))).open('r')
ct, encoding = mimetypes.guess_type(str(f.name))

response = await client.get('http://127.0.0.1:{}/download/image.png/image'.format(port))
assert response is not None
assert response.status == 200
assert response.headers.get('Content-Type') == 'image/png'
assert response.headers.get('Content-Type') == ct

with open(str(f.name), 'rb') as fobj:
data = fobj.read(20000)
assert (await response.read()) == data

async with aiohttp.ClientSession(loop=loop) as client:
response = await client.get('http://127.0.0.1:{}/static/image-404.png'.format(port))
assert response is not None
assert response.status == 404

loop.run_until_complete(_async(loop))
instance.stop_service()
loop.run_until_complete(future)
Expand Down
5 changes: 5 additions & 0 deletions tomodachi/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import traceback
import types
import uuid
import os
from types import ModuleType, TracebackType
from typing import Dict, Optional, Any, Type
from tomodachi import CLASS_ATTRIBUTE
Expand All @@ -16,6 +17,8 @@
class ServiceContainer(object):
def __init__(self, module_import: ModuleType, configuration: Optional[Dict]=None) -> None:
self.module_import = module_import

self.file_path = '{}/{}.py'.format(os.path.realpath(os.getcwd()), module_import.__name__)
self.module_name = module_import.__name__.rsplit('/', 1)[1] if '/' in module_import.__name__ else module_import.__name__
self.configuration = configuration
self.logger = logging.getLogger('services.{}'.format(self.module_name))
Expand Down Expand Up @@ -69,6 +72,8 @@ async def run_until_complete(self) -> None:
if not getattr(instance, 'context', None):
setattr(instance, 'context', {i: getattr(instance, i) for i in dir(instance) if not callable(i) and not i.startswith("__") and not isinstance(getattr(instance, i), types.MethodType)})

getattr(instance, 'context', {})['_service_file_path'] = self.file_path

self.setup_configuration(instance)

try:
Expand Down
4 changes: 3 additions & 1 deletion tomodachi/transport/aws_sns_sqs.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import binascii
import ujson
import uuid
import inspect
from typing import Any, Dict, Union, Optional, Callable, Awaitable, List, Tuple, Match
from tomodachi.invoker import Invoker

Expand Down Expand Up @@ -92,7 +93,8 @@ async def handler(payload: Optional[str], receipt_handle: Optional[str]=None, qu

_callback_kwargs = callback_kwargs # type: Any
if not _callback_kwargs:
_callback_kwargs = {k: func.__defaults__[len(func.__defaults__) - len(func.__code__.co_varnames[1:]) + i] if func.__defaults__ and len(func.__defaults__) - len(func.__code__.co_varnames[1:]) + i >= 0 else None for i, k in enumerate(func.__code__.co_varnames[1:])}
values = inspect.getfullargspec(func)
_callback_kwargs = {k: values.defaults[i - len(values.args) + 1] if values.defaults and i >= len(values.args) - len(values.defaults) - 1 else None for i, k in enumerate(values.args[1:])} if values.args and len(values.args) > 1 else {}
else:
_callback_kwargs = {k: None for k in _callback_kwargs if k != 'self'}
kwargs = {k: v for k, v in _callback_kwargs.items()}
Expand Down
52 changes: 49 additions & 3 deletions tomodachi/transport/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@
import traceback
import time
import ipaddress
import os
import pathlib
import inspect
from logging.handlers import WatchedFileHandler
from typing import Any, Dict, List, Tuple, Union, Optional, Callable, Awaitable, SupportsInt # noqa
from multidict import CIMultiDict, CIMultiDictProxy
from aiohttp import web, web_server, web_protocol, web_urldispatcher, hdrs
from aiohttp.web_fileresponse import FileResponse
from aiohttp.http import HttpVersion
from aiohttp.helpers import BasicAuth
from tomodachi.invoker import Invoker
Expand Down Expand Up @@ -172,11 +176,16 @@ async def request_handler(cls: Any, obj: Any, context: Dict, func: Any, method:

async def handler(request: web.Request) -> web.Response:
result = compiled_pattern.match(request.path)
kwargs = {k: func.__defaults__[len(func.__defaults__) - len(func.__code__.co_varnames[1:]) + i] if func.__defaults__ and len(func.__defaults__) - len(func.__code__.co_varnames[1:]) + i >= 0 else None for i, k in enumerate(func.__code__.co_varnames[1:]) if i >= 1} # type: Dict
values = inspect.getfullargspec(func)
kwargs = {k: values.defaults[i] for i, k in enumerate(values.args[len(values.args) - len(values.defaults):])} if values.defaults else {}
if result:
for k, v in result.groupdict().items():
kwargs[k] = v
routine = func(*(obj, request,), **kwargs)

try:
routine = func(*(obj, request,), **kwargs)
except Exception as e:
print(e)
return_value = (await routine) if isinstance(routine, Awaitable) else routine # type: Union[str, bytes, Dict, List, Tuple, web.Response, Response]

if isinstance(return_value, Response):
Expand Down Expand Up @@ -218,6 +227,41 @@ async def handler(request: web.Request) -> web.Response:
start_func = cls.start_server(obj, context)
return (await start_func) if start_func else None

async def static_request_handler(cls: Any, obj: Any, context: Dict, func: Any, path: str, base_url: str) -> Any:
if '?P<filename>' not in base_url:
pattern = r'^{}(?P<filename>.+?)$'.format(re.sub(r'\$$', '', re.sub(r'^\^?(.*)$', r'\1', base_url)))
else:
pattern = r'^{}$'.format(re.sub(r'\$$', '', re.sub(r'^\^?(.*)$', r'\1', base_url)))
compiled_pattern = re.compile(pattern)

if path.startswith('/'):
path = os.path.dirname(path)
else:
path = '{}/{}'.format(os.path.dirname(context.get('context', {}).get('_service_file_path')), path)

if not path.endswith('/'):
path = '{}/'.format(path)

async def handler(request: web.Request) -> web.Response:
result = compiled_pattern.match(request.path)
filename = result.groupdict()['filename']
filepath = '{}{}'.format(path, filename)

try:
if os.path.isdir(filepath) or not os.path.exists(filepath):
raise web.HTTPNotFound()

pathlib.Path(filepath).open('r')
return FileResponse(filepath)
except PermissionError as e:
raise web.HTTPForbidden()

context['_http_routes'] = context.get('_http_routes', [])
context['_http_routes'].append(('GET', pattern, handler))

start_func = cls.start_server(obj, context)
return (await start_func) if start_func else None

async def error_handler(cls: Any, obj: Any, context: Dict, func: Any, status_code: int) -> Any:
default_content_type = context.get('options', {}).get('http', {}).get('content_type', 'text/plain')
default_charset = context.get('options', {}).get('http', {}).get('charset', 'utf-8')
Expand All @@ -231,7 +275,8 @@ async def error_handler(cls: Any, obj: Any, context: Dict, func: Any, status_cod
pass

async def handler(request: web.Request) -> web.Response:
kwargs = {k: func.__defaults__[len(func.__defaults__) - len(func.__code__.co_varnames[1:]) + i] if len(func.__defaults__) - len(func.__code__.co_varnames[1:]) + i >= 0 else None for i, k in enumerate(func.__code__.co_varnames[1:]) if i >= 1} # type: Dict
values = inspect.getfullargspec(func)
kwargs = {k: values.defaults[i] for i, k in enumerate(values.args[len(values.args) - len(values.defaults):])} if values.defaults else {}
routine = func(*(obj, request,), **kwargs)
return_value = (await routine) if isinstance(routine, Awaitable) else routine # type: Union[str, bytes, Dict, List, Tuple, web.Response, Response]

Expand Down Expand Up @@ -415,3 +460,4 @@ async def stop_service(*args: Any, **kwargs: Any) -> None:

http = HttpTransport.decorator(HttpTransport.request_handler)
http_error = HttpTransport.decorator(HttpTransport.error_handler)
http_static = HttpTransport.decorator(HttpTransport.static_request_handler)
4 changes: 3 additions & 1 deletion tomodachi/transport/schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import time
import pytz
import tzlocal
import inspect
from typing import Any, Dict, List, Union, Optional, Callable, Awaitable, Tuple # noqa
from tomodachi.invoker import Invoker
from tomodachi.helpers.crontab import get_next_datetime
Expand All @@ -13,7 +14,8 @@ class Scheduler(Invoker):

async def schedule_handler(cls: Any, obj: Any, context: Dict, func: Any, interval: Optional[Union[str, int]]=None, timestamp: Optional[str]=None, timezone: Optional[str]=None) -> Any:
async def handler() -> None:
kwargs = {k: func.__defaults__[len(func.__defaults__) - len(func.__code__.co_varnames[1:]) + i] if func.__defaults__ and len(func.__defaults__) - len(func.__code__.co_varnames[1:]) + i >= 0 else None for i, k in enumerate(func.__code__.co_varnames[1:])} # type: Dict
values = inspect.getfullargspec(func)
kwargs = {k: values.defaults[i] for i, k in enumerate(values.args[len(values.args) - len(values.defaults):])} if values.defaults else {}
routine = func(*(obj,), **kwargs)
try:
if isinstance(routine, Awaitable):
Expand Down

0 comments on commit f9e7d74

Please sign in to comment.