Merge with trunk

This commit is contained in:
Pablo Hoffman 2010-09-15 01:50:42 -03:00
commit beee7e4fbc
20 changed files with 259 additions and 50 deletions

View File

@ -1,6 +1,6 @@
@ECHO off
SET test="scrapy"
SET test="scrapy scrapyd"
IF NOT "%1" == "" SET test="%1"
IF EXIST c:\python26\scripts\trial.py GOTO py26

View File

@ -40,7 +40,7 @@ fi
find -name '*.py[co]' -delete
if [ $# -eq 0 ]; then
$trial scrapy
$trial scrapy scrapyd
else
$trial "$@"
fi

View File

@ -2,7 +2,7 @@
Scrapy - a screen scraping framework written in Python
"""
version_info = (0, 10, 1, 'dev')
version_info = (0, 10, 1, '')
__version__ = "0.10.1"
import sys, os, warnings

View File

@ -3,6 +3,7 @@ from gzip import GzipFile
from cStringIO import StringIO
from scrapy.http import Response
from scrapy.core.downloader.responsetypes import responsetypes
class HttpCompressionMiddleware(object):
@ -18,7 +19,9 @@ class HttpCompressionMiddleware(object):
if content_encoding:
encoding = content_encoding.pop()
decoded_body = self._decode(response.body, encoding.lower())
response = response.replace(body=decoded_body)
respcls = responsetypes.from_args(headers=response.headers, \
url=response.url)
response = response.replace(cls=respcls, body=decoded_body)
if not content_encoding:
del response.headers['Content-Encoding']

View File

@ -46,9 +46,11 @@ class ResponseTypes(object):
basetype = "%s/*" % mimetype.split('/')[0]
return self.classes.get(basetype, Response)
def from_content_type(self, content_type):
def from_content_type(self, content_type, content_encoding=None):
"""Return the most appropiate Response class from an HTTP Content-Type
header """
if content_encoding:
return Response
mimetype = content_type.split(';')[0].strip().lower()
return self.from_mimetype(mimetype)
@ -65,7 +67,8 @@ class ResponseTypes(object):
headers"""
cls = Response
if 'Content-Type' in headers:
cls = self.from_content_type(headers['Content-type'])
cls = self.from_content_type(headers['Content-type'], \
headers.get('Content-Encoding'))
if cls is Response and 'Content-Disposition' in headers:
cls = self.from_content_disposition(headers['Content-Disposition'])
return cls

View File

@ -2,11 +2,14 @@ from __future__ import with_statement
from unittest import TestCase
from os.path import join, abspath, dirname
from cStringIO import StringIO
from gzip import GzipFile
from scrapy.spider import BaseSpider
from scrapy.http import Response, Request
from scrapy.http import Response, Request, HtmlResponse
from scrapy.contrib.downloadermiddleware.httpcompression import HttpCompressionMiddleware
from scrapy.tests import tests_datadir
from scrapy.utils.encoding import resolve_encoding
SAMPLEDIR = join(tests_datadir, 'compressed')
@ -96,3 +99,22 @@ class HttpCompressionTest(TestCase):
newresponse = self.mw.process_response(request, response, self.spider)
assert newresponse is not response
self.assertEqual(newresponse.headers.getlist('Content-Encoding'), ['uuencode'])
def test_process_response_encoding_inside_body(self):
headers = {
'Content-Type': 'text/html',
'Content-Encoding': 'gzip',
}
f = StringIO()
plainbody = """<html><head><title>Some page</title><meta http-equiv="Content-Type" content="text/html; charset=gb2312">"""
zf = GzipFile(fileobj=f, mode='wb')
zf.write(plainbody)
zf.close()
response = Response("http;//www.example.com/", headers=headers, body=f.getvalue())
request = Request("http://www.example.com/")
newresponse = self.mw.process_response(request, response, self.spider)
assert isinstance(newresponse, HtmlResponse)
self.assertEqual(newresponse.body, plainbody)
self.assertEqual(newresponse.encoding, resolve_encoding('gb2312'))

View File

@ -54,6 +54,7 @@ class ResponseTypesTest(unittest.TestCase):
mappings = [
({'Content-Type': ['text/html; charset=utf-8']}, HtmlResponse),
({'Content-Type': ['application/octet-stream'], 'Content-Disposition': ['attachment; filename=data.txt']}, TextResponse),
({'Content-Type': ['text/html; charset=utf-8'], 'Content-Encoding': ['gzip']}, Response),
]
for source, cls in mappings:
source = Headers(source)

View File

@ -1,7 +1,7 @@
import warnings
from functools import wraps
from twisted.internet.defer import maybeDeferred
from twisted.internet import defer, threads
def deprecated(use_instead=None):
@ -24,5 +24,14 @@ def defers(func):
"""Decorator to make sure a function always returns a deferred"""
@wraps(func)
def wrapped(*a, **kw):
return maybeDeferred(func, *a, **kw)
return defer.maybeDeferred(func, *a, **kw)
return wrapped
def inthread(func):
"""Decorator to call a function in a thread and return a deferred with the
result
"""
@wraps(func)
def wrapped(*a, **kw):
return threads.deferToThread(func, *a, **kw)
return wrapped

View File

@ -9,12 +9,16 @@ class Config(object):
SECTION = 'scrapyd'
def __init__(self):
sources = self._getsources()
default_config = pkgutil.get_data(__package__, 'default_scrapyd.conf')
self.cp = SafeConfigParser()
self.cp.readfp(StringIO(default_config))
self.cp.read(sources)
def __init__(self, values=None):
if values is None:
sources = self._getsources()
default_config = pkgutil.get_data(__package__, 'default_scrapyd.conf')
self.cp = SafeConfigParser()
self.cp.readfp(StringIO(default_config))
self.cp.read(sources)
else:
self.cp = SafeConfigParser(values)
self.cp.add_section(self.SECTION)
def _getsources(self):
sources = ['/etc/scrapyd/scrapyd.conf', r'c:\scrapyd\scrapyd.conf']

View File

@ -1,27 +1,9 @@
"""
This module can be used to run a Scrapy project contained in an egg file
To see all spiders in a project:
python -m scrapyd.eggrunner myproject.egg list
To crawl a spider:
python -m scrapyd.eggrunner myproject.egg crawl somespider
"""
import sys
import os
from scrapyd.eggutils import activate_egg
def main(eggpath, args):
"""Run scrapy for the settings module name passed"""
eggpath = os.environ.get('SCRAPY_EGGFILE')
if eggpath:
activate_egg(eggpath)
from scrapy.cmdline import execute
execute(['scrapy'] + list(args))
if __name__ == '__main__':
if len(sys.argv) < 2:
print "usage: %s <eggfile> [scrapy_command args ...]" % sys.argv[0]
sys.exit(1)
main(sys.argv[1], sys.argv[2:])
from scrapy.cmdline import execute
execute()

View File

@ -6,11 +6,10 @@ from shutil import copyfileobj, rmtree
from distutils.version import LooseVersion
from zope.interface import implements
from twisted.application.service import Service
from .interfaces import IEggStorage
class FilesystemEggStorage(Service):
class FilesystemEggStorage(object):
implements(IEggStorage)
@ -27,7 +26,10 @@ class FilesystemEggStorage(Service):
def get(self, project, version=None):
if version is None:
version = self.list(project)[-1]
try:
version = self.list(project)[-1]
except IndexError:
return None, None
return version, open(self._eggpath(project, version), 'rb')
def list(self, project):

View File

@ -12,9 +12,10 @@ def get_spider_list_from_eggfile(eggfile, project):
shutil.copyfileobj(eggfile, f)
f.flush()
eggfile.seek(0)
pargs = [sys.executable, '-m', 'scrapyd.eggrunner', f.name, 'list']
pargs = [sys.executable, '-m', 'scrapyd.eggrunner', 'list']
env = os.environ.copy()
env['SCRAPY_PROJECT'] = project
env['SCRAPY_EGGFILE'] = f.name
proc = Popen(pargs, stdout=PIPE, cwd=tmpdir, env=env)
out = proc.communicate()[0]
return out.splitlines()

View File

@ -11,11 +11,19 @@ class Environment(object):
def __init__(self, config):
self.dbs_dir = config.get('dbs_dir', 'dbs')
self.logs_dir = config.get('logs_dir', 'logs')
if config.cp.has_section('settings'):
self.settings = dict(config.cp.items('settings'))
else:
self.settings = {}
def get_environment(self, message, slot):
def get_environment(self, message, slot, eggpath):
project = message['project']
env = os.environ.copy()
env['SCRAPY_PROJECT'] = project
if eggpath:
env['SCRAPY_EGGFILE'] = eggpath
elif project in self.settings:
env['SCRAPY_SETTINGS_MODULE'] = self.settings[project]
dbpath = os.path.join(self.dbs_dir, '%s.db' % project)
env['SCRAPY_SQLITE_DB'] = dbpath
logpath = os.path.join(self.logs_dir, 'slot%s.log' % slot)

View File

@ -10,13 +10,14 @@ class IEggStorage(Interface):
def get(project, version=None):
"""Return a tuple (version, file) with the the egg for the specified
project and version. If version is None, the latest version is
returned."""
returned. If no egg is found for the given project/version (None, None)
should be returned."""
def list(self, project):
def list(project):
"""Return the list of versions which have eggs stored (for the given
project) in order (the latest version is the currently used)."""
def delete(self, project, version=None):
def delete(project, version=None):
"""Delete the egg stored for the given project and version. If should
also delete the project if no versions are left"""
@ -61,9 +62,13 @@ class ISpiderScheduler(Interface):
class IEnvironment(Interface):
"""A component to generate the environment of crawler processes"""
def get_environment(message, slot):
def get_environment(message, slot, eggpath):
"""Return the environment variables to use for running the process.
`message` is the message received from the IPoller.next()
`slot` is the Launcher slot where the process will be running.
`eggpath` is the path to an eggfile that contains the project code. The
`eggpath` may be `None` if no egg was found for the project, in
which case the project must be on the python path and its settings
defined in scrapyd.conf [settings] section
"""

View File

@ -39,15 +39,16 @@ class Launcher(Service):
def _spawn_process(self, message, slot):
project = message['project']
eggpath = self._get_eggpath(project)
args = [sys.executable, '-m', self.egg_runner, eggpath, 'crawl']
args = [sys.executable, '-m', self.egg_runner, 'crawl']
e = self.app.getComponent(IEnvironment)
env = e.get_environment(message, slot)
env = e.get_environment(message, slot, eggpath)
pp = ScrapyProcessProtocol(eggpath, slot)
pp.deferred.addBoth(self._process_finished, eggpath, slot)
reactor.spawnProcess(pp, sys.executable, args=args, env=env)
def _process_finished(self, _, eggpath, slot):
os.remove(eggpath)
if eggpath:
os.remove(eggpath)
self._wait_for_project(slot)

View File

View File

@ -0,0 +1,41 @@
from cStringIO import StringIO
from twisted.trial import unittest
from zope.interface.verify import verifyObject
from scrapyd.interfaces import IEggStorage
from scrapyd.config import Config
from scrapyd.eggstorage import FilesystemEggStorage
class EggStorageTest(unittest.TestCase):
def setUp(self):
d = self.mktemp()
config = Config(values={'eggs_dir': d})
self.eggst = FilesystemEggStorage(config)
def test_interface(self):
verifyObject(IEggStorage, self.eggst)
def test_put_get_list_delete(self):
self.eggst.put(StringIO("egg01"), 'mybot', '01')
self.eggst.put(StringIO("egg03"), 'mybot', '03')
self.eggst.put(StringIO("egg02"), 'mybot', '02')
self.assertEqual(self.eggst.list('mybot'), ['01', '02', '03'])
self.assertEqual(self.eggst.list('mybot2'), [])
v, f = self.eggst.get('mybot')
self.assertEqual(v, "03")
self.assertEqual(f.read(), "egg03")
v, f = self.eggst.get('mybot', '02')
self.assertEqual(v, "02")
self.assertEqual(f.read(), "egg02")
self.eggst.delete('mybot', '02')
self.assertEqual(self.eggst.list('mybot'), ['01', '03'])
self.eggst.delete('mybot')
self.assertEqual(self.eggst.list('mybot'), [])

View File

@ -0,0 +1,42 @@
import os
from twisted.trial import unittest
from zope.interface.verify import verifyObject
from scrapyd.interfaces import IEnvironment
from scrapyd.config import Config
from scrapyd.environ import Environment
class EggStorageTest(unittest.TestCase):
def setUp(self):
d = self.mktemp()
os.mkdir(d)
config = Config(values={'eggs_dir': d, 'logs_dir': d})
config.cp.add_section('settings')
config.cp.set('settings', 'newbot', 'newbot.settings')
self.environ = Environment(config)
def test_interface(self):
verifyObject(IEnvironment, self.environ)
def test_get_environment_with_eggfile(self):
msg = {'project': 'mybot'}
slot = 3
env = self.environ.get_environment(msg, slot, '/path/to/file.egg')
self.assertEqual(env['SCRAPY_PROJECT'], 'mybot')
self.assert_(env['SCRAPY_SQLITE_DB'].endswith('mybot.db'))
self.assert_(env['SCRAPY_LOG_FILE'].endswith('slot3.log'))
self.assert_(env['SCRAPY_EGGFILE'].endswith('/path/to/file.egg'))
self.failIf('SCRAPY_SETTINGS_MODULE' in env)
def test_get_environment_without_eggfile(self):
msg = {'project': 'newbot'}
slot = 3
env = self.environ.get_environment(msg, slot, None)
self.assertEqual(env['SCRAPY_PROJECT'], 'newbot')
self.assert_(env['SCRAPY_SQLITE_DB'].endswith('newbot.db'))
self.assert_(env['SCRAPY_LOG_FILE'].endswith('slot3.log'))
self.assertEqual(env['SCRAPY_SETTINGS_MODULE'], 'newbot.settings')
self.failIf('SCRAPY_EGGFILE' in env)

View File

@ -0,0 +1,41 @@
import os
from twisted.trial import unittest
from twisted.internet.defer import Deferred
from zope.interface.verify import verifyObject
from scrapyd.interfaces import IPoller
from scrapyd.config import Config
from scrapyd.poller import QueuePoller
from scrapyd.utils import get_spider_queues
class QueuePollerTest(unittest.TestCase):
def setUp(self):
d = self.mktemp()
eggs_dir = os.path.join(d, 'eggs')
dbs_dir = os.path.join(d, 'dbs')
os.makedirs(eggs_dir)
os.makedirs(dbs_dir)
os.makedirs(os.path.join(eggs_dir, 'mybot1'))
os.makedirs(os.path.join(eggs_dir, 'mybot2'))
config = Config(values={'eggs_dir': eggs_dir, 'dbs_dir': dbs_dir})
self.queues = get_spider_queues(eggs_dir, dbs_dir)
self.poller = QueuePoller(config)
def test_interface(self):
verifyObject(IPoller, self.poller)
def test_poll_next(self):
self.queues['mybot1'].add('spider1')
self.queues['mybot2'].add('spider2')
d1 = self.poller.next()
d2 = self.poller.next()
self.failUnless(isinstance(d1, Deferred))
self.failIf(hasattr(d1, 'result'))
self.poller.poll()
self.queues['mybot1'].pop()
self.poller.poll()
self.failUnlessEqual(d1.result, {'project': 'mybot1'})
self.failUnlessEqual(d2.result, {'project': 'mybot2'})

View File

@ -0,0 +1,44 @@
import os
from twisted.trial import unittest
from zope.interface.verify import verifyObject
from scrapyd.interfaces import ISpiderScheduler
from scrapyd.config import Config
from scrapyd.scheduler import SpiderScheduler
from scrapyd.utils import get_spider_queues
class SpiderSchedulerTest(unittest.TestCase):
def setUp(self):
d = self.mktemp()
eggs_dir = self.eggs_dir = os.path.join(d, 'eggs')
dbs_dir = os.path.join(d, 'dbs')
os.mkdir(d)
os.makedirs(eggs_dir)
os.makedirs(dbs_dir)
os.makedirs(os.path.join(eggs_dir, 'mybot1'))
os.makedirs(os.path.join(eggs_dir, 'mybot2'))
config = Config(values={'eggs_dir': eggs_dir, 'dbs_dir': dbs_dir})
self.queues = get_spider_queues(eggs_dir, dbs_dir)
self.sched = SpiderScheduler(config)
def test_interface(self):
verifyObject(ISpiderScheduler, self.sched)
def test_list_update_projects(self):
self.assertEqual(sorted(self.sched.list_projects()), sorted(['mybot1', 'mybot2']))
os.makedirs(os.path.join(self.eggs_dir, 'mybot3'))
self.sched.update_projects()
self.assertEqual(sorted(self.sched.list_projects()), sorted(['mybot1', 'mybot2', 'mybot3']))
def test_schedule(self):
q = self.queues['mybot1']
self.failIf(q.count())
self.sched.schedule('mybot1', 'myspider1', a='b')
self.sched.schedule('mybot2', 'myspider2', c='d')
self.assertEqual(q.pop(), {'name': 'myspider1', 'a': 'b'})
q = self.queues['mybot2']
self.assertEqual(q.pop(), {'name': 'myspider2', 'c': 'd'})