web-dev-qa-db-fra.com

Récupérer un fichier à partir d'une URL locale avec Python?

J'utilise la bibliothèque requêtes de Python dans une méthode de mon application. Le corps de la méthode ressemble à ceci:

def handle_remote_file(url, **kwargs):
    response = requests.get(url, ...)
    buff = StringIO.StringIO()
    buff.write(response.content)
    ...
    return True

J'aimerais écrire des tests unitaires pour cette méthode, cependant, ce que je veux faire, c'est passer une fausse URL locale telle que:

class RemoteTest(TestCase):
    def setUp(self):
        self.url = 'file:///tmp/dummy.txt'

    def test_handle_remote_file(self):
        self.assertTrue(handle_remote_file(self.url))

Lorsque j'appelle requests.get avec une URL locale, j'ai le KeyError exception ci-dessous:

requests.get('file:///tmp/dummy.txt')

/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/requests/packages/urllib3/poolmanager.pyc in connection_from_Host(self, Host, port, scheme)
76 
77         # Make a fresh ConnectionPool of the desired type
78         pool_cls = pool_classes_by_scheme[scheme]
79         pool = pool_cls(Host, port, **self.connection_pool_kw)
80 

KeyError: 'file'

La question est de savoir comment puis-je transmettre une URL locale à requests.get ?

PS: J'ai inventé l'exemple ci-dessus. Il peut contenir de nombreuses erreurs.

24
ozgur

Comme @WooParadog l'a expliqué, la bibliothèque de requêtes ne sait pas comment gérer les fichiers locaux. Cependant, la version actuelle permet de définir adaptateurs de transport .

Par conséquent, vous pouvez simplement définir votre propre adaptateur qui sera capable de gérer les fichiers locaux, par exemple:

from requests_testadapter import Resp

class LocalFileAdapter(requests.adapters.HTTPAdapter):
    def build_response_from_file(self, request):
        file_path = request.url[7:]
        with open(file_path, 'rb') as file:
            buff = bytearray(os.path.getsize(file_path))
            file.readinto(buff)
            resp = Resp(buff)
            r = self.build_response(request, resp)

            return r

    def send(self, request, stream=False, timeout=None,
             verify=True, cert=None, proxies=None):

        return self.build_response_from_file(request)

requests_session = requests.session()
requests_session.mount('file://', LocalFileAdapter())
requests_session.get('file://<some_local_path>')

J'utilise le module requests-testadapter dans l'exemple ci-dessus.

30
b1r3k

Voici un adaptateur de transport que j'ai écrit qui est plus fonctionnel que celui de b1r3k et n'a pas de dépendances supplémentaires au-delà de Requests lui-même. Je ne l'ai pas encore testé de manière exhaustive, mais ce que j'ai essayé semble être sans bug.

import requests
import os, sys

if sys.version_info.major < 3:
    from urllib import url2pathname
else:
    from urllib.request import url2pathname

class LocalFileAdapter(requests.adapters.BaseAdapter):
    """Protocol Adapter to allow Requests to GET file:// URLs

    @todo: Properly handle non-empty hostname portions.
    """

    @staticmethod
    def _chkpath(method, path):
        """Return an HTTP status for the given filesystem path."""
        if method.lower() in ('put', 'delete'):
            return 501, "Not Implemented"  # TODO
        Elif method.lower() not in ('get', 'head'):
            return 405, "Method Not Allowed"
        Elif os.path.isdir(path):
            return 400, "Path Not A File"
        Elif not os.path.isfile(path):
            return 404, "File Not Found"
        Elif not os.access(path, os.R_OK):
            return 403, "Access Denied"
        else:
            return 200, "OK"

    def send(self, req, **kwargs):  # pylint: disable=unused-argument
        """Return the file specified by the given request

        @type req: C{PreparedRequest}
        @todo: Should I bother filling `response.headers` and processing
               If-Modified-Since and friends using `os.stat`?
        """
        path = os.path.normcase(os.path.normpath(url2pathname(req.path_url)))
        response = requests.Response()

        response.status_code, response.reason = self._chkpath(req.method, path)
        if response.status_code == 200 and req.method.lower() != 'head':
            try:
                response.raw = open(path, 'rb')
            except (OSError, IOError) as err:
                response.status_code = 500
                response.reason = str(err)

        if isinstance(req.url, bytes):
            response.url = req.url.decode('utf-8')
        else:
            response.url = req.url

        response.request = req
        response.connection = self

        return response

    def close(self):
        pass

(Malgré le nom, il a été complètement écrit avant de penser à vérifier Google, donc cela n'a rien à voir avec b1r3k.) Comme avec l'autre réponse, suivez ceci avec:

requests_session = requests.session()
requests_session.mount('file://', LocalFileAdapter())
r = requests_session.get('file:///path/to/your/file')
16
ssokolow

packages/urllib3/poolmanager.py l'explique à peu près. Les demandes ne prennent pas en charge l'URL locale.

pool_classes_by_scheme = {                                                        
    'http': HTTPConnectionPool,                                                   
    'https': HTTPSConnectionPool,                                              
}                                                                                 
9
WooParadog

Dans un projet récent, j'ai eu le même problème. Étant donné que les requêtes ne prennent pas en charge le schéma "fichier", je patcherai notre code pour charger le contenu localement. Tout d'abord, je définis une fonction pour remplacer requests.get:

def local_get(self, url):
    "Fetch a stream from local files."
    p_url = six.moves.urllib.parse.urlparse(url)
    if p_url.scheme != 'file':
        raise ValueError("Expected file scheme")

    filename = six.moves.urllib.request.url2pathname(p_url.path)
    return open(filename, 'rb')

Ensuite, quelque part dans la configuration du test ou la décoration de la fonction de test, j'utilise mock.patch pour patcher la fonction get sur les requêtes:

@mock.patch('requests.get', local_get)
def test_handle_remote_file(self):
    ...

Cette technique est quelque peu fragile - elle n'aide pas si le code sous-jacent appelle requests.request ou construit un Session et appelle cela. Il existe peut-être un moyen de corriger les demandes à un niveau inférieur pour prendre en charge file: URL, mais dans mon enquête initiale, il ne semblait pas y avoir de crochet évident, alors j'ai opté pour cette approche plus simple.

6
Jason R. Coombs