diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..bbd083a --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts = --tb=short diff --git a/requests_unixsocket/__init__.py b/requests_unixsocket/__init__.py new file mode 100644 index 0000000..0d585cf --- /dev/null +++ b/requests_unixsocket/__init__.py @@ -0,0 +1,3 @@ +from .adapters import UnixAdapter + +__all__ = ['UnixAdapter'] diff --git a/requests_unixsocket/adapters.py b/requests_unixsocket/adapters.py new file mode 100644 index 0000000..9fc1a16 --- /dev/null +++ b/requests_unixsocket/adapters.py @@ -0,0 +1,54 @@ +import socket + +from requests.adapters import HTTPAdapter +from requests.compat import urlparse, unquote +from requests.packages.urllib3.connection import HTTPConnection +from requests.packages.urllib3.connectionpool import HTTPConnectionPool + + +# The following was adapted from some code from docker-py +# https://github.com/docker/docker-py/blob/master/docker/unixconn/unixconn.py +class UnixHTTPConnection(HTTPConnection): + def __init__(self, unix_socket_url, timeout=60): + """Create an HTTP connection to a unix domain socket + + :param unix_socket_url: A URL with a scheme of 'http+unix' and the + netloc is a percent-encoded path to a unix domain socket. E.g.: + 'http+unix://%2Ftmp%2Fprofilesvc.sock/status/pid' + """ + HTTPConnection.__init__(self, 'localhost', timeout=timeout) + self.unix_socket_url = unix_socket_url + self.timeout = timeout + + def connect(self): + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.settimeout(self.timeout) + socket_path = unquote(urlparse(self.unix_socket_url).netloc) + sock.connect(socket_path) + self.sock = sock + + def request(self, method, url, **kwargs): + url = urlparse(url).path + HTTPConnection.request(self, method, url, **kwargs) + + +class UnixHTTPConnectionPool(HTTPConnectionPool): + def __init__(self, socket_path, timeout=60): + HTTPConnectionPool.__init__(self, 'localhost', timeout=timeout) + self.socket_path = socket_path + self.timeout = timeout + + def _new_conn(self): + return UnixHTTPConnection(self.socket_path, self.timeout) + + +class UnixAdapter(HTTPAdapter): + def __init__(self, timeout=60): + super(UnixAdapter, self).__init__() + self.timeout = timeout + + def get_connection(self, socket_path, proxies=None): + if proxies: + raise ValueError('%s does not support specifying proxies' + % self.__class__.__name__) + return UnixHTTPConnectionPool(socket_path, self.timeout) diff --git a/requests_unixsocket/tests/test_requests_unixsocket.py b/requests_unixsocket/tests/test_requests_unixsocket.py new file mode 100755 index 0000000..118197d --- /dev/null +++ b/requests_unixsocket/tests/test_requests_unixsocket.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""Tests for requests_unixsocket""" + +import multiprocessing +import os +import uuid + +import pytest +import requests +import waitress + +from requests_unixsocket import UnixAdapter + + +@pytest.fixture +def wsgiapp(): + def _wsgiapp(environ, start_response): + start_response( + '200 OK', + [('X-Transport', 'unix domain socket'), + ('X-Socket-Path', environ['SERVER_PORT']), + ('X-Requested-Path', environ['PATH_INFO'])]) + return ['Hello world!'] + + return _wsgiapp + + +@pytest.fixture +def usock_process(wsgiapp): + class UnixSocketServerProcess(multiprocessing.Process): + def __init__(self, *args, **kwargs): + super(UnixSocketServerProcess, self).__init__(*args, **kwargs) + self.unix_socket = self.get_tempfile_name() + + def get_tempfile_name(self): + # I'd rather use tempfile.NamedTemporaryFile but IDNA limits + # the hostname to 63 characters and we'll get a "InvalidURL: + # URL has an invalid label" error if we exceed that. + args = (os.stat(__file__).st_ino, + os.getpid(), + uuid.uuid4().hex[-8:]) + return '/tmp/test_requests.%s_%s_%s' % args + + def run(self): + waitress.serve(wsgiapp, unix_socket=self.unix_socket) + + return UnixSocketServerProcess() + + +def test_unix_domain_adapter_ok(usock_process): + from requests.compat import quote_plus + + usock_process.start() + + try: + session = requests.Session() + session.mount('http+unix://', UnixAdapter()) + urlencoded_socket_name = quote_plus(usock_process.unix_socket) + url = 'http+unix://%s/path/to/page' % urlencoded_socket_name + r = session.get(url) + assert r.status_code == 200 + assert r.headers['server'] == 'waitress' + assert r.headers['X-Transport'] == 'unix domain socket' + assert r.headers['X-Requested-Path'] == '/path/to/page' + assert r.headers['X-Socket-Path'] == usock_process.unix_socket + assert isinstance(r.connection, UnixAdapter) + assert r.url == url + assert r.text == 'Hello world!' + finally: + usock_process.terminate() + + +def test_unix_domain_adapter_connection_error(): + session = requests.Session() + session.mount('http+unix://', UnixAdapter()) + + with pytest.raises(requests.ConnectionError): + session.get('http+unix://socket_does_not_exist/path/to/page') diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..944892c --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +requests>=1.1 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..6220576 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,28 @@ +[metadata] +name = requests-unixsocket +author = Marc Abramowitz +author-email = marc@marc-abramowitz.com +summary = Use requests to talk HTTP via a UNIX domain socket +description-file = README.rst +license = Apache-2 +home-page = https://github.com/msabramo/requests-unixsocket +# home-page = https://requests-unixsocket.readthedocs.org/ +classifier = + Development Status :: 3 - Alpha + Intended Audience :: Developers + Intended Audience :: Information Technology + License :: OSI Approved :: Apache Software License + Operating System :: OS Independent + Programming Language :: Python + Programming Language :: Python :: 2 + Programming Language :: Python :: 2.7 + Programming Language :: Python :: 2.6 + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.3 +test_suite = requests_unixsocket.tests + +[files] +packages = requests_unixsocket + +[wheel] +universal = 1 diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..aa2d8a0 --- /dev/null +++ b/setup.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python + +from setuptools import setup + +setup( + setup_requires=['pbr'], + pbr=True, +) diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..2c9feb4 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,2 @@ +pytest +waitress diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..ab47b2f --- /dev/null +++ b/tox.ini @@ -0,0 +1,43 @@ +[tox] +envlist = py26, py27, py33, py34, pypy, pep8 + +[testenv] +commands = py.test {posargs:requests_unixsocket/tests} +deps = + -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt + +[testenv:pep8] +commands = flake8 requests_unixsocket +deps = + flake8 + {[testenv]deps} + +[testenv:venv] +commands = {posargs} + +[testenv:coverage] +commands = + coverage erase + coverage run --source requests_unixsocket -m py.test requests_unixsocket/tests + coverage html +deps = + coverage + {[testenv]deps} + +[testenv:doctest] +# note this only works under python 3 because of unicode literals +commands = + python -m doctest README.rst + +[testenv:sphinx-doctest] +# note this only works under python 3 because of unicode literals +commands = + mkdir build/sphinx/doctest + sphinx-build -b doctest docs build/sphinx/doctest +deps = + pbr + {[testenv]deps} + +[testenv:docs] +commands = python setup.py build_sphinx